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) -> &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_to_active_pane(&mut self, item: Box<dyn ItemHandle>, cx: &mut WindowContext) {
1889 self.add_item(self.active_pane.clone(), item, cx)
1890 }
1891
1892 pub fn add_item(
1893 &mut self,
1894 pane: View<Pane>,
1895 item: Box<dyn ItemHandle>,
1896 cx: &mut WindowContext,
1897 ) {
1898 if let Some(text) = item.telemetry_event_text(cx) {
1899 self.client()
1900 .telemetry()
1901 .report_app_event(format!("{}: open", text));
1902 }
1903
1904 pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
1905 }
1906
1907 pub fn split_item(
1908 &mut self,
1909 split_direction: SplitDirection,
1910 item: Box<dyn ItemHandle>,
1911 cx: &mut ViewContext<Self>,
1912 ) {
1913 let new_pane = self.split_pane(self.active_pane.clone(), split_direction, cx);
1914 self.add_item(new_pane, item, cx);
1915 }
1916
1917 pub fn open_abs_path(
1918 &mut self,
1919 abs_path: PathBuf,
1920 visible: bool,
1921 cx: &mut ViewContext<Self>,
1922 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
1923 cx.spawn(|workspace, mut cx| async move {
1924 let open_paths_task_result = workspace
1925 .update(&mut cx, |workspace, cx| {
1926 workspace.open_paths(
1927 vec![abs_path.clone()],
1928 if visible {
1929 OpenVisible::All
1930 } else {
1931 OpenVisible::None
1932 },
1933 None,
1934 cx,
1935 )
1936 })
1937 .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
1938 .await;
1939 anyhow::ensure!(
1940 open_paths_task_result.len() == 1,
1941 "open abs path {abs_path:?} task returned incorrect number of results"
1942 );
1943 match open_paths_task_result
1944 .into_iter()
1945 .next()
1946 .expect("ensured single task result")
1947 {
1948 Some(open_result) => {
1949 open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
1950 }
1951 None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
1952 }
1953 })
1954 }
1955
1956 pub fn split_abs_path(
1957 &mut self,
1958 abs_path: PathBuf,
1959 visible: bool,
1960 cx: &mut ViewContext<Self>,
1961 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
1962 let project_path_task =
1963 Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
1964 cx.spawn(|this, mut cx| async move {
1965 let (_, path) = project_path_task.await?;
1966 this.update(&mut cx, |this, cx| this.split_path(path, cx))?
1967 .await
1968 })
1969 }
1970
1971 pub fn open_path(
1972 &mut self,
1973 path: impl Into<ProjectPath>,
1974 pane: Option<WeakView<Pane>>,
1975 focus_item: bool,
1976 cx: &mut WindowContext,
1977 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
1978 let pane = pane.unwrap_or_else(|| {
1979 self.last_active_center_pane.clone().unwrap_or_else(|| {
1980 self.panes
1981 .first()
1982 .expect("There must be an active pane")
1983 .downgrade()
1984 })
1985 });
1986
1987 let task = self.load_path(path.into(), cx);
1988 cx.spawn(move |mut cx| async move {
1989 let (project_entry_id, build_item) = task.await?;
1990 pane.update(&mut cx, |pane, cx| {
1991 pane.open_item(project_entry_id, focus_item, cx, build_item)
1992 })
1993 })
1994 }
1995
1996 pub fn split_path(
1997 &mut self,
1998 path: impl Into<ProjectPath>,
1999 cx: &mut ViewContext<Self>,
2000 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2001 let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
2002 self.panes
2003 .first()
2004 .expect("There must be an active pane")
2005 .downgrade()
2006 });
2007
2008 if let Member::Pane(center_pane) = &self.center.root {
2009 if center_pane.read(cx).items_len() == 0 {
2010 return self.open_path(path, Some(pane), true, cx);
2011 }
2012 }
2013
2014 let task = self.load_path(path.into(), cx);
2015 cx.spawn(|this, mut cx| async move {
2016 let (project_entry_id, build_item) = task.await?;
2017 this.update(&mut cx, move |this, cx| -> Option<_> {
2018 let pane = pane.upgrade()?;
2019 let new_pane = this.split_pane(pane, SplitDirection::Right, cx);
2020 new_pane.update(cx, |new_pane, cx| {
2021 Some(new_pane.open_item(project_entry_id, true, cx, build_item))
2022 })
2023 })
2024 .map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))?
2025 })
2026 }
2027
2028 fn load_path(
2029 &mut self,
2030 path: ProjectPath,
2031 cx: &mut WindowContext,
2032 ) -> Task<
2033 Result<(
2034 Option<ProjectEntryId>,
2035 impl 'static + Send + FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
2036 )>,
2037 > {
2038 let project = self.project().clone();
2039 let project_item = project.update(cx, |project, cx| project.open_path(path, cx));
2040 cx.spawn(|mut cx| async move {
2041 let (project_entry_id, project_item) = project_item.await?;
2042 let build_item = cx.update(|cx| {
2043 cx.default_global::<ProjectItemBuilders>()
2044 .get(&project_item.entity_type())
2045 .ok_or_else(|| anyhow!("no item builder for project item"))
2046 .cloned()
2047 })??;
2048 let build_item =
2049 move |cx: &mut ViewContext<Pane>| build_item(project, project_item, cx);
2050 Ok((project_entry_id, build_item))
2051 })
2052 }
2053
2054 pub fn open_project_item<T>(
2055 &mut self,
2056 pane: View<Pane>,
2057 project_item: Model<T::Item>,
2058 cx: &mut ViewContext<Self>,
2059 ) -> View<T>
2060 where
2061 T: ProjectItem,
2062 {
2063 use project::Item as _;
2064
2065 let entry_id = project_item.read(cx).entry_id(cx);
2066 if let Some(item) = entry_id
2067 .and_then(|entry_id| pane.read(cx).item_for_entry(entry_id, cx))
2068 .and_then(|item| item.downcast())
2069 {
2070 self.activate_item(&item, cx);
2071 return item;
2072 }
2073
2074 let item = cx.new_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
2075 self.add_item(pane, Box::new(item.clone()), cx);
2076 item
2077 }
2078
2079 pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
2080 if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
2081 self.active_pane.update(cx, |pane, cx| {
2082 pane.add_item(Box::new(shared_screen), false, true, None, cx)
2083 });
2084 }
2085 }
2086
2087 pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut WindowContext) -> bool {
2088 let result = self.panes.iter().find_map(|pane| {
2089 pane.read(cx)
2090 .index_for_item(item)
2091 .map(|ix| (pane.clone(), ix))
2092 });
2093 if let Some((pane, ix)) = result {
2094 pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
2095 true
2096 } else {
2097 false
2098 }
2099 }
2100
2101 fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
2102 let panes = self.center.panes();
2103 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
2104 cx.focus_view(&pane);
2105 } else {
2106 self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, cx);
2107 }
2108 }
2109
2110 pub fn activate_next_pane(&mut self, cx: &mut WindowContext) {
2111 let panes = self.center.panes();
2112 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
2113 let next_ix = (ix + 1) % panes.len();
2114 let next_pane = panes[next_ix].clone();
2115 cx.focus_view(&next_pane);
2116 }
2117 }
2118
2119 pub fn activate_previous_pane(&mut self, cx: &mut WindowContext) {
2120 let panes = self.center.panes();
2121 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
2122 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
2123 let prev_pane = panes[prev_ix].clone();
2124 cx.focus_view(&prev_pane);
2125 }
2126 }
2127
2128 pub fn activate_pane_in_direction(
2129 &mut self,
2130 direction: SplitDirection,
2131 cx: &mut WindowContext,
2132 ) {
2133 use ActivateInDirectionTarget as Target;
2134 enum Origin {
2135 LeftDock,
2136 RightDock,
2137 BottomDock,
2138 Center,
2139 }
2140
2141 let origin: Origin = [
2142 (&self.left_dock, Origin::LeftDock),
2143 (&self.right_dock, Origin::RightDock),
2144 (&self.bottom_dock, Origin::BottomDock),
2145 ]
2146 .into_iter()
2147 .find_map(|(dock, origin)| {
2148 if dock.focus_handle(cx).contains_focused(cx) && dock.read(cx).is_open() {
2149 Some(origin)
2150 } else {
2151 None
2152 }
2153 })
2154 .unwrap_or(Origin::Center);
2155
2156 let get_last_active_pane = || {
2157 self.last_active_center_pane.as_ref().and_then(|p| {
2158 let p = p.upgrade()?;
2159 (p.read(cx).items_len() != 0).then_some(p)
2160 })
2161 };
2162
2163 let try_dock =
2164 |dock: &View<Dock>| dock.read(cx).is_open().then(|| Target::Dock(dock.clone()));
2165
2166 let target = match (origin, direction) {
2167 // We're in the center, so we first try to go to a different pane,
2168 // otherwise try to go to a dock.
2169 (Origin::Center, direction) => {
2170 if let Some(pane) = self.find_pane_in_direction(direction, cx) {
2171 Some(Target::Pane(pane))
2172 } else {
2173 match direction {
2174 SplitDirection::Up => None,
2175 SplitDirection::Down => try_dock(&self.bottom_dock),
2176 SplitDirection::Left => try_dock(&self.left_dock),
2177 SplitDirection::Right => try_dock(&self.right_dock),
2178 }
2179 }
2180 }
2181
2182 (Origin::LeftDock, SplitDirection::Right) => {
2183 if let Some(last_active_pane) = get_last_active_pane() {
2184 Some(Target::Pane(last_active_pane))
2185 } else {
2186 try_dock(&self.bottom_dock).or_else(|| try_dock(&self.right_dock))
2187 }
2188 }
2189
2190 (Origin::LeftDock, SplitDirection::Down)
2191 | (Origin::RightDock, SplitDirection::Down) => try_dock(&self.bottom_dock),
2192
2193 (Origin::BottomDock, SplitDirection::Up) => get_last_active_pane().map(Target::Pane),
2194 (Origin::BottomDock, SplitDirection::Left) => try_dock(&self.left_dock),
2195 (Origin::BottomDock, SplitDirection::Right) => try_dock(&self.right_dock),
2196
2197 (Origin::RightDock, SplitDirection::Left) => {
2198 if let Some(last_active_pane) = get_last_active_pane() {
2199 Some(Target::Pane(last_active_pane))
2200 } else {
2201 try_dock(&self.bottom_dock).or_else(|| try_dock(&self.left_dock))
2202 }
2203 }
2204
2205 _ => None,
2206 };
2207
2208 match target {
2209 Some(ActivateInDirectionTarget::Pane(pane)) => cx.focus_view(&pane),
2210 Some(ActivateInDirectionTarget::Dock(dock)) => {
2211 if let Some(panel) = dock.read(cx).active_panel() {
2212 panel.focus_handle(cx).focus(cx);
2213 } else {
2214 log::error!("Could not find a focus target when in switching focus in {direction} direction for a {:?} dock", dock.read(cx).position());
2215 }
2216 }
2217 None => {}
2218 }
2219 }
2220
2221 fn find_pane_in_direction(
2222 &mut self,
2223 direction: SplitDirection,
2224 cx: &WindowContext,
2225 ) -> Option<View<Pane>> {
2226 let Some(bounding_box) = self.center.bounding_box_for_pane(&self.active_pane) else {
2227 return None;
2228 };
2229 let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx);
2230 let center = match cursor {
2231 Some(cursor) if bounding_box.contains(&cursor) => cursor,
2232 _ => bounding_box.center(),
2233 };
2234
2235 let distance_to_next = pane_group::HANDLE_HITBOX_SIZE;
2236
2237 let target = match direction {
2238 SplitDirection::Left => {
2239 Point::new(bounding_box.left() - distance_to_next.into(), center.y)
2240 }
2241 SplitDirection::Right => {
2242 Point::new(bounding_box.right() + distance_to_next.into(), center.y)
2243 }
2244 SplitDirection::Up => {
2245 Point::new(center.x, bounding_box.top() - distance_to_next.into())
2246 }
2247 SplitDirection::Down => {
2248 Point::new(center.x, bounding_box.bottom() + distance_to_next.into())
2249 }
2250 };
2251 self.center.pane_at_pixel_position(target).cloned()
2252 }
2253
2254 pub fn swap_pane_in_direction(
2255 &mut self,
2256 direction: SplitDirection,
2257 cx: &mut ViewContext<Self>,
2258 ) {
2259 if let Some(to) = self
2260 .find_pane_in_direction(direction, cx)
2261 .map(|pane| pane.clone())
2262 {
2263 self.center.swap(&self.active_pane.clone(), &to);
2264 cx.notify();
2265 }
2266 }
2267
2268 fn handle_pane_focused(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
2269 if self.active_pane != pane {
2270 self.active_pane = pane.clone();
2271 self.status_bar.update(cx, |status_bar, cx| {
2272 status_bar.set_active_pane(&self.active_pane, cx);
2273 });
2274 self.active_item_path_changed(cx);
2275 self.last_active_center_pane = Some(pane.downgrade());
2276 }
2277
2278 self.dismiss_zoomed_items_to_reveal(None, cx);
2279 if pane.read(cx).is_zoomed() {
2280 self.zoomed = Some(pane.downgrade().into());
2281 } else {
2282 self.zoomed = None;
2283 }
2284 self.zoomed_position = None;
2285 self.update_active_view_for_followers(cx);
2286
2287 cx.notify();
2288 }
2289
2290 fn handle_pane_event(
2291 &mut self,
2292 pane: View<Pane>,
2293 event: &pane::Event,
2294 cx: &mut ViewContext<Self>,
2295 ) {
2296 match event {
2297 pane::Event::AddItem { item } => item.added_to_pane(self, pane, cx),
2298 pane::Event::Split(direction) => {
2299 self.split_and_clone(pane, *direction, cx);
2300 }
2301 pane::Event::Remove => self.remove_pane(pane, cx),
2302 pane::Event::ActivateItem { local } => {
2303 if *local {
2304 self.unfollow(&pane, cx);
2305 }
2306 if &pane == self.active_pane() {
2307 self.active_item_path_changed(cx);
2308 self.update_active_view_for_followers(cx);
2309 }
2310 }
2311 pane::Event::ChangeItemTitle => {
2312 if pane == self.active_pane {
2313 self.active_item_path_changed(cx);
2314 }
2315 self.update_window_edited(cx);
2316 }
2317 pane::Event::RemoveItem { item_id } => {
2318 self.update_window_edited(cx);
2319 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
2320 if entry.get().entity_id() == pane.entity_id() {
2321 entry.remove();
2322 }
2323 }
2324 }
2325 pane::Event::Focus => {
2326 self.handle_pane_focused(pane.clone(), cx);
2327 }
2328 pane::Event::ZoomIn => {
2329 if pane == self.active_pane {
2330 pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
2331 if pane.read(cx).has_focus(cx) {
2332 self.zoomed = Some(pane.downgrade().into());
2333 self.zoomed_position = None;
2334 }
2335 cx.notify();
2336 }
2337 }
2338 pane::Event::ZoomOut => {
2339 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
2340 if self.zoomed_position.is_none() {
2341 self.zoomed = None;
2342 }
2343 cx.notify();
2344 }
2345 }
2346
2347 self.serialize_workspace(cx);
2348 }
2349
2350 pub fn split_pane(
2351 &mut self,
2352 pane_to_split: View<Pane>,
2353 split_direction: SplitDirection,
2354 cx: &mut ViewContext<Self>,
2355 ) -> View<Pane> {
2356 let new_pane = self.add_pane(cx);
2357 self.center
2358 .split(&pane_to_split, &new_pane, split_direction)
2359 .unwrap();
2360 cx.notify();
2361 new_pane
2362 }
2363
2364 pub fn split_and_clone(
2365 &mut self,
2366 pane: View<Pane>,
2367 direction: SplitDirection,
2368 cx: &mut ViewContext<Self>,
2369 ) -> Option<View<Pane>> {
2370 let item = pane.read(cx).active_item()?;
2371 let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
2372 let new_pane = self.add_pane(cx);
2373 new_pane.update(cx, |pane, cx| pane.add_item(clone, true, true, None, cx));
2374 self.center.split(&pane, &new_pane, direction).unwrap();
2375 Some(new_pane)
2376 } else {
2377 None
2378 };
2379 cx.notify();
2380 maybe_pane_handle
2381 }
2382
2383 pub fn split_pane_with_item(
2384 &mut self,
2385 pane_to_split: WeakView<Pane>,
2386 split_direction: SplitDirection,
2387 from: WeakView<Pane>,
2388 item_id_to_move: EntityId,
2389 cx: &mut ViewContext<Self>,
2390 ) {
2391 let Some(pane_to_split) = pane_to_split.upgrade() else {
2392 return;
2393 };
2394 let Some(from) = from.upgrade() else {
2395 return;
2396 };
2397
2398 let new_pane = self.add_pane(cx);
2399 self.move_item(from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
2400 self.center
2401 .split(&pane_to_split, &new_pane, split_direction)
2402 .unwrap();
2403 cx.notify();
2404 }
2405
2406 pub fn split_pane_with_project_entry(
2407 &mut self,
2408 pane_to_split: WeakView<Pane>,
2409 split_direction: SplitDirection,
2410 project_entry: ProjectEntryId,
2411 cx: &mut ViewContext<Self>,
2412 ) -> Option<Task<Result<()>>> {
2413 let pane_to_split = pane_to_split.upgrade()?;
2414 let new_pane = self.add_pane(cx);
2415 self.center
2416 .split(&pane_to_split, &new_pane, split_direction)
2417 .unwrap();
2418
2419 let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
2420 let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
2421 Some(cx.foreground_executor().spawn(async move {
2422 task.await?;
2423 Ok(())
2424 }))
2425 }
2426
2427 pub fn move_item(
2428 &mut self,
2429 source: View<Pane>,
2430 destination: View<Pane>,
2431 item_id_to_move: EntityId,
2432 destination_index: usize,
2433 cx: &mut ViewContext<Self>,
2434 ) {
2435 let Some((item_ix, item_handle)) = source
2436 .read(cx)
2437 .items()
2438 .enumerate()
2439 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
2440 else {
2441 // Tab was closed during drag
2442 return;
2443 };
2444
2445 let item_handle = item_handle.clone();
2446
2447 if source != destination {
2448 // Close item from previous pane
2449 source.update(cx, |source, cx| {
2450 source.remove_item(item_ix, false, cx);
2451 });
2452 }
2453
2454 // This automatically removes duplicate items in the pane
2455 destination.update(cx, |destination, cx| {
2456 destination.add_item(item_handle, true, true, Some(destination_index), cx);
2457 destination.focus(cx)
2458 });
2459 }
2460
2461 fn remove_pane(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
2462 if self.center.remove(&pane).unwrap() {
2463 self.force_remove_pane(&pane, cx);
2464 self.unfollow(&pane, cx);
2465 self.last_leaders_by_pane.remove(&pane.downgrade());
2466 for removed_item in pane.read(cx).items() {
2467 self.panes_by_item.remove(&removed_item.item_id());
2468 }
2469
2470 cx.notify();
2471 } else {
2472 self.active_item_path_changed(cx);
2473 }
2474 }
2475
2476 pub fn panes(&self) -> &[View<Pane>] {
2477 &self.panes
2478 }
2479
2480 pub fn active_pane(&self) -> &View<Pane> {
2481 &self.active_pane
2482 }
2483
2484 pub fn adjacent_pane(&mut self, cx: &mut ViewContext<Self>) -> View<Pane> {
2485 self.find_pane_in_direction(SplitDirection::Right, cx)
2486 .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
2487 .unwrap_or_else(|| self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx))
2488 .clone()
2489 }
2490
2491 pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<View<Pane>> {
2492 let weak_pane = self.panes_by_item.get(&handle.item_id())?;
2493 weak_pane.upgrade()
2494 }
2495
2496 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
2497 self.follower_states.retain(|_, state| {
2498 if state.leader_id == peer_id {
2499 for item in state.items_by_leader_view_id.values() {
2500 item.set_leader_peer_id(None, cx);
2501 }
2502 false
2503 } else {
2504 true
2505 }
2506 });
2507 cx.notify();
2508 }
2509
2510 pub fn start_following(
2511 &mut self,
2512 leader_id: PeerId,
2513 cx: &mut ViewContext<Self>,
2514 ) -> Option<Task<Result<()>>> {
2515 let pane = self.active_pane().clone();
2516
2517 self.last_leaders_by_pane
2518 .insert(pane.downgrade(), leader_id);
2519 self.unfollow(&pane, cx);
2520 self.follower_states.insert(
2521 pane.clone(),
2522 FollowerState {
2523 leader_id,
2524 active_view_id: None,
2525 items_by_leader_view_id: Default::default(),
2526 },
2527 );
2528 cx.notify();
2529
2530 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
2531 let project_id = self.project.read(cx).remote_id();
2532 let request = self.app_state.client.request(proto::Follow {
2533 room_id,
2534 project_id,
2535 leader_id: Some(leader_id),
2536 });
2537
2538 Some(cx.spawn(|this, mut cx| async move {
2539 let response = request.await?;
2540 this.update(&mut cx, |this, _| {
2541 let state = this
2542 .follower_states
2543 .get_mut(&pane)
2544 .ok_or_else(|| anyhow!("following interrupted"))?;
2545 state.active_view_id = if let Some(active_view_id) = response.active_view_id {
2546 Some(ViewId::from_proto(active_view_id)?)
2547 } else {
2548 None
2549 };
2550 Ok::<_, anyhow::Error>(())
2551 })??;
2552 if let Some(view) = response.active_view {
2553 Self::add_view_from_leader(this.clone(), leader_id, pane.clone(), &view, &mut cx)
2554 .await?;
2555 }
2556 Self::add_views_from_leader(
2557 this.clone(),
2558 leader_id,
2559 vec![pane],
2560 response.views,
2561 &mut cx,
2562 )
2563 .await?;
2564 this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
2565 Ok(())
2566 }))
2567 }
2568
2569 pub fn follow_next_collaborator(
2570 &mut self,
2571 _: &FollowNextCollaborator,
2572 cx: &mut ViewContext<Self>,
2573 ) {
2574 let collaborators = self.project.read(cx).collaborators();
2575 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
2576 let mut collaborators = collaborators.keys().copied();
2577 for peer_id in collaborators.by_ref() {
2578 if peer_id == leader_id {
2579 break;
2580 }
2581 }
2582 collaborators.next()
2583 } else if let Some(last_leader_id) =
2584 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
2585 {
2586 if collaborators.contains_key(last_leader_id) {
2587 Some(*last_leader_id)
2588 } else {
2589 None
2590 }
2591 } else {
2592 None
2593 };
2594
2595 let pane = self.active_pane.clone();
2596 let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next())
2597 else {
2598 return;
2599 };
2600 if Some(leader_id) == self.unfollow(&pane, cx) {
2601 return;
2602 }
2603 self.start_following(leader_id, cx)
2604 .map(|task| task.detach_and_log_err(cx));
2605 }
2606
2607 pub fn follow(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) {
2608 let Some(room) = ActiveCall::global(cx).read(cx).room() else {
2609 return;
2610 };
2611 let room = room.read(cx);
2612 let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
2613 return;
2614 };
2615
2616 let project = self.project.read(cx);
2617
2618 let other_project_id = match remote_participant.location {
2619 call::ParticipantLocation::External => None,
2620 call::ParticipantLocation::UnsharedProject => None,
2621 call::ParticipantLocation::SharedProject { project_id } => {
2622 if Some(project_id) == project.remote_id() {
2623 None
2624 } else {
2625 Some(project_id)
2626 }
2627 }
2628 };
2629
2630 // if they are active in another project, follow there.
2631 if let Some(project_id) = other_project_id {
2632 let app_state = self.app_state.clone();
2633 crate::join_remote_project(project_id, remote_participant.user.id, app_state, cx)
2634 .detach_and_log_err(cx);
2635 }
2636
2637 // if you're already following, find the right pane and focus it.
2638 for (pane, state) in &self.follower_states {
2639 if leader_id == state.leader_id {
2640 cx.focus_view(pane);
2641 return;
2642 }
2643 }
2644
2645 // Otherwise, follow.
2646 self.start_following(leader_id, cx)
2647 .map(|task| task.detach_and_log_err(cx));
2648 }
2649
2650 pub fn unfollow(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Self>) -> Option<PeerId> {
2651 let state = self.follower_states.remove(pane)?;
2652 let leader_id = state.leader_id;
2653 for (_, item) in state.items_by_leader_view_id {
2654 item.set_leader_peer_id(None, cx);
2655 }
2656
2657 if self
2658 .follower_states
2659 .values()
2660 .all(|state| state.leader_id != state.leader_id)
2661 {
2662 let project_id = self.project.read(cx).remote_id();
2663 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
2664 self.app_state
2665 .client
2666 .send(proto::Unfollow {
2667 room_id,
2668 project_id,
2669 leader_id: Some(leader_id),
2670 })
2671 .log_err();
2672 }
2673
2674 cx.notify();
2675 Some(leader_id)
2676 }
2677
2678 pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
2679 self.follower_states
2680 .values()
2681 .any(|state| state.leader_id == peer_id)
2682 }
2683
2684 fn active_item_path_changed(&mut self, cx: &mut WindowContext) {
2685 let active_entry = self.active_project_path(cx);
2686 self.project
2687 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
2688 self.update_window_title(cx);
2689 }
2690
2691 fn update_window_title(&mut self, cx: &mut WindowContext) {
2692 let project = self.project().read(cx);
2693 let mut title = String::new();
2694
2695 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
2696 let filename = path
2697 .path
2698 .file_name()
2699 .map(|s| s.to_string_lossy())
2700 .or_else(|| {
2701 Some(Cow::Borrowed(
2702 project
2703 .worktree_for_id(path.worktree_id, cx)?
2704 .read(cx)
2705 .root_name(),
2706 ))
2707 });
2708
2709 if let Some(filename) = filename {
2710 title.push_str(filename.as_ref());
2711 title.push_str(" — ");
2712 }
2713 }
2714
2715 for (i, name) in project.worktree_root_names(cx).enumerate() {
2716 if i > 0 {
2717 title.push_str(", ");
2718 }
2719 title.push_str(name);
2720 }
2721
2722 if title.is_empty() {
2723 title = "empty project".to_string();
2724 }
2725
2726 if project.is_remote() {
2727 title.push_str(" ↙");
2728 } else if project.is_shared() {
2729 title.push_str(" ↗");
2730 }
2731
2732 cx.set_window_title(&title);
2733 }
2734
2735 fn update_window_edited(&mut self, cx: &mut WindowContext) {
2736 let is_edited = !self.project.read(cx).is_disconnected()
2737 && self
2738 .items(cx)
2739 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
2740 if is_edited != self.window_edited {
2741 self.window_edited = is_edited;
2742 cx.set_window_edited(self.window_edited)
2743 }
2744 }
2745
2746 fn render_notifications(&self, _cx: &ViewContext<Self>) -> Option<Div> {
2747 if self.notifications.is_empty() {
2748 None
2749 } else {
2750 Some(
2751 div()
2752 .absolute()
2753 .z_index(100)
2754 .right_3()
2755 .bottom_3()
2756 .w_112()
2757 .h_full()
2758 .flex()
2759 .flex_col()
2760 .justify_end()
2761 .gap_2()
2762 .children(
2763 self.notifications
2764 .iter()
2765 .map(|(_, _, notification)| notification.to_any()),
2766 ),
2767 )
2768 }
2769 }
2770
2771 // RPC handlers
2772
2773 fn active_view_for_follower(
2774 &self,
2775 follower_project_id: Option<u64>,
2776 cx: &mut ViewContext<Self>,
2777 ) -> Option<proto::View> {
2778 let item = self.active_item(cx)?;
2779 let leader_id = self
2780 .pane_for(&*item)
2781 .and_then(|pane| self.leader_for_pane(&pane));
2782
2783 let item_handle = item.to_followable_item_handle(cx)?;
2784 let id = item_handle.remote_id(&self.app_state.client, cx)?;
2785 let variant = item_handle.to_state_proto(cx)?;
2786
2787 if item_handle.is_project_item(cx)
2788 && (follower_project_id.is_none()
2789 || follower_project_id != self.project.read(cx).remote_id())
2790 {
2791 return None;
2792 }
2793
2794 Some(proto::View {
2795 id: Some(id.to_proto()),
2796 leader_id,
2797 variant: Some(variant),
2798 })
2799 }
2800
2801 fn handle_follow(
2802 &mut self,
2803 follower_project_id: Option<u64>,
2804 cx: &mut ViewContext<Self>,
2805 ) -> proto::FollowResponse {
2806 let client = &self.app_state.client;
2807 let project_id = self.project.read(cx).remote_id();
2808
2809 let active_view = self.active_view_for_follower(follower_project_id, cx);
2810 let active_view_id = active_view.as_ref().and_then(|view| view.id.clone());
2811
2812 cx.notify();
2813
2814 proto::FollowResponse {
2815 active_view,
2816 // TODO: once v0.124.0 is retired we can stop sending these
2817 active_view_id,
2818 views: self
2819 .panes()
2820 .iter()
2821 .flat_map(|pane| {
2822 let leader_id = self.leader_for_pane(pane);
2823 pane.read(cx).items().filter_map({
2824 let cx = &cx;
2825 move |item| {
2826 let item = item.to_followable_item_handle(cx)?;
2827
2828 // If the item belongs to a particular project, then it should
2829 // only be included if this project is shared, and the follower
2830 // is in the project.
2831 //
2832 // Some items, like channel notes, do not belong to a particular
2833 // project, so they should be included regardless of whether the
2834 // current project is shared, or what project the follower is in.
2835 if item.is_project_item(cx)
2836 && (project_id.is_none() || project_id != follower_project_id)
2837 {
2838 return None;
2839 }
2840
2841 let id = item.remote_id(client, cx)?.to_proto();
2842 let variant = item.to_state_proto(cx)?;
2843 Some(proto::View {
2844 id: Some(id),
2845 leader_id,
2846 variant: Some(variant),
2847 })
2848 }
2849 })
2850 })
2851 .collect(),
2852 }
2853 }
2854
2855 fn handle_update_followers(
2856 &mut self,
2857 leader_id: PeerId,
2858 message: proto::UpdateFollowers,
2859 _cx: &mut ViewContext<Self>,
2860 ) {
2861 self.leader_updates_tx
2862 .unbounded_send((leader_id, message))
2863 .ok();
2864 }
2865
2866 async fn process_leader_update(
2867 this: &WeakView<Self>,
2868 leader_id: PeerId,
2869 update: proto::UpdateFollowers,
2870 cx: &mut AsyncWindowContext,
2871 ) -> Result<()> {
2872 match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
2873 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
2874 let panes_missing_view = this.update(cx, |this, _| {
2875 let mut panes = vec![];
2876 for (pane, state) in &mut this.follower_states {
2877 if state.leader_id != leader_id {
2878 continue;
2879 }
2880
2881 state.active_view_id =
2882 if let Some(active_view_id) = update_active_view.id.clone() {
2883 Some(ViewId::from_proto(active_view_id)?)
2884 } else {
2885 None
2886 };
2887
2888 if state.active_view_id.is_some_and(|view_id| {
2889 !state.items_by_leader_view_id.contains_key(&view_id)
2890 }) {
2891 panes.push(pane.clone())
2892 }
2893 }
2894 anyhow::Ok(panes)
2895 })??;
2896
2897 if let Some(view) = update_active_view.view {
2898 for pane in panes_missing_view {
2899 Self::add_view_from_leader(this.clone(), leader_id, pane.clone(), &view, cx)
2900 .await?
2901 }
2902 }
2903 }
2904 proto::update_followers::Variant::UpdateView(update_view) => {
2905 let variant = update_view
2906 .variant
2907 .ok_or_else(|| anyhow!("missing update view variant"))?;
2908 let id = update_view
2909 .id
2910 .ok_or_else(|| anyhow!("missing update view id"))?;
2911 let mut tasks = Vec::new();
2912 this.update(cx, |this, cx| {
2913 let project = this.project.clone();
2914 for (_, state) in &mut this.follower_states {
2915 if state.leader_id == leader_id {
2916 let view_id = ViewId::from_proto(id.clone())?;
2917 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
2918 tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
2919 }
2920 }
2921 }
2922 anyhow::Ok(())
2923 })??;
2924 try_join_all(tasks).await.log_err();
2925 }
2926 proto::update_followers::Variant::CreateView(view) => {
2927 let panes = this.update(cx, |this, _| {
2928 this.follower_states
2929 .iter()
2930 .filter_map(|(pane, state)| (state.leader_id == leader_id).then_some(pane))
2931 .cloned()
2932 .collect()
2933 })?;
2934 Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?;
2935 }
2936 }
2937 this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
2938 Ok(())
2939 }
2940
2941 async fn add_view_from_leader(
2942 this: WeakView<Self>,
2943 leader_id: PeerId,
2944 pane: View<Pane>,
2945 view: &proto::View,
2946 cx: &mut AsyncWindowContext,
2947 ) -> Result<()> {
2948 let this = this.upgrade().context("workspace dropped")?;
2949
2950 let item_builders = cx.update(|cx| {
2951 cx.default_global::<FollowableItemBuilders>()
2952 .values()
2953 .map(|b| b.0)
2954 .collect::<Vec<_>>()
2955 })?;
2956
2957 let Some(id) = view.id.clone() else {
2958 return Err(anyhow!("no id for view")).into();
2959 };
2960 let id = ViewId::from_proto(id)?;
2961
2962 let mut variant = view.variant.clone();
2963 if variant.is_none() {
2964 Err(anyhow!("missing view variant"))?;
2965 }
2966
2967 let task = item_builders.iter().find_map(|build_item| {
2968 cx.update(|cx| build_item(pane.clone(), this.clone(), id, &mut variant, cx))
2969 .log_err()
2970 .flatten()
2971 });
2972 let Some(task) = task else {
2973 return Err(anyhow!(
2974 "failed to construct view from leader (maybe from a different version of zed?)"
2975 ));
2976 };
2977
2978 let item = task.await?;
2979
2980 this.update(cx, |this, cx| {
2981 let state = this.follower_states.get_mut(&pane)?;
2982 item.set_leader_peer_id(Some(leader_id), cx);
2983 state.items_by_leader_view_id.insert(id, item);
2984
2985 Some(())
2986 })?;
2987
2988 Ok(())
2989 }
2990
2991 async fn add_views_from_leader(
2992 this: WeakView<Self>,
2993 leader_id: PeerId,
2994 panes: Vec<View<Pane>>,
2995 views: Vec<proto::View>,
2996 cx: &mut AsyncWindowContext,
2997 ) -> Result<()> {
2998 let this = this.upgrade().context("workspace dropped")?;
2999
3000 let item_builders = cx.update(|cx| {
3001 cx.default_global::<FollowableItemBuilders>()
3002 .values()
3003 .map(|b| b.0)
3004 .collect::<Vec<_>>()
3005 })?;
3006
3007 let mut item_tasks_by_pane = HashMap::default();
3008 for pane in panes {
3009 let mut item_tasks = Vec::new();
3010 let mut leader_view_ids = Vec::new();
3011 for view in &views {
3012 let Some(id) = &view.id else {
3013 continue;
3014 };
3015 let id = ViewId::from_proto(id.clone())?;
3016 let mut variant = view.variant.clone();
3017 if variant.is_none() {
3018 Err(anyhow!("missing view variant"))?;
3019 }
3020 for build_item in &item_builders {
3021 let task = cx.update(|cx| {
3022 build_item(pane.clone(), this.clone(), id, &mut variant, cx)
3023 })?;
3024 if let Some(task) = task {
3025 item_tasks.push(task);
3026 leader_view_ids.push(id);
3027 break;
3028 } else if variant.is_none() {
3029 Err(anyhow!(
3030 "failed to construct view from leader (maybe from a different version of zed?)"
3031 ))?;
3032 }
3033 }
3034 }
3035
3036 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
3037 }
3038
3039 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
3040 let items = futures::future::try_join_all(item_tasks).await?;
3041 this.update(cx, |this, cx| {
3042 let state = this.follower_states.get_mut(&pane)?;
3043 for (id, item) in leader_view_ids.into_iter().zip(items) {
3044 item.set_leader_peer_id(Some(leader_id), cx);
3045 state.items_by_leader_view_id.insert(id, item);
3046 }
3047
3048 Some(())
3049 })?;
3050 }
3051 Ok(())
3052 }
3053
3054 pub fn update_active_view_for_followers(&mut self, cx: &mut WindowContext) {
3055 let mut is_project_item = true;
3056 let mut update = proto::UpdateActiveView::default();
3057 if cx.is_window_active() {
3058 if let Some(item) = self.active_item(cx) {
3059 if item.focus_handle(cx).contains_focused(cx) {
3060 let leader_id = self
3061 .pane_for(&*item)
3062 .and_then(|pane| self.leader_for_pane(&pane));
3063
3064 if let Some(item) = item.to_followable_item_handle(cx) {
3065 let id = item
3066 .remote_id(&self.app_state.client, cx)
3067 .map(|id| id.to_proto());
3068
3069 if let Some(id) = id.clone() {
3070 if let Some(variant) = item.to_state_proto(cx) {
3071 let view = Some(proto::View {
3072 id: Some(id.clone()),
3073 leader_id,
3074 variant: Some(variant),
3075 });
3076
3077 is_project_item = item.is_project_item(cx);
3078 update = proto::UpdateActiveView {
3079 view,
3080 // TODO: once v0.124.0 is retired we can stop sending these
3081 id: Some(id),
3082 leader_id,
3083 };
3084 }
3085 };
3086 }
3087 }
3088 }
3089 }
3090
3091 if &update.id != &self.last_active_view_id {
3092 self.last_active_view_id = update.id.clone();
3093 self.update_followers(
3094 is_project_item,
3095 proto::update_followers::Variant::UpdateActiveView(update),
3096 cx,
3097 );
3098 }
3099 }
3100
3101 fn update_followers(
3102 &self,
3103 project_only: bool,
3104 update: proto::update_followers::Variant,
3105 cx: &mut WindowContext,
3106 ) -> Option<()> {
3107 // If this update only applies to for followers in the current project,
3108 // then skip it unless this project is shared. If it applies to all
3109 // followers, regardless of project, then set `project_id` to none,
3110 // indicating that it goes to all followers.
3111 let project_id = if project_only {
3112 Some(self.project.read(cx).remote_id()?)
3113 } else {
3114 None
3115 };
3116 self.app_state().workspace_store.update(cx, |store, cx| {
3117 store.update_followers(project_id, update, cx)
3118 })
3119 }
3120
3121 pub fn leader_for_pane(&self, pane: &View<Pane>) -> Option<PeerId> {
3122 self.follower_states.get(pane).map(|state| state.leader_id)
3123 }
3124
3125 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
3126 cx.notify();
3127
3128 let call = self.active_call()?;
3129 let room = call.read(cx).room()?.read(cx);
3130 let participant = room.remote_participant_for_peer_id(leader_id)?;
3131 let mut items_to_activate = Vec::new();
3132
3133 let leader_in_this_app;
3134 let leader_in_this_project;
3135 match participant.location {
3136 call::ParticipantLocation::SharedProject { project_id } => {
3137 leader_in_this_app = true;
3138 leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
3139 }
3140 call::ParticipantLocation::UnsharedProject => {
3141 leader_in_this_app = true;
3142 leader_in_this_project = false;
3143 }
3144 call::ParticipantLocation::External => {
3145 leader_in_this_app = false;
3146 leader_in_this_project = false;
3147 }
3148 };
3149
3150 for (pane, state) in &self.follower_states {
3151 if state.leader_id != leader_id {
3152 continue;
3153 }
3154 if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
3155 if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
3156 if leader_in_this_project || !item.is_project_item(cx) {
3157 items_to_activate.push((pane.clone(), item.boxed_clone()));
3158 }
3159 }
3160 continue;
3161 }
3162
3163 if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
3164 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
3165 }
3166 }
3167
3168 for (pane, item) in items_to_activate {
3169 let pane_was_focused = pane.read(cx).has_focus(cx);
3170 if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
3171 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
3172 } else {
3173 pane.update(cx, |pane, cx| {
3174 pane.add_item(item.boxed_clone(), false, false, None, cx)
3175 });
3176 }
3177
3178 if pane_was_focused {
3179 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
3180 }
3181 }
3182
3183 None
3184 }
3185
3186 fn shared_screen_for_peer(
3187 &self,
3188 peer_id: PeerId,
3189 pane: &View<Pane>,
3190 cx: &mut WindowContext,
3191 ) -> Option<View<SharedScreen>> {
3192 let call = self.active_call()?;
3193 let room = call.read(cx).room()?.read(cx);
3194 let participant = room.remote_participant_for_peer_id(peer_id)?;
3195 let track = participant.video_tracks.values().next()?.clone();
3196 let user = participant.user.clone();
3197
3198 for item in pane.read(cx).items_of_type::<SharedScreen>() {
3199 if item.read(cx).peer_id == peer_id {
3200 return Some(item);
3201 }
3202 }
3203
3204 Some(cx.new_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
3205 }
3206
3207 pub fn on_window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
3208 if cx.is_window_active() {
3209 self.update_active_view_for_followers(cx);
3210 cx.background_executor()
3211 .spawn(persistence::DB.update_timestamp(self.database_id()))
3212 .detach();
3213 } else {
3214 for pane in &self.panes {
3215 pane.update(cx, |pane, cx| {
3216 if let Some(item) = pane.active_item() {
3217 item.workspace_deactivated(cx);
3218 }
3219 if matches!(
3220 WorkspaceSettings::get_global(cx).autosave,
3221 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
3222 ) {
3223 for item in pane.items() {
3224 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
3225 .detach_and_log_err(cx);
3226 }
3227 }
3228 });
3229 }
3230 }
3231 }
3232
3233 fn active_call(&self) -> Option<&Model<ActiveCall>> {
3234 self.active_call.as_ref().map(|(call, _)| call)
3235 }
3236
3237 fn on_active_call_event(
3238 &mut self,
3239 _: Model<ActiveCall>,
3240 event: &call::room::Event,
3241 cx: &mut ViewContext<Self>,
3242 ) {
3243 match event {
3244 call::room::Event::ParticipantLocationChanged { participant_id }
3245 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
3246 self.leader_updated(*participant_id, cx);
3247 }
3248 _ => {}
3249 }
3250 }
3251
3252 pub fn database_id(&self) -> WorkspaceId {
3253 self.database_id
3254 }
3255
3256 fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
3257 let project = self.project().read(cx);
3258
3259 if project.is_local() {
3260 Some(
3261 project
3262 .visible_worktrees(cx)
3263 .map(|worktree| worktree.read(cx).abs_path())
3264 .collect::<Vec<_>>()
3265 .into(),
3266 )
3267 } else {
3268 None
3269 }
3270 }
3271
3272 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
3273 match member {
3274 Member::Axis(PaneAxis { members, .. }) => {
3275 for child in members.iter() {
3276 self.remove_panes(child.clone(), cx)
3277 }
3278 }
3279 Member::Pane(pane) => {
3280 self.force_remove_pane(&pane, cx);
3281 }
3282 }
3283 }
3284
3285 fn force_remove_pane(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Workspace>) {
3286 self.panes.retain(|p| p != pane);
3287 self.panes
3288 .last()
3289 .unwrap()
3290 .update(cx, |pane, cx| pane.focus(cx));
3291 if self.last_active_center_pane == Some(pane.downgrade()) {
3292 self.last_active_center_pane = None;
3293 }
3294 cx.notify();
3295 }
3296
3297 fn schedule_serialize(&mut self, cx: &mut ViewContext<Self>) {
3298 self._schedule_serialize = Some(cx.spawn(|this, mut cx| async move {
3299 cx.background_executor()
3300 .timer(Duration::from_millis(100))
3301 .await;
3302 this.update(&mut cx, |this, cx| this.serialize_workspace(cx))
3303 .log_err();
3304 }));
3305 }
3306
3307 fn serialize_workspace(&self, cx: &mut WindowContext) {
3308 fn serialize_pane_handle(pane_handle: &View<Pane>, cx: &WindowContext) -> SerializedPane {
3309 let (items, active) = {
3310 let pane = pane_handle.read(cx);
3311 let active_item_id = pane.active_item().map(|item| item.item_id());
3312 (
3313 pane.items()
3314 .filter_map(|item_handle| {
3315 Some(SerializedItem {
3316 kind: Arc::from(item_handle.serialized_item_kind()?),
3317 item_id: item_handle.item_id().as_u64(),
3318 active: Some(item_handle.item_id()) == active_item_id,
3319 })
3320 })
3321 .collect::<Vec<_>>(),
3322 pane.has_focus(cx),
3323 )
3324 };
3325
3326 SerializedPane::new(items, active)
3327 }
3328
3329 fn build_serialized_pane_group(
3330 pane_group: &Member,
3331 cx: &WindowContext,
3332 ) -> SerializedPaneGroup {
3333 match pane_group {
3334 Member::Axis(PaneAxis {
3335 axis,
3336 members,
3337 flexes,
3338 bounding_boxes: _,
3339 }) => SerializedPaneGroup::Group {
3340 axis: SerializedAxis(*axis),
3341 children: members
3342 .iter()
3343 .map(|member| build_serialized_pane_group(member, cx))
3344 .collect::<Vec<_>>(),
3345 flexes: Some(flexes.lock().clone()),
3346 },
3347 Member::Pane(pane_handle) => {
3348 SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, cx))
3349 }
3350 }
3351 }
3352
3353 fn build_serialized_docks(this: &Workspace, cx: &mut WindowContext) -> DockStructure {
3354 let left_dock = this.left_dock.read(cx);
3355 let left_visible = left_dock.is_open();
3356 let left_active_panel = left_dock
3357 .visible_panel()
3358 .and_then(|panel| Some(panel.persistent_name().to_string()));
3359 let left_dock_zoom = left_dock
3360 .visible_panel()
3361 .map(|panel| panel.is_zoomed(cx))
3362 .unwrap_or(false);
3363
3364 let right_dock = this.right_dock.read(cx);
3365 let right_visible = right_dock.is_open();
3366 let right_active_panel = right_dock
3367 .visible_panel()
3368 .and_then(|panel| Some(panel.persistent_name().to_string()));
3369 let right_dock_zoom = right_dock
3370 .visible_panel()
3371 .map(|panel| panel.is_zoomed(cx))
3372 .unwrap_or(false);
3373
3374 let bottom_dock = this.bottom_dock.read(cx);
3375 let bottom_visible = bottom_dock.is_open();
3376 let bottom_active_panel = bottom_dock
3377 .visible_panel()
3378 .and_then(|panel| Some(panel.persistent_name().to_string()));
3379 let bottom_dock_zoom = bottom_dock
3380 .visible_panel()
3381 .map(|panel| panel.is_zoomed(cx))
3382 .unwrap_or(false);
3383
3384 DockStructure {
3385 left: DockData {
3386 visible: left_visible,
3387 active_panel: left_active_panel,
3388 zoom: left_dock_zoom,
3389 },
3390 right: DockData {
3391 visible: right_visible,
3392 active_panel: right_active_panel,
3393 zoom: right_dock_zoom,
3394 },
3395 bottom: DockData {
3396 visible: bottom_visible,
3397 active_panel: bottom_active_panel,
3398 zoom: bottom_dock_zoom,
3399 },
3400 }
3401 }
3402
3403 if let Some(location) = self.location(cx) {
3404 // Load bearing special case:
3405 // - with_local_workspace() relies on this to not have other stuff open
3406 // when you open your log
3407 if !location.paths().is_empty() {
3408 let center_group = build_serialized_pane_group(&self.center.root, cx);
3409 let docks = build_serialized_docks(self, cx);
3410
3411 let serialized_workspace = SerializedWorkspace {
3412 id: self.database_id,
3413 location,
3414 center_group,
3415 bounds: Default::default(),
3416 display: Default::default(),
3417 docks,
3418 };
3419
3420 cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace))
3421 .detach();
3422 }
3423 }
3424 }
3425
3426 pub(crate) fn load_workspace(
3427 serialized_workspace: SerializedWorkspace,
3428 paths_to_open: Vec<Option<ProjectPath>>,
3429 cx: &mut ViewContext<Workspace>,
3430 ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
3431 cx.spawn(|workspace, mut cx| async move {
3432 let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
3433
3434 let mut center_group = None;
3435 let mut center_items = None;
3436
3437 // Traverse the splits tree and add to things
3438 if let Some((group, active_pane, items)) = serialized_workspace
3439 .center_group
3440 .deserialize(
3441 &project,
3442 serialized_workspace.id,
3443 workspace.clone(),
3444 &mut cx,
3445 )
3446 .await
3447 {
3448 center_items = Some(items);
3449 center_group = Some((group, active_pane))
3450 }
3451
3452 let mut items_by_project_path = cx.update(|cx| {
3453 center_items
3454 .unwrap_or_default()
3455 .into_iter()
3456 .filter_map(|item| {
3457 let item = item?;
3458 let project_path = item.project_path(cx)?;
3459 Some((project_path, item))
3460 })
3461 .collect::<HashMap<_, _>>()
3462 })?;
3463
3464 let opened_items = paths_to_open
3465 .into_iter()
3466 .map(|path_to_open| {
3467 path_to_open
3468 .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
3469 })
3470 .collect::<Vec<_>>();
3471
3472 // Remove old panes from workspace panes list
3473 workspace.update(&mut cx, |workspace, cx| {
3474 if let Some((center_group, active_pane)) = center_group {
3475 workspace.remove_panes(workspace.center.root.clone(), cx);
3476
3477 // Swap workspace center group
3478 workspace.center = PaneGroup::with_root(center_group);
3479 workspace.last_active_center_pane = active_pane.as_ref().map(|p| p.downgrade());
3480 if let Some(active_pane) = active_pane {
3481 workspace.active_pane = active_pane;
3482 cx.focus_self();
3483 } else {
3484 workspace.active_pane = workspace.center.first_pane().clone();
3485 }
3486 }
3487
3488 let docks = serialized_workspace.docks;
3489
3490 let right = docks.right.clone();
3491 workspace
3492 .right_dock
3493 .update(cx, |dock, _| dock.serialized_dock = Some(right));
3494 let left = docks.left.clone();
3495 workspace
3496 .left_dock
3497 .update(cx, |dock, _| dock.serialized_dock = Some(left));
3498 let bottom = docks.bottom.clone();
3499 workspace
3500 .bottom_dock
3501 .update(cx, |dock, _| dock.serialized_dock = Some(bottom));
3502
3503 cx.notify();
3504 })?;
3505
3506 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
3507 workspace.update(&mut cx, |workspace, cx| workspace.serialize_workspace(cx))?;
3508
3509 Ok(opened_items)
3510 })
3511 }
3512
3513 fn actions(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
3514 self.add_workspace_actions_listeners(div, cx)
3515 .on_action(cx.listener(Self::close_inactive_items_and_panes))
3516 .on_action(cx.listener(Self::close_all_items_and_panes))
3517 .on_action(cx.listener(Self::save_all))
3518 .on_action(cx.listener(Self::send_keystrokes))
3519 .on_action(cx.listener(Self::add_folder_to_project))
3520 .on_action(cx.listener(Self::follow_next_collaborator))
3521 .on_action(cx.listener(|workspace, _: &Unfollow, cx| {
3522 let pane = workspace.active_pane().clone();
3523 workspace.unfollow(&pane, cx);
3524 }))
3525 .on_action(cx.listener(|workspace, action: &Save, cx| {
3526 workspace
3527 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
3528 .detach_and_log_err(cx);
3529 }))
3530 .on_action(cx.listener(|workspace, _: &SaveAs, cx| {
3531 workspace
3532 .save_active_item(SaveIntent::SaveAs, cx)
3533 .detach_and_log_err(cx);
3534 }))
3535 .on_action(cx.listener(|workspace, _: &ActivatePreviousPane, cx| {
3536 workspace.activate_previous_pane(cx)
3537 }))
3538 .on_action(
3539 cx.listener(|workspace, _: &ActivateNextPane, cx| workspace.activate_next_pane(cx)),
3540 )
3541 .on_action(
3542 cx.listener(|workspace, action: &ActivatePaneInDirection, cx| {
3543 workspace.activate_pane_in_direction(action.0, cx)
3544 }),
3545 )
3546 .on_action(cx.listener(|workspace, action: &SwapPaneInDirection, cx| {
3547 workspace.swap_pane_in_direction(action.0, cx)
3548 }))
3549 .on_action(cx.listener(|this, _: &ToggleLeftDock, cx| {
3550 this.toggle_dock(DockPosition::Left, cx);
3551 }))
3552 .on_action(
3553 cx.listener(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
3554 workspace.toggle_dock(DockPosition::Right, cx);
3555 }),
3556 )
3557 .on_action(
3558 cx.listener(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
3559 workspace.toggle_dock(DockPosition::Bottom, cx);
3560 }),
3561 )
3562 .on_action(
3563 cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
3564 workspace.close_all_docks(cx);
3565 }),
3566 )
3567 .on_action(cx.listener(Workspace::open))
3568 .on_action(cx.listener(Workspace::close_window))
3569 .on_action(cx.listener(Workspace::activate_pane_at_index))
3570 .on_action(
3571 cx.listener(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
3572 workspace.reopen_closed_item(cx).detach();
3573 }),
3574 )
3575 }
3576
3577 #[cfg(any(test, feature = "test-support"))]
3578 pub fn test_new(project: Model<Project>, cx: &mut ViewContext<Self>) -> Self {
3579 use node_runtime::FakeNodeRuntime;
3580
3581 let client = project.read(cx).client();
3582 let user_store = project.read(cx).user_store();
3583
3584 let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
3585 cx.activate_window();
3586 let app_state = Arc::new(AppState {
3587 languages: project.read(cx).languages().clone(),
3588 workspace_store,
3589 client,
3590 user_store,
3591 fs: project.read(cx).fs().clone(),
3592 build_window_options: |_, _, _| Default::default(),
3593 node_runtime: FakeNodeRuntime::new(),
3594 });
3595 let workspace = Self::new(0, project, app_state, cx);
3596 workspace.active_pane.update(cx, |pane, cx| pane.focus(cx));
3597 workspace
3598 }
3599
3600 pub fn register_action<A: Action>(
3601 &mut self,
3602 callback: impl Fn(&mut Self, &A, &mut ViewContext<Self>) + 'static,
3603 ) -> &mut Self {
3604 let callback = Arc::new(callback);
3605
3606 self.workspace_actions.push(Box::new(move |div, cx| {
3607 let callback = callback.clone();
3608 div.on_action(
3609 cx.listener(move |workspace, event, cx| (callback.clone())(workspace, event, cx)),
3610 )
3611 }));
3612 self
3613 }
3614
3615 fn add_workspace_actions_listeners(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
3616 let mut div = div
3617 .on_action(cx.listener(Self::close_inactive_items_and_panes))
3618 .on_action(cx.listener(Self::close_all_items_and_panes))
3619 .on_action(cx.listener(Self::add_folder_to_project))
3620 .on_action(cx.listener(Self::save_all))
3621 .on_action(cx.listener(Self::open));
3622 for action in self.workspace_actions.iter() {
3623 div = (action)(div, cx)
3624 }
3625 div
3626 }
3627
3628 pub fn has_active_modal(&self, cx: &WindowContext<'_>) -> bool {
3629 self.modal_layer.read(cx).has_active_modal()
3630 }
3631
3632 pub fn active_modal<V: ManagedView + 'static>(&mut self, cx: &AppContext) -> Option<View<V>> {
3633 self.modal_layer.read(cx).active_modal()
3634 }
3635
3636 pub fn toggle_modal<V: ModalView, B>(&mut self, cx: &mut WindowContext, build: B)
3637 where
3638 B: FnOnce(&mut ViewContext<V>) -> V,
3639 {
3640 self.modal_layer
3641 .update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build))
3642 }
3643}
3644
3645fn window_bounds_env_override(cx: &AsyncAppContext) -> Option<WindowBounds> {
3646 let display_origin = cx
3647 .update(|cx| Some(cx.displays().first()?.bounds().origin))
3648 .ok()??;
3649 ZED_WINDOW_POSITION
3650 .zip(*ZED_WINDOW_SIZE)
3651 .map(|(position, size)| {
3652 WindowBounds::Fixed(Bounds {
3653 origin: display_origin + position,
3654 size,
3655 })
3656 })
3657}
3658
3659fn open_items(
3660 serialized_workspace: Option<SerializedWorkspace>,
3661 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
3662 app_state: Arc<AppState>,
3663 cx: &mut ViewContext<Workspace>,
3664) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> {
3665 let restored_items = serialized_workspace.map(|serialized_workspace| {
3666 Workspace::load_workspace(
3667 serialized_workspace,
3668 project_paths_to_open
3669 .iter()
3670 .map(|(_, project_path)| project_path)
3671 .cloned()
3672 .collect(),
3673 cx,
3674 )
3675 });
3676
3677 cx.spawn(|workspace, mut cx| async move {
3678 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
3679
3680 if let Some(restored_items) = restored_items {
3681 let restored_items = restored_items.await?;
3682
3683 let restored_project_paths = restored_items
3684 .iter()
3685 .filter_map(|item| {
3686 cx.update(|cx| item.as_ref()?.project_path(cx))
3687 .ok()
3688 .flatten()
3689 })
3690 .collect::<HashSet<_>>();
3691
3692 for restored_item in restored_items {
3693 opened_items.push(restored_item.map(Ok));
3694 }
3695
3696 project_paths_to_open
3697 .iter_mut()
3698 .for_each(|(_, project_path)| {
3699 if let Some(project_path_to_open) = project_path {
3700 if restored_project_paths.contains(project_path_to_open) {
3701 *project_path = None;
3702 }
3703 }
3704 });
3705 } else {
3706 for _ in 0..project_paths_to_open.len() {
3707 opened_items.push(None);
3708 }
3709 }
3710 assert!(opened_items.len() == project_paths_to_open.len());
3711
3712 let tasks =
3713 project_paths_to_open
3714 .into_iter()
3715 .enumerate()
3716 .map(|(i, (abs_path, project_path))| {
3717 let workspace = workspace.clone();
3718 cx.spawn(|mut cx| {
3719 let fs = app_state.fs.clone();
3720 async move {
3721 let file_project_path = project_path?;
3722 if fs.is_file(&abs_path).await {
3723 Some((
3724 i,
3725 workspace
3726 .update(&mut cx, |workspace, cx| {
3727 workspace.open_path(file_project_path, None, true, cx)
3728 })
3729 .log_err()?
3730 .await,
3731 ))
3732 } else {
3733 None
3734 }
3735 }
3736 })
3737 });
3738
3739 let tasks = tasks.collect::<Vec<_>>();
3740
3741 let tasks = futures::future::join_all(tasks.into_iter());
3742 for maybe_opened_path in tasks.await.into_iter() {
3743 if let Some((i, path_open_result)) = maybe_opened_path {
3744 opened_items[i] = Some(path_open_result);
3745 }
3746 }
3747
3748 Ok(opened_items)
3749 })
3750}
3751
3752enum ActivateInDirectionTarget {
3753 Pane(View<Pane>),
3754 Dock(View<Dock>),
3755}
3756
3757fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncAppContext) {
3758 const REPORT_ISSUE_URL: &str = "https://github.com/zed-industries/zed/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
3759
3760 workspace
3761 .update(cx, |workspace, cx| {
3762 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
3763 workspace.show_notification_once(0, cx, |cx| {
3764 cx.new_view(|_| {
3765 MessageNotification::new("Failed to load the database file.")
3766 .with_click_message("Click to let us know about this error")
3767 .on_click(|cx| cx.open_url(REPORT_ISSUE_URL))
3768 })
3769 });
3770 }
3771 })
3772 .log_err();
3773}
3774
3775impl FocusableView for Workspace {
3776 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
3777 self.active_pane.focus_handle(cx)
3778 }
3779}
3780
3781#[derive(Clone, Render)]
3782struct DraggedDock(DockPosition);
3783
3784impl Render for Workspace {
3785 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3786 let mut context = KeyContext::default();
3787 context.add("Workspace");
3788
3789 let (ui_font, ui_font_size) = {
3790 let theme_settings = ThemeSettings::get_global(cx);
3791 (
3792 theme_settings.ui_font.family.clone(),
3793 theme_settings.ui_font_size.clone(),
3794 )
3795 };
3796
3797 let theme = cx.theme().clone();
3798 let colors = theme.colors();
3799 cx.set_rem_size(ui_font_size);
3800
3801 self.actions(div(), cx)
3802 .key_context(context)
3803 .relative()
3804 .size_full()
3805 .flex()
3806 .flex_col()
3807 .font(ui_font)
3808 .gap_0()
3809 .justify_start()
3810 .items_start()
3811 .text_color(colors.text)
3812 .bg(colors.background)
3813 .border()
3814 .border_color(colors.border)
3815 .children(self.titlebar_item.clone())
3816 .child(
3817 div()
3818 .id("workspace")
3819 .relative()
3820 .flex_1()
3821 .w_full()
3822 .flex()
3823 .flex_col()
3824 .overflow_hidden()
3825 .border_t()
3826 .border_b()
3827 .border_color(colors.border)
3828 .child(
3829 canvas({
3830 let this = cx.view().clone();
3831 move |bounds, cx| {
3832 this.update(cx, |this, _cx| {
3833 this.bounds = *bounds;
3834 })
3835 }
3836 })
3837 .absolute()
3838 .size_full(),
3839 )
3840 .on_drag_move(
3841 cx.listener(|workspace, e: &DragMoveEvent<DraggedDock>, cx| {
3842 match e.drag(cx).0 {
3843 DockPosition::Left => {
3844 let size = workspace.bounds.left() + e.event.position.x;
3845 workspace.left_dock.update(cx, |left_dock, cx| {
3846 left_dock.resize_active_panel(Some(size), cx);
3847 });
3848 }
3849 DockPosition::Right => {
3850 let size = workspace.bounds.right() - e.event.position.x;
3851 workspace.right_dock.update(cx, |right_dock, cx| {
3852 right_dock.resize_active_panel(Some(size), cx);
3853 });
3854 }
3855 DockPosition::Bottom => {
3856 let size = workspace.bounds.bottom() - e.event.position.y;
3857 workspace.bottom_dock.update(cx, |bottom_dock, cx| {
3858 bottom_dock.resize_active_panel(Some(size), cx);
3859 });
3860 }
3861 }
3862 }),
3863 )
3864 .child(self.modal_layer.clone())
3865 .child(
3866 div()
3867 .flex()
3868 .flex_row()
3869 .h_full()
3870 // Left Dock
3871 .children(self.zoomed_position.ne(&Some(DockPosition::Left)).then(
3872 || {
3873 div()
3874 .flex()
3875 .flex_none()
3876 .overflow_hidden()
3877 .child(self.left_dock.clone())
3878 },
3879 ))
3880 // Panes
3881 .child(
3882 div()
3883 .flex()
3884 .flex_col()
3885 .flex_1()
3886 .overflow_hidden()
3887 .child(self.center.render(
3888 &self.project,
3889 &self.follower_states,
3890 self.active_call(),
3891 &self.active_pane,
3892 self.zoomed.as_ref(),
3893 &self.app_state,
3894 cx,
3895 ))
3896 .children(
3897 self.zoomed_position
3898 .ne(&Some(DockPosition::Bottom))
3899 .then(|| self.bottom_dock.clone()),
3900 ),
3901 )
3902 // Right Dock
3903 .children(self.zoomed_position.ne(&Some(DockPosition::Right)).then(
3904 || {
3905 div()
3906 .flex()
3907 .flex_none()
3908 .overflow_hidden()
3909 .child(self.right_dock.clone())
3910 },
3911 )),
3912 )
3913 .children(self.render_notifications(cx))
3914 .children(self.zoomed.as_ref().and_then(|view| {
3915 let zoomed_view = view.upgrade()?;
3916 let div = div()
3917 .z_index(1)
3918 .absolute()
3919 .overflow_hidden()
3920 .border_color(colors.border)
3921 .bg(colors.background)
3922 .child(zoomed_view)
3923 .inset_0()
3924 .shadow_lg();
3925
3926 Some(match self.zoomed_position {
3927 Some(DockPosition::Left) => div.right_2().border_r(),
3928 Some(DockPosition::Right) => div.left_2().border_l(),
3929 Some(DockPosition::Bottom) => div.top_2().border_t(),
3930 None => div.top_2().bottom_2().left_2().right_2().border(),
3931 })
3932 })),
3933 )
3934 .child(self.status_bar.clone())
3935 .children(if self.project.read(cx).is_disconnected() {
3936 Some(DisconnectedOverlay)
3937 } else {
3938 None
3939 })
3940 }
3941}
3942
3943impl WorkspaceStore {
3944 pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
3945 Self {
3946 workspaces: Default::default(),
3947 _subscriptions: vec![
3948 client.add_request_handler(cx.weak_model(), Self::handle_follow),
3949 client.add_message_handler(cx.weak_model(), Self::handle_update_followers),
3950 ],
3951 client,
3952 }
3953 }
3954
3955 pub fn update_followers(
3956 &self,
3957 project_id: Option<u64>,
3958 update: proto::update_followers::Variant,
3959 cx: &AppContext,
3960 ) -> Option<()> {
3961 let active_call = ActiveCall::try_global(cx)?;
3962 let room_id = active_call.read(cx).room()?.read(cx).id();
3963 self.client
3964 .send(proto::UpdateFollowers {
3965 room_id,
3966 project_id,
3967 variant: Some(update),
3968 })
3969 .log_err()
3970 }
3971
3972 pub async fn handle_follow(
3973 this: Model<Self>,
3974 envelope: TypedEnvelope<proto::Follow>,
3975 _: Arc<Client>,
3976 mut cx: AsyncAppContext,
3977 ) -> Result<proto::FollowResponse> {
3978 this.update(&mut cx, |this, cx| {
3979 let follower = Follower {
3980 project_id: envelope.payload.project_id,
3981 peer_id: envelope.original_sender_id()?,
3982 };
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 || workspace.project.read(cx).remote_id() == follower.project_id
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 response.active_view_id.is_none()
4005 || workspace.project.read(cx).remote_id() == follower.project_id
4006 {
4007 response.active_view = Some(active_view)
4008 }
4009 }
4010 })
4011 .is_ok()
4012 });
4013
4014 Ok(response)
4015 })?
4016 }
4017
4018 async fn handle_update_followers(
4019 this: Model<Self>,
4020 envelope: TypedEnvelope<proto::UpdateFollowers>,
4021 _: Arc<Client>,
4022 mut cx: AsyncAppContext,
4023 ) -> Result<()> {
4024 let leader_id = envelope.original_sender_id()?;
4025 let update = envelope.payload;
4026
4027 this.update(&mut cx, |this, cx| {
4028 this.workspaces.retain(|workspace| {
4029 workspace
4030 .update(cx, |workspace, cx| {
4031 let project_id = workspace.project.read(cx).remote_id();
4032 if update.project_id != project_id && update.project_id.is_some() {
4033 return;
4034 }
4035 workspace.handle_update_followers(leader_id, update.clone(), cx);
4036 })
4037 .is_ok()
4038 });
4039 Ok(())
4040 })?
4041 }
4042}
4043
4044impl ViewId {
4045 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
4046 Ok(Self {
4047 creator: message
4048 .creator
4049 .ok_or_else(|| anyhow!("creator is missing"))?,
4050 id: message.id,
4051 })
4052 }
4053
4054 pub(crate) fn to_proto(&self) -> proto::ViewId {
4055 proto::ViewId {
4056 creator: Some(self.creator),
4057 id: self.id,
4058 }
4059 }
4060}
4061
4062pub trait WorkspaceHandle {
4063 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
4064}
4065
4066impl WorkspaceHandle for View<Workspace> {
4067 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
4068 self.read(cx)
4069 .worktrees(cx)
4070 .flat_map(|worktree| {
4071 let worktree_id = worktree.read(cx).id();
4072 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
4073 worktree_id,
4074 path: f.path.clone(),
4075 })
4076 })
4077 .collect::<Vec<_>>()
4078 }
4079}
4080
4081impl std::fmt::Debug for OpenPaths {
4082 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4083 f.debug_struct("OpenPaths")
4084 .field("paths", &self.paths)
4085 .finish()
4086 }
4087}
4088
4089pub fn activate_workspace_for_project(
4090 cx: &mut AppContext,
4091 predicate: impl Fn(&Project, &AppContext) -> bool + Send + 'static,
4092) -> Option<WindowHandle<Workspace>> {
4093 for window in cx.windows() {
4094 let Some(workspace) = window.downcast::<Workspace>() else {
4095 continue;
4096 };
4097
4098 let predicate = workspace
4099 .update(cx, |workspace, cx| {
4100 let project = workspace.project.read(cx);
4101 if predicate(project, cx) {
4102 cx.activate_window();
4103 true
4104 } else {
4105 false
4106 }
4107 })
4108 .log_err()
4109 .unwrap_or(false);
4110
4111 if predicate {
4112 return Some(workspace);
4113 }
4114 }
4115
4116 None
4117}
4118
4119pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
4120 DB.last_workspace().await.log_err().flatten()
4121}
4122
4123actions!(collab, [OpenChannelNotes]);
4124
4125async fn join_channel_internal(
4126 channel_id: ChannelId,
4127 app_state: &Arc<AppState>,
4128 requesting_window: Option<WindowHandle<Workspace>>,
4129 active_call: &Model<ActiveCall>,
4130 cx: &mut AsyncAppContext,
4131) -> Result<bool> {
4132 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
4133 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
4134 return (false, None);
4135 };
4136
4137 let already_in_channel = room.channel_id() == Some(channel_id);
4138 let should_prompt = room.is_sharing_project()
4139 && room.remote_participants().len() > 0
4140 && !already_in_channel;
4141 let open_room = if already_in_channel {
4142 active_call.room().cloned()
4143 } else {
4144 None
4145 };
4146 (should_prompt, open_room)
4147 })?;
4148
4149 if let Some(room) = open_room {
4150 let task = room.update(cx, |room, cx| {
4151 if let Some((project, host)) = room.most_active_project(cx) {
4152 return Some(join_remote_project(project, host, app_state.clone(), cx));
4153 }
4154
4155 None
4156 })?;
4157 if let Some(task) = task {
4158 task.await?;
4159 }
4160 return anyhow::Ok(true);
4161 }
4162
4163 if should_prompt {
4164 if let Some(workspace) = requesting_window {
4165 let answer = workspace
4166 .update(cx, |_, cx| {
4167 cx.prompt(
4168 PromptLevel::Warning,
4169 "Do you want to switch channels?",
4170 Some("Leaving this call will unshare your current project."),
4171 &["Yes, Join Channel", "Cancel"],
4172 )
4173 })?
4174 .await;
4175
4176 if answer == Ok(1) {
4177 return Ok(false);
4178 }
4179 } else {
4180 return Ok(false); // unreachable!() hopefully
4181 }
4182 }
4183
4184 let client = cx.update(|cx| active_call.read(cx).client())?;
4185
4186 let mut client_status = client.status();
4187
4188 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
4189 'outer: loop {
4190 let Some(status) = client_status.recv().await else {
4191 return Err(anyhow!("error connecting"));
4192 };
4193
4194 match status {
4195 Status::Connecting
4196 | Status::Authenticating
4197 | Status::Reconnecting
4198 | Status::Reauthenticating => continue,
4199 Status::Connected { .. } => break 'outer,
4200 Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
4201 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
4202 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
4203 return Err(ErrorCode::Disconnected.into());
4204 }
4205 }
4206 }
4207
4208 let room = active_call
4209 .update(cx, |active_call, cx| {
4210 active_call.join_channel(channel_id, cx)
4211 })?
4212 .await?;
4213
4214 let Some(room) = room else {
4215 return anyhow::Ok(true);
4216 };
4217
4218 room.update(cx, |room, _| room.room_update_completed())?
4219 .await;
4220
4221 let task = room.update(cx, |room, cx| {
4222 if let Some((project, host)) = room.most_active_project(cx) {
4223 return Some(join_remote_project(project, host, app_state.clone(), cx));
4224 }
4225
4226 // if you are the first to join a channel, share your project
4227 if room.remote_participants().len() == 0 && !room.local_participant_is_guest() {
4228 if let Some(workspace) = requesting_window {
4229 let project = workspace.update(cx, |workspace, cx| {
4230 if !CallSettings::get_global(cx).share_on_join {
4231 return None;
4232 }
4233 let project = workspace.project.read(cx);
4234 if project.is_local()
4235 && project.visible_worktrees(cx).any(|tree| {
4236 tree.read(cx)
4237 .root_entry()
4238 .map_or(false, |entry| entry.is_dir())
4239 })
4240 {
4241 Some(workspace.project.clone())
4242 } else {
4243 None
4244 }
4245 });
4246 if let Ok(Some(project)) = project {
4247 return Some(cx.spawn(|room, mut cx| async move {
4248 room.update(&mut cx, |room, cx| room.share_project(project, cx))?
4249 .await?;
4250 Ok(())
4251 }));
4252 }
4253 }
4254 }
4255
4256 None
4257 })?;
4258 if let Some(task) = task {
4259 task.await?;
4260 return anyhow::Ok(true);
4261 }
4262 anyhow::Ok(false)
4263}
4264
4265pub fn join_channel(
4266 channel_id: ChannelId,
4267 app_state: Arc<AppState>,
4268 requesting_window: Option<WindowHandle<Workspace>>,
4269 cx: &mut AppContext,
4270) -> Task<Result<()>> {
4271 let active_call = ActiveCall::global(cx);
4272 cx.spawn(|mut cx| async move {
4273 let result = join_channel_internal(
4274 channel_id,
4275 &app_state,
4276 requesting_window,
4277 &active_call,
4278 &mut cx,
4279 )
4280 .await;
4281
4282 // join channel succeeded, and opened a window
4283 if matches!(result, Ok(true)) {
4284 return anyhow::Ok(());
4285 }
4286
4287 // find an existing workspace to focus and show call controls
4288 let mut active_window =
4289 requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
4290 if active_window.is_none() {
4291 // no open workspaces, make one to show the error in (blergh)
4292 let (window_handle, _) = cx
4293 .update(|cx| {
4294 Workspace::new_local(vec![], app_state.clone(), requesting_window, cx)
4295 })?
4296 .await?;
4297
4298 if result.is_ok() {
4299 cx.update(|cx| {
4300 cx.dispatch_action(&OpenChannelNotes);
4301 }).log_err();
4302 }
4303
4304 active_window = Some(window_handle);
4305 }
4306
4307 if let Err(err) = result {
4308 log::error!("failed to join channel: {}", err);
4309 if let Some(active_window) = active_window {
4310 active_window
4311 .update(&mut cx, |_, cx| {
4312 let detail: SharedString = match err.error_code() {
4313 ErrorCode::SignedOut => {
4314 "Please sign in to continue.".into()
4315 }
4316 ErrorCode::UpgradeRequired => {
4317 "Your are running an unsupported version of Zed. Please update to continue.".into()
4318 }
4319 ErrorCode::NoSuchChannel => {
4320 "No matching channel was found. Please check the link and try again.".into()
4321 }
4322 ErrorCode::Forbidden => {
4323 "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
4324 }
4325 ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
4326 _ => format!("{}\n\nPlease try again.", err).into(),
4327 };
4328 cx.prompt(
4329 PromptLevel::Critical,
4330 "Failed to join channel",
4331 Some(&detail),
4332 &["Ok"],
4333 )
4334 })?
4335 .await
4336 .ok();
4337 }
4338 }
4339
4340 // return ok, we showed the error to the user.
4341 return anyhow::Ok(());
4342 })
4343}
4344
4345pub async fn get_any_active_workspace(
4346 app_state: Arc<AppState>,
4347 mut cx: AsyncAppContext,
4348) -> anyhow::Result<WindowHandle<Workspace>> {
4349 // find an existing workspace to focus and show call controls
4350 let active_window = activate_any_workspace_window(&mut cx);
4351 if active_window.is_none() {
4352 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, cx))?
4353 .await?;
4354 }
4355 activate_any_workspace_window(&mut cx).context("could not open zed")
4356}
4357
4358fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandle<Workspace>> {
4359 cx.update(|cx| {
4360 for window in cx.windows() {
4361 if let Some(workspace_window) = window.downcast::<Workspace>() {
4362 workspace_window
4363 .update(cx, |_, cx| cx.activate_window())
4364 .ok();
4365 return Some(workspace_window);
4366 }
4367 }
4368 None
4369 })
4370 .ok()
4371 .flatten()
4372}
4373
4374#[allow(clippy::type_complexity)]
4375pub fn open_paths(
4376 abs_paths: &[PathBuf],
4377 app_state: &Arc<AppState>,
4378 requesting_window: Option<WindowHandle<Workspace>>,
4379 cx: &mut AppContext,
4380) -> Task<
4381 anyhow::Result<(
4382 WindowHandle<Workspace>,
4383 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
4384 )>,
4385> {
4386 let app_state = app_state.clone();
4387 let abs_paths = abs_paths.to_vec();
4388 // Open paths in existing workspace if possible
4389 let existing = activate_workspace_for_project(cx, {
4390 let abs_paths = abs_paths.clone();
4391 move |project, cx| project.contains_paths(&abs_paths, cx)
4392 });
4393 cx.spawn(move |mut cx| async move {
4394 if let Some(existing) = existing {
4395 Ok((
4396 existing.clone(),
4397 existing
4398 .update(&mut cx, |workspace, cx| {
4399 workspace.open_paths(abs_paths, OpenVisible::All, None, cx)
4400 })?
4401 .await,
4402 ))
4403 } else {
4404 cx.update(move |cx| {
4405 Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx)
4406 })?
4407 .await
4408 }
4409 })
4410}
4411
4412pub fn open_new(
4413 app_state: &Arc<AppState>,
4414 cx: &mut AppContext,
4415 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static + Send,
4416) -> Task<()> {
4417 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
4418 cx.spawn(|mut cx| async move {
4419 if let Some((workspace, opened_paths)) = task.await.log_err() {
4420 workspace
4421 .update(&mut cx, |workspace, cx| {
4422 if opened_paths.is_empty() {
4423 init(workspace, cx)
4424 }
4425 })
4426 .log_err();
4427 }
4428 })
4429}
4430
4431pub fn create_and_open_local_file(
4432 path: &'static Path,
4433 cx: &mut ViewContext<Workspace>,
4434 default_content: impl 'static + Send + FnOnce() -> Rope,
4435) -> Task<Result<Box<dyn ItemHandle>>> {
4436 cx.spawn(|workspace, mut cx| async move {
4437 let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
4438 if !fs.is_file(path).await {
4439 fs.create_file(path, Default::default()).await?;
4440 fs.save(path, &default_content(), Default::default())
4441 .await?;
4442 }
4443
4444 let mut items = workspace
4445 .update(&mut cx, |workspace, cx| {
4446 workspace.with_local_workspace(cx, |workspace, cx| {
4447 workspace.open_paths(vec![path.to_path_buf()], OpenVisible::None, None, cx)
4448 })
4449 })?
4450 .await?
4451 .await;
4452
4453 let item = items.pop().flatten();
4454 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
4455 })
4456}
4457
4458pub fn join_remote_project(
4459 project_id: u64,
4460 follow_user_id: u64,
4461 app_state: Arc<AppState>,
4462 cx: &mut AppContext,
4463) -> Task<Result<()>> {
4464 let windows = cx.windows();
4465 cx.spawn(|mut cx| async move {
4466 let existing_workspace = windows.into_iter().find_map(|window| {
4467 window.downcast::<Workspace>().and_then(|window| {
4468 window
4469 .update(&mut cx, |workspace, cx| {
4470 if workspace.project().read(cx).remote_id() == Some(project_id) {
4471 Some(window)
4472 } else {
4473 None
4474 }
4475 })
4476 .unwrap_or(None)
4477 })
4478 });
4479
4480 let workspace = if let Some(existing_workspace) = existing_workspace {
4481 existing_workspace
4482 } else {
4483 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
4484 let room = active_call
4485 .read_with(&cx, |call, _| call.room().cloned())?
4486 .ok_or_else(|| anyhow!("not in a call"))?;
4487 let project = room
4488 .update(&mut cx, |room, cx| {
4489 room.join_project(
4490 project_id,
4491 app_state.languages.clone(),
4492 app_state.fs.clone(),
4493 cx,
4494 )
4495 })?
4496 .await?;
4497
4498 let window_bounds_override = window_bounds_env_override(&cx);
4499 cx.update(|cx| {
4500 let options = (app_state.build_window_options)(window_bounds_override, None, cx);
4501 cx.open_window(options, |cx| {
4502 cx.new_view(|cx| Workspace::new(0, project, app_state.clone(), cx))
4503 })
4504 })?
4505 };
4506
4507 workspace.update(&mut cx, |workspace, cx| {
4508 cx.activate(true);
4509 cx.activate_window();
4510
4511 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
4512 let follow_peer_id = room
4513 .read(cx)
4514 .remote_participants()
4515 .iter()
4516 .find(|(_, participant)| participant.user.id == follow_user_id)
4517 .map(|(_, p)| p.peer_id)
4518 .or_else(|| {
4519 // If we couldn't follow the given user, follow the host instead.
4520 let collaborator = workspace
4521 .project()
4522 .read(cx)
4523 .collaborators()
4524 .values()
4525 .find(|collaborator| collaborator.replica_id == 0)?;
4526 Some(collaborator.peer_id)
4527 });
4528
4529 if let Some(follow_peer_id) = follow_peer_id {
4530 workspace.follow(follow_peer_id, cx);
4531 }
4532 }
4533 })?;
4534
4535 anyhow::Ok(())
4536 })
4537}
4538
4539pub fn restart(_: &Restart, cx: &mut AppContext) {
4540 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
4541 let mut workspace_windows = cx
4542 .windows()
4543 .into_iter()
4544 .filter_map(|window| window.downcast::<Workspace>())
4545 .collect::<Vec<_>>();
4546
4547 // If multiple windows have unsaved changes, and need a save prompt,
4548 // prompt in the active window before switching to a different window.
4549 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
4550
4551 let mut prompt = None;
4552 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
4553 prompt = window
4554 .update(cx, |_, cx| {
4555 cx.prompt(
4556 PromptLevel::Info,
4557 "Are you sure you want to restart?",
4558 None,
4559 &["Restart", "Cancel"],
4560 )
4561 })
4562 .ok();
4563 }
4564
4565 cx.spawn(|mut cx| async move {
4566 if let Some(prompt) = prompt {
4567 let answer = prompt.await?;
4568 if answer != 0 {
4569 return Ok(());
4570 }
4571 }
4572
4573 // If the user cancels any save prompt, then keep the app open.
4574 for window in workspace_windows {
4575 if let Ok(should_close) = window.update(&mut cx, |workspace, cx| {
4576 workspace.prepare_to_close(true, cx)
4577 }) {
4578 if !should_close.await? {
4579 return Ok(());
4580 }
4581 }
4582 }
4583
4584 cx.update(|cx| cx.restart())
4585 })
4586 .detach_and_log_err(cx);
4587}
4588
4589fn parse_pixel_position_env_var(value: &str) -> Option<Point<GlobalPixels>> {
4590 let mut parts = value.split(',');
4591 let x: usize = parts.next()?.parse().ok()?;
4592 let y: usize = parts.next()?.parse().ok()?;
4593 Some(point((x as f64).into(), (y as f64).into()))
4594}
4595
4596fn parse_pixel_size_env_var(value: &str) -> Option<Size<GlobalPixels>> {
4597 let mut parts = value.split(',');
4598 let width: usize = parts.next()?.parse().ok()?;
4599 let height: usize = parts.next()?.parse().ok()?;
4600 Some(size((width as f64).into(), (height as f64).into()))
4601}
4602
4603pub fn titlebar_height(cx: &mut WindowContext) -> Pixels {
4604 (1.75 * cx.rem_size()).max(px(32.))
4605}
4606
4607struct DisconnectedOverlay;
4608
4609impl Element for DisconnectedOverlay {
4610 type State = AnyElement;
4611
4612 fn request_layout(
4613 &mut self,
4614 _: Option<Self::State>,
4615 cx: &mut ElementContext,
4616 ) -> (LayoutId, Self::State) {
4617 let mut background = cx.theme().colors().elevated_surface_background;
4618 background.fade_out(0.2);
4619 let mut overlay = div()
4620 .bg(background)
4621 .absolute()
4622 .left_0()
4623 .top(titlebar_height(cx))
4624 .size_full()
4625 .flex()
4626 .items_center()
4627 .justify_center()
4628 .capture_any_mouse_down(|_, cx| cx.stop_propagation())
4629 .capture_any_mouse_up(|_, cx| cx.stop_propagation())
4630 .child(Label::new(
4631 "Your connection to the remote project has been lost.",
4632 ))
4633 .into_any();
4634 (overlay.request_layout(cx), overlay)
4635 }
4636
4637 fn paint(
4638 &mut self,
4639 bounds: Bounds<Pixels>,
4640 overlay: &mut Self::State,
4641 cx: &mut ElementContext,
4642 ) {
4643 cx.with_z_index(u16::MAX, |cx| {
4644 cx.add_opaque_layer(bounds);
4645 overlay.paint(cx);
4646 })
4647 }
4648}
4649
4650impl IntoElement for DisconnectedOverlay {
4651 type Element = Self;
4652
4653 fn element_id(&self) -> Option<ui::prelude::ElementId> {
4654 None
4655 }
4656
4657 fn into_element(self) -> Self::Element {
4658 self
4659 }
4660}
4661
4662#[cfg(test)]
4663mod tests {
4664 use std::{cell::RefCell, rc::Rc};
4665
4666 use super::*;
4667 use crate::{
4668 dock::{test::TestPanel, PanelEvent},
4669 item::{
4670 test::{TestItem, TestProjectItem},
4671 ItemEvent,
4672 },
4673 };
4674 use fs::FakeFs;
4675 use gpui::{px, DismissEvent, TestAppContext, VisualTestContext};
4676 use project::{Project, ProjectEntryId};
4677 use serde_json::json;
4678 use settings::SettingsStore;
4679
4680 #[gpui::test]
4681 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
4682 init_test(cx);
4683
4684 let fs = FakeFs::new(cx.executor());
4685 let project = Project::test(fs, [], cx).await;
4686 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4687
4688 // Adding an item with no ambiguity renders the tab without detail.
4689 let item1 = cx.new_view(|cx| {
4690 let mut item = TestItem::new(cx);
4691 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
4692 item
4693 });
4694 workspace.update(cx, |workspace, cx| {
4695 workspace.add_item_to_active_pane(Box::new(item1.clone()), cx);
4696 });
4697 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
4698
4699 // Adding an item that creates ambiguity increases the level of detail on
4700 // both tabs.
4701 let item2 = cx.new_view(|cx| {
4702 let mut item = TestItem::new(cx);
4703 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4704 item
4705 });
4706 workspace.update(cx, |workspace, cx| {
4707 workspace.add_item_to_active_pane(Box::new(item2.clone()), cx);
4708 });
4709 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4710 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4711
4712 // Adding an item that creates ambiguity increases the level of detail only
4713 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
4714 // we stop at the highest detail available.
4715 let item3 = cx.new_view(|cx| {
4716 let mut item = TestItem::new(cx);
4717 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4718 item
4719 });
4720 workspace.update(cx, |workspace, cx| {
4721 workspace.add_item_to_active_pane(Box::new(item3.clone()), cx);
4722 });
4723 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4724 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4725 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4726 }
4727
4728 #[gpui::test]
4729 async fn test_tracking_active_path(cx: &mut TestAppContext) {
4730 init_test(cx);
4731
4732 let fs = FakeFs::new(cx.executor());
4733 fs.insert_tree(
4734 "/root1",
4735 json!({
4736 "one.txt": "",
4737 "two.txt": "",
4738 }),
4739 )
4740 .await;
4741 fs.insert_tree(
4742 "/root2",
4743 json!({
4744 "three.txt": "",
4745 }),
4746 )
4747 .await;
4748
4749 let project = Project::test(fs, ["root1".as_ref()], cx).await;
4750 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4751 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4752 let worktree_id = project.update(cx, |project, cx| {
4753 project.worktrees().next().unwrap().read(cx).id()
4754 });
4755
4756 let item1 = cx.new_view(|cx| {
4757 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
4758 });
4759 let item2 = cx.new_view(|cx| {
4760 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
4761 });
4762
4763 // Add an item to an empty pane
4764 workspace.update(cx, |workspace, cx| {
4765 workspace.add_item_to_active_pane(Box::new(item1), cx)
4766 });
4767 project.update(cx, |project, cx| {
4768 assert_eq!(
4769 project.active_entry(),
4770 project
4771 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4772 .map(|e| e.id)
4773 );
4774 });
4775 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
4776
4777 // Add a second item to a non-empty pane
4778 workspace.update(cx, |workspace, cx| {
4779 workspace.add_item_to_active_pane(Box::new(item2), cx)
4780 });
4781 assert_eq!(cx.window_title().as_deref(), Some("two.txt — root1"));
4782 project.update(cx, |project, cx| {
4783 assert_eq!(
4784 project.active_entry(),
4785 project
4786 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
4787 .map(|e| e.id)
4788 );
4789 });
4790
4791 // Close the active item
4792 pane.update(cx, |pane, cx| {
4793 pane.close_active_item(&Default::default(), cx).unwrap()
4794 })
4795 .await
4796 .unwrap();
4797 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
4798 project.update(cx, |project, cx| {
4799 assert_eq!(
4800 project.active_entry(),
4801 project
4802 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4803 .map(|e| e.id)
4804 );
4805 });
4806
4807 // Add a project folder
4808 project
4809 .update(cx, |project, cx| {
4810 project.find_or_create_local_worktree("/root2", true, cx)
4811 })
4812 .await
4813 .unwrap();
4814 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1, root2"));
4815
4816 // Remove a project folder
4817 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
4818 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root2"));
4819 }
4820
4821 #[gpui::test]
4822 async fn test_close_window(cx: &mut TestAppContext) {
4823 init_test(cx);
4824
4825 let fs = FakeFs::new(cx.executor());
4826 fs.insert_tree("/root", json!({ "one": "" })).await;
4827
4828 let project = Project::test(fs, ["root".as_ref()], cx).await;
4829 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4830
4831 // When there are no dirty items, there's nothing to do.
4832 let item1 = cx.new_view(|cx| TestItem::new(cx));
4833 workspace.update(cx, |w, cx| {
4834 w.add_item_to_active_pane(Box::new(item1.clone()), cx)
4835 });
4836 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4837 assert!(task.await.unwrap());
4838
4839 // When there are dirty untitled items, prompt to save each one. If the user
4840 // cancels any prompt, then abort.
4841 let item2 = cx.new_view(|cx| TestItem::new(cx).with_dirty(true));
4842 let item3 = cx.new_view(|cx| {
4843 TestItem::new(cx)
4844 .with_dirty(true)
4845 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4846 });
4847 workspace.update(cx, |w, cx| {
4848 w.add_item_to_active_pane(Box::new(item2.clone()), cx);
4849 w.add_item_to_active_pane(Box::new(item3.clone()), cx);
4850 });
4851 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4852 cx.executor().run_until_parked();
4853 cx.simulate_prompt_answer(2); // cancel save all
4854 cx.executor().run_until_parked();
4855 cx.simulate_prompt_answer(2); // cancel save all
4856 cx.executor().run_until_parked();
4857 assert!(!cx.has_pending_prompt());
4858 assert!(!task.await.unwrap());
4859 }
4860
4861 #[gpui::test]
4862 async fn test_close_pane_items(cx: &mut TestAppContext) {
4863 init_test(cx);
4864
4865 let fs = FakeFs::new(cx.executor());
4866
4867 let project = Project::test(fs, None, cx).await;
4868 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4869
4870 let item1 = cx.new_view(|cx| {
4871 TestItem::new(cx)
4872 .with_dirty(true)
4873 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4874 });
4875 let item2 = cx.new_view(|cx| {
4876 TestItem::new(cx)
4877 .with_dirty(true)
4878 .with_conflict(true)
4879 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
4880 });
4881 let item3 = cx.new_view(|cx| {
4882 TestItem::new(cx)
4883 .with_dirty(true)
4884 .with_conflict(true)
4885 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
4886 });
4887 let item4 = cx.new_view(|cx| {
4888 TestItem::new(cx)
4889 .with_dirty(true)
4890 .with_project_items(&[TestProjectItem::new_untitled(cx)])
4891 });
4892 let pane = workspace.update(cx, |workspace, cx| {
4893 workspace.add_item_to_active_pane(Box::new(item1.clone()), cx);
4894 workspace.add_item_to_active_pane(Box::new(item2.clone()), cx);
4895 workspace.add_item_to_active_pane(Box::new(item3.clone()), cx);
4896 workspace.add_item_to_active_pane(Box::new(item4.clone()), cx);
4897 workspace.active_pane().clone()
4898 });
4899
4900 let close_items = pane.update(cx, |pane, cx| {
4901 pane.activate_item(1, true, true, cx);
4902 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
4903 let item1_id = item1.item_id();
4904 let item3_id = item3.item_id();
4905 let item4_id = item4.item_id();
4906 pane.close_items(cx, SaveIntent::Close, move |id| {
4907 [item1_id, item3_id, item4_id].contains(&id)
4908 })
4909 });
4910 cx.executor().run_until_parked();
4911
4912 assert!(cx.has_pending_prompt());
4913 // Ignore "Save all" prompt
4914 cx.simulate_prompt_answer(2);
4915 cx.executor().run_until_parked();
4916 // There's a prompt to save item 1.
4917 pane.update(cx, |pane, _| {
4918 assert_eq!(pane.items_len(), 4);
4919 assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
4920 });
4921 // Confirm saving item 1.
4922 cx.simulate_prompt_answer(0);
4923 cx.executor().run_until_parked();
4924
4925 // Item 1 is saved. There's a prompt to save item 3.
4926 pane.update(cx, |pane, cx| {
4927 assert_eq!(item1.read(cx).save_count, 1);
4928 assert_eq!(item1.read(cx).save_as_count, 0);
4929 assert_eq!(item1.read(cx).reload_count, 0);
4930 assert_eq!(pane.items_len(), 3);
4931 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
4932 });
4933 assert!(cx.has_pending_prompt());
4934
4935 // Cancel saving item 3.
4936 cx.simulate_prompt_answer(1);
4937 cx.executor().run_until_parked();
4938
4939 // Item 3 is reloaded. There's a prompt to save item 4.
4940 pane.update(cx, |pane, cx| {
4941 assert_eq!(item3.read(cx).save_count, 0);
4942 assert_eq!(item3.read(cx).save_as_count, 0);
4943 assert_eq!(item3.read(cx).reload_count, 1);
4944 assert_eq!(pane.items_len(), 2);
4945 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
4946 });
4947 assert!(cx.has_pending_prompt());
4948
4949 // Confirm saving item 4.
4950 cx.simulate_prompt_answer(0);
4951 cx.executor().run_until_parked();
4952
4953 // There's a prompt for a path for item 4.
4954 cx.simulate_new_path_selection(|_| Some(Default::default()));
4955 close_items.await.unwrap();
4956
4957 // The requested items are closed.
4958 pane.update(cx, |pane, cx| {
4959 assert_eq!(item4.read(cx).save_count, 0);
4960 assert_eq!(item4.read(cx).save_as_count, 1);
4961 assert_eq!(item4.read(cx).reload_count, 0);
4962 assert_eq!(pane.items_len(), 1);
4963 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
4964 });
4965 }
4966
4967 #[gpui::test]
4968 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
4969 init_test(cx);
4970
4971 let fs = FakeFs::new(cx.executor());
4972 let project = Project::test(fs, [], cx).await;
4973 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4974
4975 // Create several workspace items with single project entries, and two
4976 // workspace items with multiple project entries.
4977 let single_entry_items = (0..=4)
4978 .map(|project_entry_id| {
4979 cx.new_view(|cx| {
4980 TestItem::new(cx)
4981 .with_dirty(true)
4982 .with_project_items(&[TestProjectItem::new(
4983 project_entry_id,
4984 &format!("{project_entry_id}.txt"),
4985 cx,
4986 )])
4987 })
4988 })
4989 .collect::<Vec<_>>();
4990 let item_2_3 = cx.new_view(|cx| {
4991 TestItem::new(cx)
4992 .with_dirty(true)
4993 .with_singleton(false)
4994 .with_project_items(&[
4995 single_entry_items[2].read(cx).project_items[0].clone(),
4996 single_entry_items[3].read(cx).project_items[0].clone(),
4997 ])
4998 });
4999 let item_3_4 = cx.new_view(|cx| {
5000 TestItem::new(cx)
5001 .with_dirty(true)
5002 .with_singleton(false)
5003 .with_project_items(&[
5004 single_entry_items[3].read(cx).project_items[0].clone(),
5005 single_entry_items[4].read(cx).project_items[0].clone(),
5006 ])
5007 });
5008
5009 // Create two panes that contain the following project entries:
5010 // left pane:
5011 // multi-entry items: (2, 3)
5012 // single-entry items: 0, 1, 2, 3, 4
5013 // right pane:
5014 // single-entry items: 1
5015 // multi-entry items: (3, 4)
5016 let left_pane = workspace.update(cx, |workspace, cx| {
5017 let left_pane = workspace.active_pane().clone();
5018 workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), cx);
5019 for item in single_entry_items {
5020 workspace.add_item_to_active_pane(Box::new(item), cx);
5021 }
5022 left_pane.update(cx, |pane, cx| {
5023 pane.activate_item(2, true, true, cx);
5024 });
5025
5026 let right_pane = workspace
5027 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
5028 .unwrap();
5029
5030 right_pane.update(cx, |pane, cx| {
5031 pane.add_item(Box::new(item_3_4.clone()), true, true, None, cx);
5032 });
5033
5034 left_pane
5035 });
5036
5037 cx.focus_view(&left_pane);
5038
5039 // When closing all of the items in the left pane, we should be prompted twice:
5040 // once for project entry 0, and once for project entry 2. Project entries 1,
5041 // 3, and 4 are all still open in the other paten. After those two
5042 // prompts, the task should complete.
5043
5044 let close = left_pane.update(cx, |pane, cx| {
5045 pane.close_all_items(&CloseAllItems::default(), cx).unwrap()
5046 });
5047 cx.executor().run_until_parked();
5048
5049 // Discard "Save all" prompt
5050 cx.simulate_prompt_answer(2);
5051
5052 cx.executor().run_until_parked();
5053 left_pane.update(cx, |pane, cx| {
5054 assert_eq!(
5055 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
5056 &[ProjectEntryId::from_proto(0)]
5057 );
5058 });
5059 cx.simulate_prompt_answer(0);
5060
5061 cx.executor().run_until_parked();
5062 left_pane.update(cx, |pane, cx| {
5063 assert_eq!(
5064 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
5065 &[ProjectEntryId::from_proto(2)]
5066 );
5067 });
5068 cx.simulate_prompt_answer(0);
5069
5070 cx.executor().run_until_parked();
5071 close.await.unwrap();
5072 left_pane.update(cx, |pane, _| {
5073 assert_eq!(pane.items_len(), 0);
5074 });
5075 }
5076
5077 #[gpui::test]
5078 async fn test_autosave(cx: &mut gpui::TestAppContext) {
5079 init_test(cx);
5080
5081 let fs = FakeFs::new(cx.executor());
5082 let project = Project::test(fs, [], cx).await;
5083 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5084 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5085
5086 let item = cx.new_view(|cx| {
5087 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5088 });
5089 let item_id = item.entity_id();
5090 workspace.update(cx, |workspace, cx| {
5091 workspace.add_item_to_active_pane(Box::new(item.clone()), cx);
5092 });
5093
5094 // Autosave on window change.
5095 item.update(cx, |item, cx| {
5096 cx.update_global(|settings: &mut SettingsStore, cx| {
5097 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5098 settings.autosave = Some(AutosaveSetting::OnWindowChange);
5099 })
5100 });
5101 item.is_dirty = true;
5102 });
5103
5104 // Deactivating the window saves the file.
5105 cx.deactivate_window();
5106 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
5107
5108 // Autosave on focus change.
5109 item.update(cx, |item, cx| {
5110 cx.focus_self();
5111 cx.update_global(|settings: &mut SettingsStore, cx| {
5112 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5113 settings.autosave = Some(AutosaveSetting::OnFocusChange);
5114 })
5115 });
5116 item.is_dirty = true;
5117 });
5118
5119 // Blurring the item saves the file.
5120 item.update(cx, |_, cx| cx.blur());
5121 cx.executor().run_until_parked();
5122 item.update(cx, |item, _| assert_eq!(item.save_count, 2));
5123
5124 // Deactivating the window still saves the file.
5125 cx.update(|cx| cx.activate_window());
5126 item.update(cx, |item, cx| {
5127 cx.focus_self();
5128 item.is_dirty = true;
5129 });
5130 cx.deactivate_window();
5131
5132 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
5133
5134 // Autosave after delay.
5135 item.update(cx, |item, cx| {
5136 cx.update_global(|settings: &mut SettingsStore, cx| {
5137 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5138 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
5139 })
5140 });
5141 item.is_dirty = true;
5142 cx.emit(ItemEvent::Edit);
5143 });
5144
5145 // Delay hasn't fully expired, so the file is still dirty and unsaved.
5146 cx.executor().advance_clock(Duration::from_millis(250));
5147 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
5148
5149 // After delay expires, the file is saved.
5150 cx.executor().advance_clock(Duration::from_millis(250));
5151 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
5152
5153 // Autosave on focus change, ensuring closing the tab counts as such.
5154 item.update(cx, |item, cx| {
5155 cx.update_global(|settings: &mut SettingsStore, cx| {
5156 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5157 settings.autosave = Some(AutosaveSetting::OnFocusChange);
5158 })
5159 });
5160 item.is_dirty = true;
5161 });
5162
5163 pane.update(cx, |pane, cx| {
5164 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
5165 })
5166 .await
5167 .unwrap();
5168 assert!(!cx.has_pending_prompt());
5169 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
5170
5171 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
5172 workspace.update(cx, |workspace, cx| {
5173 workspace.add_item_to_active_pane(Box::new(item.clone()), cx);
5174 });
5175 item.update(cx, |item, cx| {
5176 item.project_items[0].update(cx, |item, _| {
5177 item.entry_id = None;
5178 });
5179 item.is_dirty = true;
5180 cx.blur();
5181 });
5182 cx.run_until_parked();
5183 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
5184
5185 // Ensure autosave is prevented for deleted files also when closing the buffer.
5186 let _close_items = pane.update(cx, |pane, cx| {
5187 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
5188 });
5189 cx.run_until_parked();
5190 assert!(cx.has_pending_prompt());
5191 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
5192 }
5193
5194 #[gpui::test]
5195 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
5196 init_test(cx);
5197
5198 let fs = FakeFs::new(cx.executor());
5199
5200 let project = Project::test(fs, [], cx).await;
5201 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5202
5203 let item = cx.new_view(|cx| {
5204 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5205 });
5206 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5207 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
5208 let toolbar_notify_count = Rc::new(RefCell::new(0));
5209
5210 workspace.update(cx, |workspace, cx| {
5211 workspace.add_item_to_active_pane(Box::new(item.clone()), cx);
5212 let toolbar_notification_count = toolbar_notify_count.clone();
5213 cx.observe(&toolbar, move |_, _, _| {
5214 *toolbar_notification_count.borrow_mut() += 1
5215 })
5216 .detach();
5217 });
5218
5219 pane.update(cx, |pane, _| {
5220 assert!(!pane.can_navigate_backward());
5221 assert!(!pane.can_navigate_forward());
5222 });
5223
5224 item.update(cx, |item, cx| {
5225 item.set_state("one".to_string(), cx);
5226 });
5227
5228 // Toolbar must be notified to re-render the navigation buttons
5229 assert_eq!(*toolbar_notify_count.borrow(), 1);
5230
5231 pane.update(cx, |pane, _| {
5232 assert!(pane.can_navigate_backward());
5233 assert!(!pane.can_navigate_forward());
5234 });
5235
5236 workspace
5237 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
5238 .await
5239 .unwrap();
5240
5241 assert_eq!(*toolbar_notify_count.borrow(), 2);
5242 pane.update(cx, |pane, _| {
5243 assert!(!pane.can_navigate_backward());
5244 assert!(pane.can_navigate_forward());
5245 });
5246 }
5247
5248 #[gpui::test]
5249 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
5250 init_test(cx);
5251 let fs = FakeFs::new(cx.executor());
5252
5253 let project = Project::test(fs, [], cx).await;
5254 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5255
5256 let panel = workspace.update(cx, |workspace, cx| {
5257 let panel = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
5258 workspace.add_panel(panel.clone(), cx);
5259
5260 workspace
5261 .right_dock()
5262 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5263
5264 panel
5265 });
5266
5267 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5268 pane.update(cx, |pane, cx| {
5269 let item = cx.new_view(|cx| TestItem::new(cx));
5270 pane.add_item(Box::new(item), true, true, None, cx);
5271 });
5272
5273 // Transfer focus from center to panel
5274 workspace.update(cx, |workspace, cx| {
5275 workspace.toggle_panel_focus::<TestPanel>(cx);
5276 });
5277
5278 workspace.update(cx, |workspace, cx| {
5279 assert!(workspace.right_dock().read(cx).is_open());
5280 assert!(!panel.is_zoomed(cx));
5281 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5282 });
5283
5284 // Transfer focus from panel to center
5285 workspace.update(cx, |workspace, cx| {
5286 workspace.toggle_panel_focus::<TestPanel>(cx);
5287 });
5288
5289 workspace.update(cx, |workspace, cx| {
5290 assert!(workspace.right_dock().read(cx).is_open());
5291 assert!(!panel.is_zoomed(cx));
5292 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5293 });
5294
5295 // Close the dock
5296 workspace.update(cx, |workspace, cx| {
5297 workspace.toggle_dock(DockPosition::Right, cx);
5298 });
5299
5300 workspace.update(cx, |workspace, cx| {
5301 assert!(!workspace.right_dock().read(cx).is_open());
5302 assert!(!panel.is_zoomed(cx));
5303 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5304 });
5305
5306 // Open the dock
5307 workspace.update(cx, |workspace, cx| {
5308 workspace.toggle_dock(DockPosition::Right, cx);
5309 });
5310
5311 workspace.update(cx, |workspace, cx| {
5312 assert!(workspace.right_dock().read(cx).is_open());
5313 assert!(!panel.is_zoomed(cx));
5314 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5315 });
5316
5317 // Focus and zoom panel
5318 panel.update(cx, |panel, cx| {
5319 cx.focus_self();
5320 panel.set_zoomed(true, cx)
5321 });
5322
5323 workspace.update(cx, |workspace, cx| {
5324 assert!(workspace.right_dock().read(cx).is_open());
5325 assert!(panel.is_zoomed(cx));
5326 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5327 });
5328
5329 // Transfer focus to the center closes the dock
5330 workspace.update(cx, |workspace, cx| {
5331 workspace.toggle_panel_focus::<TestPanel>(cx);
5332 });
5333
5334 workspace.update(cx, |workspace, cx| {
5335 assert!(!workspace.right_dock().read(cx).is_open());
5336 assert!(panel.is_zoomed(cx));
5337 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5338 });
5339
5340 // Transferring focus back to the panel keeps it zoomed
5341 workspace.update(cx, |workspace, cx| {
5342 workspace.toggle_panel_focus::<TestPanel>(cx);
5343 });
5344
5345 workspace.update(cx, |workspace, cx| {
5346 assert!(workspace.right_dock().read(cx).is_open());
5347 assert!(panel.is_zoomed(cx));
5348 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5349 });
5350
5351 // Close the dock while it is zoomed
5352 workspace.update(cx, |workspace, cx| {
5353 workspace.toggle_dock(DockPosition::Right, cx)
5354 });
5355
5356 workspace.update(cx, |workspace, cx| {
5357 assert!(!workspace.right_dock().read(cx).is_open());
5358 assert!(panel.is_zoomed(cx));
5359 assert!(workspace.zoomed.is_none());
5360 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5361 });
5362
5363 // Opening the dock, when it's zoomed, retains focus
5364 workspace.update(cx, |workspace, cx| {
5365 workspace.toggle_dock(DockPosition::Right, cx)
5366 });
5367
5368 workspace.update(cx, |workspace, cx| {
5369 assert!(workspace.right_dock().read(cx).is_open());
5370 assert!(panel.is_zoomed(cx));
5371 assert!(workspace.zoomed.is_some());
5372 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5373 });
5374
5375 // Unzoom and close the panel, zoom the active pane.
5376 panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
5377 workspace.update(cx, |workspace, cx| {
5378 workspace.toggle_dock(DockPosition::Right, cx)
5379 });
5380 pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
5381
5382 // Opening a dock unzooms the pane.
5383 workspace.update(cx, |workspace, cx| {
5384 workspace.toggle_dock(DockPosition::Right, cx)
5385 });
5386 workspace.update(cx, |workspace, cx| {
5387 let pane = pane.read(cx);
5388 assert!(!pane.is_zoomed());
5389 assert!(!pane.focus_handle(cx).is_focused(cx));
5390 assert!(workspace.right_dock().read(cx).is_open());
5391 assert!(workspace.zoomed.is_none());
5392 });
5393 }
5394
5395 struct TestModal(FocusHandle);
5396
5397 impl TestModal {
5398 fn new(cx: &mut ViewContext<Self>) -> Self {
5399 Self(cx.focus_handle())
5400 }
5401 }
5402
5403 impl EventEmitter<DismissEvent> for TestModal {}
5404
5405 impl FocusableView for TestModal {
5406 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
5407 self.0.clone()
5408 }
5409 }
5410
5411 impl ModalView for TestModal {}
5412
5413 impl Render for TestModal {
5414 fn render(&mut self, _cx: &mut ViewContext<TestModal>) -> impl IntoElement {
5415 div().track_focus(&self.0)
5416 }
5417 }
5418
5419 #[gpui::test]
5420 async fn test_panels(cx: &mut gpui::TestAppContext) {
5421 init_test(cx);
5422 let fs = FakeFs::new(cx.executor());
5423
5424 let project = Project::test(fs, [], cx).await;
5425 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5426
5427 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
5428 let panel_1 = cx.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
5429 workspace.add_panel(panel_1.clone(), cx);
5430 workspace
5431 .left_dock()
5432 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
5433 let panel_2 = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
5434 workspace.add_panel(panel_2.clone(), cx);
5435 workspace
5436 .right_dock()
5437 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5438
5439 let left_dock = workspace.left_dock();
5440 assert_eq!(
5441 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5442 panel_1.panel_id()
5443 );
5444 assert_eq!(
5445 left_dock.read(cx).active_panel_size(cx).unwrap(),
5446 panel_1.size(cx)
5447 );
5448
5449 left_dock.update(cx, |left_dock, cx| {
5450 left_dock.resize_active_panel(Some(px(1337.)), cx)
5451 });
5452 assert_eq!(
5453 workspace
5454 .right_dock()
5455 .read(cx)
5456 .visible_panel()
5457 .unwrap()
5458 .panel_id(),
5459 panel_2.panel_id(),
5460 );
5461
5462 (panel_1, panel_2)
5463 });
5464
5465 // Move panel_1 to the right
5466 panel_1.update(cx, |panel_1, cx| {
5467 panel_1.set_position(DockPosition::Right, cx)
5468 });
5469
5470 workspace.update(cx, |workspace, cx| {
5471 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
5472 // Since it was the only panel on the left, the left dock should now be closed.
5473 assert!(!workspace.left_dock().read(cx).is_open());
5474 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
5475 let right_dock = workspace.right_dock();
5476 assert_eq!(
5477 right_dock.read(cx).visible_panel().unwrap().panel_id(),
5478 panel_1.panel_id()
5479 );
5480 assert_eq!(
5481 right_dock.read(cx).active_panel_size(cx).unwrap(),
5482 px(1337.)
5483 );
5484
5485 // Now we move panel_2 to the left
5486 panel_2.set_position(DockPosition::Left, cx);
5487 });
5488
5489 workspace.update(cx, |workspace, cx| {
5490 // Since panel_2 was not visible on the right, we don't open the left dock.
5491 assert!(!workspace.left_dock().read(cx).is_open());
5492 // And the right dock is unaffected in its displaying of panel_1
5493 assert!(workspace.right_dock().read(cx).is_open());
5494 assert_eq!(
5495 workspace
5496 .right_dock()
5497 .read(cx)
5498 .visible_panel()
5499 .unwrap()
5500 .panel_id(),
5501 panel_1.panel_id(),
5502 );
5503 });
5504
5505 // Move panel_1 back to the left
5506 panel_1.update(cx, |panel_1, cx| {
5507 panel_1.set_position(DockPosition::Left, cx)
5508 });
5509
5510 workspace.update(cx, |workspace, cx| {
5511 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
5512 let left_dock = workspace.left_dock();
5513 assert!(left_dock.read(cx).is_open());
5514 assert_eq!(
5515 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5516 panel_1.panel_id()
5517 );
5518 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), px(1337.));
5519 // And the right dock should be closed as it no longer has any panels.
5520 assert!(!workspace.right_dock().read(cx).is_open());
5521
5522 // Now we move panel_1 to the bottom
5523 panel_1.set_position(DockPosition::Bottom, cx);
5524 });
5525
5526 workspace.update(cx, |workspace, cx| {
5527 // Since panel_1 was visible on the left, we close the left dock.
5528 assert!(!workspace.left_dock().read(cx).is_open());
5529 // The bottom dock is sized based on the panel's default size,
5530 // since the panel orientation changed from vertical to horizontal.
5531 let bottom_dock = workspace.bottom_dock();
5532 assert_eq!(
5533 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
5534 panel_1.size(cx),
5535 );
5536 // Close bottom dock and move panel_1 back to the left.
5537 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
5538 panel_1.set_position(DockPosition::Left, cx);
5539 });
5540
5541 // Emit activated event on panel 1
5542 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
5543
5544 // Now the left dock is open and panel_1 is active and focused.
5545 workspace.update(cx, |workspace, cx| {
5546 let left_dock = workspace.left_dock();
5547 assert!(left_dock.read(cx).is_open());
5548 assert_eq!(
5549 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5550 panel_1.panel_id(),
5551 );
5552 assert!(panel_1.focus_handle(cx).is_focused(cx));
5553 });
5554
5555 // Emit closed event on panel 2, which is not active
5556 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
5557
5558 // Wo don't close the left dock, because panel_2 wasn't the active panel
5559 workspace.update(cx, |workspace, cx| {
5560 let left_dock = workspace.left_dock();
5561 assert!(left_dock.read(cx).is_open());
5562 assert_eq!(
5563 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5564 panel_1.panel_id(),
5565 );
5566 });
5567
5568 // Emitting a ZoomIn event shows the panel as zoomed.
5569 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
5570 workspace.update(cx, |workspace, _| {
5571 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5572 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
5573 });
5574
5575 // Move panel to another dock while it is zoomed
5576 panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
5577 workspace.update(cx, |workspace, _| {
5578 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5579
5580 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5581 });
5582
5583 // This is a helper for getting a:
5584 // - valid focus on an element,
5585 // - that isn't a part of the panes and panels system of the Workspace,
5586 // - and doesn't trigger the 'on_focus_lost' API.
5587 let focus_other_view = {
5588 let workspace = workspace.clone();
5589 move |cx: &mut VisualTestContext| {
5590 workspace.update(cx, |workspace, cx| {
5591 if let Some(_) = workspace.active_modal::<TestModal>(cx) {
5592 workspace.toggle_modal(cx, TestModal::new);
5593 workspace.toggle_modal(cx, TestModal::new);
5594 } else {
5595 workspace.toggle_modal(cx, TestModal::new);
5596 }
5597 })
5598 }
5599 };
5600
5601 // If focus is transferred to another view that's not a panel or another pane, we still show
5602 // the panel as zoomed.
5603 focus_other_view(cx);
5604 workspace.update(cx, |workspace, _| {
5605 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5606 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5607 });
5608
5609 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
5610 workspace.update(cx, |_, cx| cx.focus_self());
5611 workspace.update(cx, |workspace, _| {
5612 assert_eq!(workspace.zoomed, None);
5613 assert_eq!(workspace.zoomed_position, None);
5614 });
5615
5616 // If focus is transferred again to another view that's not a panel or a pane, we won't
5617 // show the panel as zoomed because it wasn't zoomed before.
5618 focus_other_view(cx);
5619 workspace.update(cx, |workspace, _| {
5620 assert_eq!(workspace.zoomed, None);
5621 assert_eq!(workspace.zoomed_position, None);
5622 });
5623
5624 // When the panel is activated, it is zoomed again.
5625 cx.dispatch_action(ToggleRightDock);
5626 workspace.update(cx, |workspace, _| {
5627 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5628 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5629 });
5630
5631 // Emitting a ZoomOut event unzooms the panel.
5632 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
5633 workspace.update(cx, |workspace, _| {
5634 assert_eq!(workspace.zoomed, None);
5635 assert_eq!(workspace.zoomed_position, None);
5636 });
5637
5638 // Emit closed event on panel 1, which is active
5639 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
5640
5641 // Now the left dock is closed, because panel_1 was the active panel
5642 workspace.update(cx, |workspace, cx| {
5643 let right_dock = workspace.right_dock();
5644 assert!(!right_dock.read(cx).is_open());
5645 });
5646 }
5647
5648 pub fn init_test(cx: &mut TestAppContext) {
5649 cx.update(|cx| {
5650 let settings_store = SettingsStore::test(cx);
5651 cx.set_global(settings_store);
5652 theme::init(theme::LoadThemes::JustBase, cx);
5653 language::init(cx);
5654 crate::init_settings(cx);
5655 Project::init_settings(cx);
5656 });
5657 }
5658}