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;
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 .map(|panel| 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 .map(|panel| 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 .map(|panel| 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(|(ix, (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 ix,
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 (ix, path_open_result) in tasks.await.into_iter().flatten() {
3743 opened_items[ix] = Some(path_open_result);
3744 }
3745
3746 Ok(opened_items)
3747 })
3748}
3749
3750enum ActivateInDirectionTarget {
3751 Pane(View<Pane>),
3752 Dock(View<Dock>),
3753}
3754
3755fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncAppContext) {
3756 const REPORT_ISSUE_URL: &str = "https://github.com/zed-industries/zed/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
3757
3758 workspace
3759 .update(cx, |workspace, cx| {
3760 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
3761 workspace.show_notification_once(0, cx, |cx| {
3762 cx.new_view(|_| {
3763 MessageNotification::new("Failed to load the database file.")
3764 .with_click_message("Click to let us know about this error")
3765 .on_click(|cx| cx.open_url(REPORT_ISSUE_URL))
3766 })
3767 });
3768 }
3769 })
3770 .log_err();
3771}
3772
3773impl FocusableView for Workspace {
3774 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
3775 self.active_pane.focus_handle(cx)
3776 }
3777}
3778
3779#[derive(Clone, Render)]
3780struct DraggedDock(DockPosition);
3781
3782impl Render for Workspace {
3783 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3784 let mut context = KeyContext::default();
3785 context.add("Workspace");
3786
3787 let (ui_font, ui_font_size) = {
3788 let theme_settings = ThemeSettings::get_global(cx);
3789 (
3790 theme_settings.ui_font.family.clone(),
3791 theme_settings.ui_font_size,
3792 )
3793 };
3794
3795 let theme = cx.theme().clone();
3796 let colors = theme.colors();
3797 cx.set_rem_size(ui_font_size);
3798
3799 self.actions(div(), cx)
3800 .key_context(context)
3801 .relative()
3802 .size_full()
3803 .flex()
3804 .flex_col()
3805 .font(ui_font)
3806 .gap_0()
3807 .justify_start()
3808 .items_start()
3809 .text_color(colors.text)
3810 .bg(colors.background)
3811 .border()
3812 .border_color(colors.border)
3813 .children(self.titlebar_item.clone())
3814 .child(
3815 div()
3816 .id("workspace")
3817 .relative()
3818 .flex_1()
3819 .w_full()
3820 .flex()
3821 .flex_col()
3822 .overflow_hidden()
3823 .border_t()
3824 .border_b()
3825 .border_color(colors.border)
3826 .child(
3827 canvas({
3828 let this = cx.view().clone();
3829 move |bounds, cx| {
3830 this.update(cx, |this, _cx| {
3831 this.bounds = *bounds;
3832 })
3833 }
3834 })
3835 .absolute()
3836 .size_full(),
3837 )
3838 .on_drag_move(
3839 cx.listener(|workspace, e: &DragMoveEvent<DraggedDock>, cx| {
3840 match e.drag(cx).0 {
3841 DockPosition::Left => {
3842 let size = workspace.bounds.left() + e.event.position.x;
3843 workspace.left_dock.update(cx, |left_dock, cx| {
3844 left_dock.resize_active_panel(Some(size), cx);
3845 });
3846 }
3847 DockPosition::Right => {
3848 let size = workspace.bounds.right() - e.event.position.x;
3849 workspace.right_dock.update(cx, |right_dock, cx| {
3850 right_dock.resize_active_panel(Some(size), cx);
3851 });
3852 }
3853 DockPosition::Bottom => {
3854 let size = workspace.bounds.bottom() - e.event.position.y;
3855 workspace.bottom_dock.update(cx, |bottom_dock, cx| {
3856 bottom_dock.resize_active_panel(Some(size), cx);
3857 });
3858 }
3859 }
3860 }),
3861 )
3862 .child(self.modal_layer.clone())
3863 .child(
3864 div()
3865 .flex()
3866 .flex_row()
3867 .h_full()
3868 // Left Dock
3869 .children(self.zoomed_position.ne(&Some(DockPosition::Left)).then(
3870 || {
3871 div()
3872 .flex()
3873 .flex_none()
3874 .overflow_hidden()
3875 .child(self.left_dock.clone())
3876 },
3877 ))
3878 // Panes
3879 .child(
3880 div()
3881 .flex()
3882 .flex_col()
3883 .flex_1()
3884 .overflow_hidden()
3885 .child(self.center.render(
3886 &self.project,
3887 &self.follower_states,
3888 self.active_call(),
3889 &self.active_pane,
3890 self.zoomed.as_ref(),
3891 &self.app_state,
3892 cx,
3893 ))
3894 .children(
3895 self.zoomed_position
3896 .ne(&Some(DockPosition::Bottom))
3897 .then(|| self.bottom_dock.clone()),
3898 ),
3899 )
3900 // Right Dock
3901 .children(self.zoomed_position.ne(&Some(DockPosition::Right)).then(
3902 || {
3903 div()
3904 .flex()
3905 .flex_none()
3906 .overflow_hidden()
3907 .child(self.right_dock.clone())
3908 },
3909 )),
3910 )
3911 .children(self.render_notifications(cx))
3912 .children(self.zoomed.as_ref().and_then(|view| {
3913 let zoomed_view = view.upgrade()?;
3914 let div = div()
3915 .z_index(1)
3916 .absolute()
3917 .overflow_hidden()
3918 .border_color(colors.border)
3919 .bg(colors.background)
3920 .child(zoomed_view)
3921 .inset_0()
3922 .shadow_lg();
3923
3924 Some(match self.zoomed_position {
3925 Some(DockPosition::Left) => div.right_2().border_r(),
3926 Some(DockPosition::Right) => div.left_2().border_l(),
3927 Some(DockPosition::Bottom) => div.top_2().border_t(),
3928 None => div.top_2().bottom_2().left_2().right_2().border(),
3929 })
3930 })),
3931 )
3932 .child(self.status_bar.clone())
3933 .children(if self.project.read(cx).is_disconnected() {
3934 Some(DisconnectedOverlay)
3935 } else {
3936 None
3937 })
3938 }
3939}
3940
3941impl WorkspaceStore {
3942 pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
3943 Self {
3944 workspaces: Default::default(),
3945 _subscriptions: vec![
3946 client.add_request_handler(cx.weak_model(), Self::handle_follow),
3947 client.add_message_handler(cx.weak_model(), Self::handle_update_followers),
3948 ],
3949 client,
3950 }
3951 }
3952
3953 pub fn update_followers(
3954 &self,
3955 project_id: Option<u64>,
3956 update: proto::update_followers::Variant,
3957 cx: &AppContext,
3958 ) -> Option<()> {
3959 let active_call = ActiveCall::try_global(cx)?;
3960 let room_id = active_call.read(cx).room()?.read(cx).id();
3961 self.client
3962 .send(proto::UpdateFollowers {
3963 room_id,
3964 project_id,
3965 variant: Some(update),
3966 })
3967 .log_err()
3968 }
3969
3970 pub async fn handle_follow(
3971 this: Model<Self>,
3972 envelope: TypedEnvelope<proto::Follow>,
3973 _: Arc<Client>,
3974 mut cx: AsyncAppContext,
3975 ) -> Result<proto::FollowResponse> {
3976 this.update(&mut cx, |this, cx| {
3977 let follower = Follower {
3978 project_id: envelope.payload.project_id,
3979 peer_id: envelope.original_sender_id()?,
3980 };
3981
3982 let mut response = proto::FollowResponse::default();
3983 this.workspaces.retain(|workspace| {
3984 workspace
3985 .update(cx, |workspace, cx| {
3986 let handler_response = workspace.handle_follow(follower.project_id, cx);
3987 if response.views.is_empty() {
3988 response.views = handler_response.views;
3989 } else {
3990 response.views.extend_from_slice(&handler_response.views);
3991 }
3992
3993 if let Some(active_view_id) = handler_response.active_view_id.clone() {
3994 if response.active_view_id.is_none()
3995 || workspace.project.read(cx).remote_id() == follower.project_id
3996 {
3997 response.active_view_id = Some(active_view_id);
3998 }
3999 }
4000
4001 if let Some(active_view) = handler_response.active_view.clone() {
4002 if response.active_view_id.is_none()
4003 || workspace.project.read(cx).remote_id() == follower.project_id
4004 {
4005 response.active_view = Some(active_view)
4006 }
4007 }
4008 })
4009 .is_ok()
4010 });
4011
4012 Ok(response)
4013 })?
4014 }
4015
4016 async fn handle_update_followers(
4017 this: Model<Self>,
4018 envelope: TypedEnvelope<proto::UpdateFollowers>,
4019 _: Arc<Client>,
4020 mut cx: AsyncAppContext,
4021 ) -> Result<()> {
4022 let leader_id = envelope.original_sender_id()?;
4023 let update = envelope.payload;
4024
4025 this.update(&mut cx, |this, cx| {
4026 this.workspaces.retain(|workspace| {
4027 workspace
4028 .update(cx, |workspace, cx| {
4029 let project_id = workspace.project.read(cx).remote_id();
4030 if update.project_id != project_id && update.project_id.is_some() {
4031 return;
4032 }
4033 workspace.handle_update_followers(leader_id, update.clone(), cx);
4034 })
4035 .is_ok()
4036 });
4037 Ok(())
4038 })?
4039 }
4040}
4041
4042impl ViewId {
4043 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
4044 Ok(Self {
4045 creator: message
4046 .creator
4047 .ok_or_else(|| anyhow!("creator is missing"))?,
4048 id: message.id,
4049 })
4050 }
4051
4052 pub(crate) fn to_proto(&self) -> proto::ViewId {
4053 proto::ViewId {
4054 creator: Some(self.creator),
4055 id: self.id,
4056 }
4057 }
4058}
4059
4060pub trait WorkspaceHandle {
4061 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
4062}
4063
4064impl WorkspaceHandle for View<Workspace> {
4065 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
4066 self.read(cx)
4067 .worktrees(cx)
4068 .flat_map(|worktree| {
4069 let worktree_id = worktree.read(cx).id();
4070 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
4071 worktree_id,
4072 path: f.path.clone(),
4073 })
4074 })
4075 .collect::<Vec<_>>()
4076 }
4077}
4078
4079impl std::fmt::Debug for OpenPaths {
4080 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4081 f.debug_struct("OpenPaths")
4082 .field("paths", &self.paths)
4083 .finish()
4084 }
4085}
4086
4087pub fn activate_workspace_for_project(
4088 cx: &mut AppContext,
4089 predicate: impl Fn(&Project, &AppContext) -> bool + Send + 'static,
4090) -> Option<WindowHandle<Workspace>> {
4091 for window in cx.windows() {
4092 let Some(workspace) = window.downcast::<Workspace>() else {
4093 continue;
4094 };
4095
4096 let predicate = workspace
4097 .update(cx, |workspace, cx| {
4098 let project = workspace.project.read(cx);
4099 if predicate(project, cx) {
4100 cx.activate_window();
4101 true
4102 } else {
4103 false
4104 }
4105 })
4106 .log_err()
4107 .unwrap_or(false);
4108
4109 if predicate {
4110 return Some(workspace);
4111 }
4112 }
4113
4114 None
4115}
4116
4117pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
4118 DB.last_workspace().await.log_err().flatten()
4119}
4120
4121actions!(collab, [OpenChannelNotes]);
4122
4123async fn join_channel_internal(
4124 channel_id: ChannelId,
4125 app_state: &Arc<AppState>,
4126 requesting_window: Option<WindowHandle<Workspace>>,
4127 active_call: &Model<ActiveCall>,
4128 cx: &mut AsyncAppContext,
4129) -> Result<bool> {
4130 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
4131 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
4132 return (false, None);
4133 };
4134
4135 let already_in_channel = room.channel_id() == Some(channel_id);
4136 let should_prompt = room.is_sharing_project()
4137 && room.remote_participants().len() > 0
4138 && !already_in_channel;
4139 let open_room = if already_in_channel {
4140 active_call.room().cloned()
4141 } else {
4142 None
4143 };
4144 (should_prompt, open_room)
4145 })?;
4146
4147 if let Some(room) = open_room {
4148 let task = room.update(cx, |room, cx| {
4149 if let Some((project, host)) = room.most_active_project(cx) {
4150 return Some(join_remote_project(project, host, app_state.clone(), cx));
4151 }
4152
4153 None
4154 })?;
4155 if let Some(task) = task {
4156 task.await?;
4157 }
4158 return anyhow::Ok(true);
4159 }
4160
4161 if should_prompt {
4162 if let Some(workspace) = requesting_window {
4163 let answer = workspace
4164 .update(cx, |_, cx| {
4165 cx.prompt(
4166 PromptLevel::Warning,
4167 "Do you want to switch channels?",
4168 Some("Leaving this call will unshare your current project."),
4169 &["Yes, Join Channel", "Cancel"],
4170 )
4171 })?
4172 .await;
4173
4174 if answer == Ok(1) {
4175 return Ok(false);
4176 }
4177 } else {
4178 return Ok(false); // unreachable!() hopefully
4179 }
4180 }
4181
4182 let client = cx.update(|cx| active_call.read(cx).client())?;
4183
4184 let mut client_status = client.status();
4185
4186 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
4187 'outer: loop {
4188 let Some(status) = client_status.recv().await else {
4189 return Err(anyhow!("error connecting"));
4190 };
4191
4192 match status {
4193 Status::Connecting
4194 | Status::Authenticating
4195 | Status::Reconnecting
4196 | Status::Reauthenticating => continue,
4197 Status::Connected { .. } => break 'outer,
4198 Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
4199 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
4200 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
4201 return Err(ErrorCode::Disconnected.into());
4202 }
4203 }
4204 }
4205
4206 let room = active_call
4207 .update(cx, |active_call, cx| {
4208 active_call.join_channel(channel_id, cx)
4209 })?
4210 .await?;
4211
4212 let Some(room) = room else {
4213 return anyhow::Ok(true);
4214 };
4215
4216 room.update(cx, |room, _| room.room_update_completed())?
4217 .await;
4218
4219 let task = room.update(cx, |room, cx| {
4220 if let Some((project, host)) = room.most_active_project(cx) {
4221 return Some(join_remote_project(project, host, app_state.clone(), cx));
4222 }
4223
4224 // if you are the first to join a channel, share your project
4225 if room.remote_participants().len() == 0 && !room.local_participant_is_guest() {
4226 if let Some(workspace) = requesting_window {
4227 let project = workspace.update(cx, |workspace, cx| {
4228 if !CallSettings::get_global(cx).share_on_join {
4229 return None;
4230 }
4231 let project = workspace.project.read(cx);
4232 if project.is_local()
4233 && project.visible_worktrees(cx).any(|tree| {
4234 tree.read(cx)
4235 .root_entry()
4236 .map_or(false, |entry| entry.is_dir())
4237 })
4238 {
4239 Some(workspace.project.clone())
4240 } else {
4241 None
4242 }
4243 });
4244 if let Ok(Some(project)) = project {
4245 return Some(cx.spawn(|room, mut cx| async move {
4246 room.update(&mut cx, |room, cx| room.share_project(project, cx))?
4247 .await?;
4248 Ok(())
4249 }));
4250 }
4251 }
4252 }
4253
4254 None
4255 })?;
4256 if let Some(task) = task {
4257 task.await?;
4258 return anyhow::Ok(true);
4259 }
4260 anyhow::Ok(false)
4261}
4262
4263pub fn join_channel(
4264 channel_id: ChannelId,
4265 app_state: Arc<AppState>,
4266 requesting_window: Option<WindowHandle<Workspace>>,
4267 cx: &mut AppContext,
4268) -> Task<Result<()>> {
4269 let active_call = ActiveCall::global(cx);
4270 cx.spawn(|mut cx| async move {
4271 let result = join_channel_internal(
4272 channel_id,
4273 &app_state,
4274 requesting_window,
4275 &active_call,
4276 &mut cx,
4277 )
4278 .await;
4279
4280 // join channel succeeded, and opened a window
4281 if matches!(result, Ok(true)) {
4282 return anyhow::Ok(());
4283 }
4284
4285 // find an existing workspace to focus and show call controls
4286 let mut active_window =
4287 requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
4288 if active_window.is_none() {
4289 // no open workspaces, make one to show the error in (blergh)
4290 let (window_handle, _) = cx
4291 .update(|cx| {
4292 Workspace::new_local(vec![], app_state.clone(), requesting_window, cx)
4293 })?
4294 .await?;
4295
4296 if result.is_ok() {
4297 cx.update(|cx| {
4298 cx.dispatch_action(&OpenChannelNotes);
4299 }).log_err();
4300 }
4301
4302 active_window = Some(window_handle);
4303 }
4304
4305 if let Err(err) = result {
4306 log::error!("failed to join channel: {}", err);
4307 if let Some(active_window) = active_window {
4308 active_window
4309 .update(&mut cx, |_, cx| {
4310 let detail: SharedString = match err.error_code() {
4311 ErrorCode::SignedOut => {
4312 "Please sign in to continue.".into()
4313 }
4314 ErrorCode::UpgradeRequired => {
4315 "Your are running an unsupported version of Zed. Please update to continue.".into()
4316 }
4317 ErrorCode::NoSuchChannel => {
4318 "No matching channel was found. Please check the link and try again.".into()
4319 }
4320 ErrorCode::Forbidden => {
4321 "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
4322 }
4323 ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
4324 _ => format!("{}\n\nPlease try again.", err).into(),
4325 };
4326 cx.prompt(
4327 PromptLevel::Critical,
4328 "Failed to join channel",
4329 Some(&detail),
4330 &["Ok"],
4331 )
4332 })?
4333 .await
4334 .ok();
4335 }
4336 }
4337
4338 // return ok, we showed the error to the user.
4339 return anyhow::Ok(());
4340 })
4341}
4342
4343pub async fn get_any_active_workspace(
4344 app_state: Arc<AppState>,
4345 mut cx: AsyncAppContext,
4346) -> anyhow::Result<WindowHandle<Workspace>> {
4347 // find an existing workspace to focus and show call controls
4348 let active_window = activate_any_workspace_window(&mut cx);
4349 if active_window.is_none() {
4350 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, cx))?
4351 .await?;
4352 }
4353 activate_any_workspace_window(&mut cx).context("could not open zed")
4354}
4355
4356fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandle<Workspace>> {
4357 cx.update(|cx| {
4358 for window in cx.windows() {
4359 if let Some(workspace_window) = window.downcast::<Workspace>() {
4360 workspace_window
4361 .update(cx, |_, cx| cx.activate_window())
4362 .ok();
4363 return Some(workspace_window);
4364 }
4365 }
4366 None
4367 })
4368 .ok()
4369 .flatten()
4370}
4371
4372#[allow(clippy::type_complexity)]
4373pub fn open_paths(
4374 abs_paths: &[PathBuf],
4375 app_state: &Arc<AppState>,
4376 requesting_window: Option<WindowHandle<Workspace>>,
4377 cx: &mut AppContext,
4378) -> Task<
4379 anyhow::Result<(
4380 WindowHandle<Workspace>,
4381 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
4382 )>,
4383> {
4384 let app_state = app_state.clone();
4385 let abs_paths = abs_paths.to_vec();
4386 // Open paths in existing workspace if possible
4387 let existing = activate_workspace_for_project(cx, {
4388 let abs_paths = abs_paths.clone();
4389 move |project, cx| project.contains_paths(&abs_paths, cx)
4390 });
4391 cx.spawn(move |mut cx| async move {
4392 if let Some(existing) = existing {
4393 Ok((
4394 existing,
4395 existing
4396 .update(&mut cx, |workspace, cx| {
4397 workspace.open_paths(abs_paths, OpenVisible::All, None, cx)
4398 })?
4399 .await,
4400 ))
4401 } else {
4402 cx.update(move |cx| {
4403 Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx)
4404 })?
4405 .await
4406 }
4407 })
4408}
4409
4410pub fn open_new(
4411 app_state: &Arc<AppState>,
4412 cx: &mut AppContext,
4413 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static + Send,
4414) -> Task<()> {
4415 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
4416 cx.spawn(|mut cx| async move {
4417 if let Some((workspace, opened_paths)) = task.await.log_err() {
4418 workspace
4419 .update(&mut cx, |workspace, cx| {
4420 if opened_paths.is_empty() {
4421 init(workspace, cx)
4422 }
4423 })
4424 .log_err();
4425 }
4426 })
4427}
4428
4429pub fn create_and_open_local_file(
4430 path: &'static Path,
4431 cx: &mut ViewContext<Workspace>,
4432 default_content: impl 'static + Send + FnOnce() -> Rope,
4433) -> Task<Result<Box<dyn ItemHandle>>> {
4434 cx.spawn(|workspace, mut cx| async move {
4435 let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
4436 if !fs.is_file(path).await {
4437 fs.create_file(path, Default::default()).await?;
4438 fs.save(path, &default_content(), Default::default())
4439 .await?;
4440 }
4441
4442 let mut items = workspace
4443 .update(&mut cx, |workspace, cx| {
4444 workspace.with_local_workspace(cx, |workspace, cx| {
4445 workspace.open_paths(vec![path.to_path_buf()], OpenVisible::None, None, cx)
4446 })
4447 })?
4448 .await?
4449 .await;
4450
4451 let item = items.pop().flatten();
4452 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
4453 })
4454}
4455
4456pub fn join_remote_project(
4457 project_id: u64,
4458 follow_user_id: u64,
4459 app_state: Arc<AppState>,
4460 cx: &mut AppContext,
4461) -> Task<Result<()>> {
4462 let windows = cx.windows();
4463 cx.spawn(|mut cx| async move {
4464 let existing_workspace = windows.into_iter().find_map(|window| {
4465 window.downcast::<Workspace>().and_then(|window| {
4466 window
4467 .update(&mut cx, |workspace, cx| {
4468 if workspace.project().read(cx).remote_id() == Some(project_id) {
4469 Some(window)
4470 } else {
4471 None
4472 }
4473 })
4474 .unwrap_or(None)
4475 })
4476 });
4477
4478 let workspace = if let Some(existing_workspace) = existing_workspace {
4479 existing_workspace
4480 } else {
4481 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
4482 let room = active_call
4483 .read_with(&cx, |call, _| call.room().cloned())?
4484 .ok_or_else(|| anyhow!("not in a call"))?;
4485 let project = room
4486 .update(&mut cx, |room, cx| {
4487 room.join_project(
4488 project_id,
4489 app_state.languages.clone(),
4490 app_state.fs.clone(),
4491 cx,
4492 )
4493 })?
4494 .await?;
4495
4496 let window_bounds_override = window_bounds_env_override(&cx);
4497 cx.update(|cx| {
4498 let options = (app_state.build_window_options)(window_bounds_override, None, cx);
4499 cx.open_window(options, |cx| {
4500 cx.new_view(|cx| Workspace::new(0, project, app_state.clone(), cx))
4501 })
4502 })?
4503 };
4504
4505 workspace.update(&mut cx, |workspace, cx| {
4506 cx.activate(true);
4507 cx.activate_window();
4508
4509 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
4510 let follow_peer_id = room
4511 .read(cx)
4512 .remote_participants()
4513 .iter()
4514 .find(|(_, participant)| participant.user.id == follow_user_id)
4515 .map(|(_, p)| p.peer_id)
4516 .or_else(|| {
4517 // If we couldn't follow the given user, follow the host instead.
4518 let collaborator = workspace
4519 .project()
4520 .read(cx)
4521 .collaborators()
4522 .values()
4523 .find(|collaborator| collaborator.replica_id == 0)?;
4524 Some(collaborator.peer_id)
4525 });
4526
4527 if let Some(follow_peer_id) = follow_peer_id {
4528 workspace.follow(follow_peer_id, cx);
4529 }
4530 }
4531 })?;
4532
4533 anyhow::Ok(())
4534 })
4535}
4536
4537pub fn restart(_: &Restart, cx: &mut AppContext) {
4538 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
4539 let mut workspace_windows = cx
4540 .windows()
4541 .into_iter()
4542 .filter_map(|window| window.downcast::<Workspace>())
4543 .collect::<Vec<_>>();
4544
4545 // If multiple windows have unsaved changes, and need a save prompt,
4546 // prompt in the active window before switching to a different window.
4547 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
4548
4549 let mut prompt = None;
4550 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
4551 prompt = window
4552 .update(cx, |_, cx| {
4553 cx.prompt(
4554 PromptLevel::Info,
4555 "Are you sure you want to restart?",
4556 None,
4557 &["Restart", "Cancel"],
4558 )
4559 })
4560 .ok();
4561 }
4562
4563 cx.spawn(|mut cx| async move {
4564 if let Some(prompt) = prompt {
4565 let answer = prompt.await?;
4566 if answer != 0 {
4567 return Ok(());
4568 }
4569 }
4570
4571 // If the user cancels any save prompt, then keep the app open.
4572 for window in workspace_windows {
4573 if let Ok(should_close) = window.update(&mut cx, |workspace, cx| {
4574 workspace.prepare_to_close(true, cx)
4575 }) {
4576 if !should_close.await? {
4577 return Ok(());
4578 }
4579 }
4580 }
4581
4582 cx.update(|cx| cx.restart())
4583 })
4584 .detach_and_log_err(cx);
4585}
4586
4587fn parse_pixel_position_env_var(value: &str) -> Option<Point<GlobalPixels>> {
4588 let mut parts = value.split(',');
4589 let x: usize = parts.next()?.parse().ok()?;
4590 let y: usize = parts.next()?.parse().ok()?;
4591 Some(point((x as f64).into(), (y as f64).into()))
4592}
4593
4594fn parse_pixel_size_env_var(value: &str) -> Option<Size<GlobalPixels>> {
4595 let mut parts = value.split(',');
4596 let width: usize = parts.next()?.parse().ok()?;
4597 let height: usize = parts.next()?.parse().ok()?;
4598 Some(size((width as f64).into(), (height as f64).into()))
4599}
4600
4601pub fn titlebar_height(cx: &mut WindowContext) -> Pixels {
4602 (1.75 * cx.rem_size()).max(px(32.))
4603}
4604
4605struct DisconnectedOverlay;
4606
4607impl Element for DisconnectedOverlay {
4608 type State = AnyElement;
4609
4610 fn request_layout(
4611 &mut self,
4612 _: Option<Self::State>,
4613 cx: &mut ElementContext,
4614 ) -> (LayoutId, Self::State) {
4615 let mut background = cx.theme().colors().elevated_surface_background;
4616 background.fade_out(0.2);
4617 let mut overlay = div()
4618 .bg(background)
4619 .absolute()
4620 .left_0()
4621 .top(titlebar_height(cx))
4622 .size_full()
4623 .flex()
4624 .items_center()
4625 .justify_center()
4626 .capture_any_mouse_down(|_, cx| cx.stop_propagation())
4627 .capture_any_mouse_up(|_, cx| cx.stop_propagation())
4628 .child(Label::new(
4629 "Your connection to the remote project has been lost.",
4630 ))
4631 .into_any();
4632 (overlay.request_layout(cx), overlay)
4633 }
4634
4635 fn paint(
4636 &mut self,
4637 bounds: Bounds<Pixels>,
4638 overlay: &mut Self::State,
4639 cx: &mut ElementContext,
4640 ) {
4641 cx.with_z_index(u16::MAX, |cx| {
4642 cx.add_opaque_layer(bounds);
4643 overlay.paint(cx);
4644 })
4645 }
4646}
4647
4648impl IntoElement for DisconnectedOverlay {
4649 type Element = Self;
4650
4651 fn element_id(&self) -> Option<ui::prelude::ElementId> {
4652 None
4653 }
4654
4655 fn into_element(self) -> Self::Element {
4656 self
4657 }
4658}
4659
4660#[cfg(test)]
4661mod tests {
4662 use std::{cell::RefCell, rc::Rc};
4663
4664 use super::*;
4665 use crate::{
4666 dock::{test::TestPanel, PanelEvent},
4667 item::{
4668 test::{TestItem, TestProjectItem},
4669 ItemEvent,
4670 },
4671 };
4672 use fs::FakeFs;
4673 use gpui::{px, DismissEvent, TestAppContext, VisualTestContext};
4674 use project::{Project, ProjectEntryId};
4675 use serde_json::json;
4676 use settings::SettingsStore;
4677
4678 #[gpui::test]
4679 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
4680 init_test(cx);
4681
4682 let fs = FakeFs::new(cx.executor());
4683 let project = Project::test(fs, [], cx).await;
4684 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4685
4686 // Adding an item with no ambiguity renders the tab without detail.
4687 let item1 = cx.new_view(|cx| {
4688 let mut item = TestItem::new(cx);
4689 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
4690 item
4691 });
4692 workspace.update(cx, |workspace, cx| {
4693 workspace.add_item_to_active_pane(Box::new(item1.clone()), cx);
4694 });
4695 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
4696
4697 // Adding an item that creates ambiguity increases the level of detail on
4698 // both tabs.
4699 let item2 = cx.new_view(|cx| {
4700 let mut item = TestItem::new(cx);
4701 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4702 item
4703 });
4704 workspace.update(cx, |workspace, cx| {
4705 workspace.add_item_to_active_pane(Box::new(item2.clone()), cx);
4706 });
4707 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4708 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4709
4710 // Adding an item that creates ambiguity increases the level of detail only
4711 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
4712 // we stop at the highest detail available.
4713 let item3 = cx.new_view(|cx| {
4714 let mut item = TestItem::new(cx);
4715 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4716 item
4717 });
4718 workspace.update(cx, |workspace, cx| {
4719 workspace.add_item_to_active_pane(Box::new(item3.clone()), cx);
4720 });
4721 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4722 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4723 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4724 }
4725
4726 #[gpui::test]
4727 async fn test_tracking_active_path(cx: &mut TestAppContext) {
4728 init_test(cx);
4729
4730 let fs = FakeFs::new(cx.executor());
4731 fs.insert_tree(
4732 "/root1",
4733 json!({
4734 "one.txt": "",
4735 "two.txt": "",
4736 }),
4737 )
4738 .await;
4739 fs.insert_tree(
4740 "/root2",
4741 json!({
4742 "three.txt": "",
4743 }),
4744 )
4745 .await;
4746
4747 let project = Project::test(fs, ["root1".as_ref()], cx).await;
4748 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4749 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4750 let worktree_id = project.update(cx, |project, cx| {
4751 project.worktrees().next().unwrap().read(cx).id()
4752 });
4753
4754 let item1 = cx.new_view(|cx| {
4755 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
4756 });
4757 let item2 = cx.new_view(|cx| {
4758 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
4759 });
4760
4761 // Add an item to an empty pane
4762 workspace.update(cx, |workspace, cx| {
4763 workspace.add_item_to_active_pane(Box::new(item1), cx)
4764 });
4765 project.update(cx, |project, cx| {
4766 assert_eq!(
4767 project.active_entry(),
4768 project
4769 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4770 .map(|e| e.id)
4771 );
4772 });
4773 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
4774
4775 // Add a second item to a non-empty pane
4776 workspace.update(cx, |workspace, cx| {
4777 workspace.add_item_to_active_pane(Box::new(item2), cx)
4778 });
4779 assert_eq!(cx.window_title().as_deref(), Some("two.txt — root1"));
4780 project.update(cx, |project, cx| {
4781 assert_eq!(
4782 project.active_entry(),
4783 project
4784 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
4785 .map(|e| e.id)
4786 );
4787 });
4788
4789 // Close the active item
4790 pane.update(cx, |pane, cx| {
4791 pane.close_active_item(&Default::default(), cx).unwrap()
4792 })
4793 .await
4794 .unwrap();
4795 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
4796 project.update(cx, |project, cx| {
4797 assert_eq!(
4798 project.active_entry(),
4799 project
4800 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4801 .map(|e| e.id)
4802 );
4803 });
4804
4805 // Add a project folder
4806 project
4807 .update(cx, |project, cx| {
4808 project.find_or_create_local_worktree("/root2", true, cx)
4809 })
4810 .await
4811 .unwrap();
4812 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1, root2"));
4813
4814 // Remove a project folder
4815 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
4816 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root2"));
4817 }
4818
4819 #[gpui::test]
4820 async fn test_close_window(cx: &mut TestAppContext) {
4821 init_test(cx);
4822
4823 let fs = FakeFs::new(cx.executor());
4824 fs.insert_tree("/root", json!({ "one": "" })).await;
4825
4826 let project = Project::test(fs, ["root".as_ref()], cx).await;
4827 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4828
4829 // When there are no dirty items, there's nothing to do.
4830 let item1 = cx.new_view(|cx| TestItem::new(cx));
4831 workspace.update(cx, |w, cx| {
4832 w.add_item_to_active_pane(Box::new(item1.clone()), cx)
4833 });
4834 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4835 assert!(task.await.unwrap());
4836
4837 // When there are dirty untitled items, prompt to save each one. If the user
4838 // cancels any prompt, then abort.
4839 let item2 = cx.new_view(|cx| TestItem::new(cx).with_dirty(true));
4840 let item3 = cx.new_view(|cx| {
4841 TestItem::new(cx)
4842 .with_dirty(true)
4843 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4844 });
4845 workspace.update(cx, |w, cx| {
4846 w.add_item_to_active_pane(Box::new(item2.clone()), cx);
4847 w.add_item_to_active_pane(Box::new(item3.clone()), cx);
4848 });
4849 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4850 cx.executor().run_until_parked();
4851 cx.simulate_prompt_answer(2); // cancel save all
4852 cx.executor().run_until_parked();
4853 cx.simulate_prompt_answer(2); // cancel save all
4854 cx.executor().run_until_parked();
4855 assert!(!cx.has_pending_prompt());
4856 assert!(!task.await.unwrap());
4857 }
4858
4859 #[gpui::test]
4860 async fn test_close_pane_items(cx: &mut TestAppContext) {
4861 init_test(cx);
4862
4863 let fs = FakeFs::new(cx.executor());
4864
4865 let project = Project::test(fs, None, cx).await;
4866 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4867
4868 let item1 = cx.new_view(|cx| {
4869 TestItem::new(cx)
4870 .with_dirty(true)
4871 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4872 });
4873 let item2 = cx.new_view(|cx| {
4874 TestItem::new(cx)
4875 .with_dirty(true)
4876 .with_conflict(true)
4877 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
4878 });
4879 let item3 = cx.new_view(|cx| {
4880 TestItem::new(cx)
4881 .with_dirty(true)
4882 .with_conflict(true)
4883 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
4884 });
4885 let item4 = cx.new_view(|cx| {
4886 TestItem::new(cx)
4887 .with_dirty(true)
4888 .with_project_items(&[TestProjectItem::new_untitled(cx)])
4889 });
4890 let pane = workspace.update(cx, |workspace, cx| {
4891 workspace.add_item_to_active_pane(Box::new(item1.clone()), cx);
4892 workspace.add_item_to_active_pane(Box::new(item2.clone()), cx);
4893 workspace.add_item_to_active_pane(Box::new(item3.clone()), cx);
4894 workspace.add_item_to_active_pane(Box::new(item4.clone()), cx);
4895 workspace.active_pane().clone()
4896 });
4897
4898 let close_items = pane.update(cx, |pane, cx| {
4899 pane.activate_item(1, true, true, cx);
4900 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
4901 let item1_id = item1.item_id();
4902 let item3_id = item3.item_id();
4903 let item4_id = item4.item_id();
4904 pane.close_items(cx, SaveIntent::Close, move |id| {
4905 [item1_id, item3_id, item4_id].contains(&id)
4906 })
4907 });
4908 cx.executor().run_until_parked();
4909
4910 assert!(cx.has_pending_prompt());
4911 // Ignore "Save all" prompt
4912 cx.simulate_prompt_answer(2);
4913 cx.executor().run_until_parked();
4914 // There's a prompt to save item 1.
4915 pane.update(cx, |pane, _| {
4916 assert_eq!(pane.items_len(), 4);
4917 assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
4918 });
4919 // Confirm saving item 1.
4920 cx.simulate_prompt_answer(0);
4921 cx.executor().run_until_parked();
4922
4923 // Item 1 is saved. There's a prompt to save item 3.
4924 pane.update(cx, |pane, cx| {
4925 assert_eq!(item1.read(cx).save_count, 1);
4926 assert_eq!(item1.read(cx).save_as_count, 0);
4927 assert_eq!(item1.read(cx).reload_count, 0);
4928 assert_eq!(pane.items_len(), 3);
4929 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
4930 });
4931 assert!(cx.has_pending_prompt());
4932
4933 // Cancel saving item 3.
4934 cx.simulate_prompt_answer(1);
4935 cx.executor().run_until_parked();
4936
4937 // Item 3 is reloaded. There's a prompt to save item 4.
4938 pane.update(cx, |pane, cx| {
4939 assert_eq!(item3.read(cx).save_count, 0);
4940 assert_eq!(item3.read(cx).save_as_count, 0);
4941 assert_eq!(item3.read(cx).reload_count, 1);
4942 assert_eq!(pane.items_len(), 2);
4943 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
4944 });
4945 assert!(cx.has_pending_prompt());
4946
4947 // Confirm saving item 4.
4948 cx.simulate_prompt_answer(0);
4949 cx.executor().run_until_parked();
4950
4951 // There's a prompt for a path for item 4.
4952 cx.simulate_new_path_selection(|_| Some(Default::default()));
4953 close_items.await.unwrap();
4954
4955 // The requested items are closed.
4956 pane.update(cx, |pane, cx| {
4957 assert_eq!(item4.read(cx).save_count, 0);
4958 assert_eq!(item4.read(cx).save_as_count, 1);
4959 assert_eq!(item4.read(cx).reload_count, 0);
4960 assert_eq!(pane.items_len(), 1);
4961 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
4962 });
4963 }
4964
4965 #[gpui::test]
4966 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
4967 init_test(cx);
4968
4969 let fs = FakeFs::new(cx.executor());
4970 let project = Project::test(fs, [], cx).await;
4971 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4972
4973 // Create several workspace items with single project entries, and two
4974 // workspace items with multiple project entries.
4975 let single_entry_items = (0..=4)
4976 .map(|project_entry_id| {
4977 cx.new_view(|cx| {
4978 TestItem::new(cx)
4979 .with_dirty(true)
4980 .with_project_items(&[TestProjectItem::new(
4981 project_entry_id,
4982 &format!("{project_entry_id}.txt"),
4983 cx,
4984 )])
4985 })
4986 })
4987 .collect::<Vec<_>>();
4988 let item_2_3 = cx.new_view(|cx| {
4989 TestItem::new(cx)
4990 .with_dirty(true)
4991 .with_singleton(false)
4992 .with_project_items(&[
4993 single_entry_items[2].read(cx).project_items[0].clone(),
4994 single_entry_items[3].read(cx).project_items[0].clone(),
4995 ])
4996 });
4997 let item_3_4 = cx.new_view(|cx| {
4998 TestItem::new(cx)
4999 .with_dirty(true)
5000 .with_singleton(false)
5001 .with_project_items(&[
5002 single_entry_items[3].read(cx).project_items[0].clone(),
5003 single_entry_items[4].read(cx).project_items[0].clone(),
5004 ])
5005 });
5006
5007 // Create two panes that contain the following project entries:
5008 // left pane:
5009 // multi-entry items: (2, 3)
5010 // single-entry items: 0, 1, 2, 3, 4
5011 // right pane:
5012 // single-entry items: 1
5013 // multi-entry items: (3, 4)
5014 let left_pane = workspace.update(cx, |workspace, cx| {
5015 let left_pane = workspace.active_pane().clone();
5016 workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), cx);
5017 for item in single_entry_items {
5018 workspace.add_item_to_active_pane(Box::new(item), cx);
5019 }
5020 left_pane.update(cx, |pane, cx| {
5021 pane.activate_item(2, true, true, cx);
5022 });
5023
5024 let right_pane = workspace
5025 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
5026 .unwrap();
5027
5028 right_pane.update(cx, |pane, cx| {
5029 pane.add_item(Box::new(item_3_4.clone()), true, true, None, cx);
5030 });
5031
5032 left_pane
5033 });
5034
5035 cx.focus_view(&left_pane);
5036
5037 // When closing all of the items in the left pane, we should be prompted twice:
5038 // once for project entry 0, and once for project entry 2. Project entries 1,
5039 // 3, and 4 are all still open in the other paten. After those two
5040 // prompts, the task should complete.
5041
5042 let close = left_pane.update(cx, |pane, cx| {
5043 pane.close_all_items(&CloseAllItems::default(), cx).unwrap()
5044 });
5045 cx.executor().run_until_parked();
5046
5047 // Discard "Save all" prompt
5048 cx.simulate_prompt_answer(2);
5049
5050 cx.executor().run_until_parked();
5051 left_pane.update(cx, |pane, cx| {
5052 assert_eq!(
5053 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
5054 &[ProjectEntryId::from_proto(0)]
5055 );
5056 });
5057 cx.simulate_prompt_answer(0);
5058
5059 cx.executor().run_until_parked();
5060 left_pane.update(cx, |pane, cx| {
5061 assert_eq!(
5062 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
5063 &[ProjectEntryId::from_proto(2)]
5064 );
5065 });
5066 cx.simulate_prompt_answer(0);
5067
5068 cx.executor().run_until_parked();
5069 close.await.unwrap();
5070 left_pane.update(cx, |pane, _| {
5071 assert_eq!(pane.items_len(), 0);
5072 });
5073 }
5074
5075 #[gpui::test]
5076 async fn test_autosave(cx: &mut gpui::TestAppContext) {
5077 init_test(cx);
5078
5079 let fs = FakeFs::new(cx.executor());
5080 let project = Project::test(fs, [], cx).await;
5081 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5082 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5083
5084 let item = cx.new_view(|cx| {
5085 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5086 });
5087 let item_id = item.entity_id();
5088 workspace.update(cx, |workspace, cx| {
5089 workspace.add_item_to_active_pane(Box::new(item.clone()), cx);
5090 });
5091
5092 // Autosave on window change.
5093 item.update(cx, |item, cx| {
5094 cx.update_global(|settings: &mut SettingsStore, cx| {
5095 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5096 settings.autosave = Some(AutosaveSetting::OnWindowChange);
5097 })
5098 });
5099 item.is_dirty = true;
5100 });
5101
5102 // Deactivating the window saves the file.
5103 cx.deactivate_window();
5104 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
5105
5106 // Autosave on focus change.
5107 item.update(cx, |item, cx| {
5108 cx.focus_self();
5109 cx.update_global(|settings: &mut SettingsStore, cx| {
5110 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5111 settings.autosave = Some(AutosaveSetting::OnFocusChange);
5112 })
5113 });
5114 item.is_dirty = true;
5115 });
5116
5117 // Blurring the item saves the file.
5118 item.update(cx, |_, cx| cx.blur());
5119 cx.executor().run_until_parked();
5120 item.update(cx, |item, _| assert_eq!(item.save_count, 2));
5121
5122 // Deactivating the window still saves the file.
5123 cx.update(|cx| cx.activate_window());
5124 item.update(cx, |item, cx| {
5125 cx.focus_self();
5126 item.is_dirty = true;
5127 });
5128 cx.deactivate_window();
5129
5130 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
5131
5132 // Autosave after delay.
5133 item.update(cx, |item, cx| {
5134 cx.update_global(|settings: &mut SettingsStore, cx| {
5135 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5136 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
5137 })
5138 });
5139 item.is_dirty = true;
5140 cx.emit(ItemEvent::Edit);
5141 });
5142
5143 // Delay hasn't fully expired, so the file is still dirty and unsaved.
5144 cx.executor().advance_clock(Duration::from_millis(250));
5145 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
5146
5147 // After delay expires, the file is saved.
5148 cx.executor().advance_clock(Duration::from_millis(250));
5149 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
5150
5151 // Autosave on focus change, ensuring closing the tab counts as such.
5152 item.update(cx, |item, cx| {
5153 cx.update_global(|settings: &mut SettingsStore, cx| {
5154 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5155 settings.autosave = Some(AutosaveSetting::OnFocusChange);
5156 })
5157 });
5158 item.is_dirty = true;
5159 });
5160
5161 pane.update(cx, |pane, cx| {
5162 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
5163 })
5164 .await
5165 .unwrap();
5166 assert!(!cx.has_pending_prompt());
5167 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
5168
5169 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
5170 workspace.update(cx, |workspace, cx| {
5171 workspace.add_item_to_active_pane(Box::new(item.clone()), cx);
5172 });
5173 item.update(cx, |item, cx| {
5174 item.project_items[0].update(cx, |item, _| {
5175 item.entry_id = None;
5176 });
5177 item.is_dirty = true;
5178 cx.blur();
5179 });
5180 cx.run_until_parked();
5181 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
5182
5183 // Ensure autosave is prevented for deleted files also when closing the buffer.
5184 let _close_items = pane.update(cx, |pane, cx| {
5185 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
5186 });
5187 cx.run_until_parked();
5188 assert!(cx.has_pending_prompt());
5189 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
5190 }
5191
5192 #[gpui::test]
5193 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
5194 init_test(cx);
5195
5196 let fs = FakeFs::new(cx.executor());
5197
5198 let project = Project::test(fs, [], cx).await;
5199 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5200
5201 let item = cx.new_view(|cx| {
5202 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5203 });
5204 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5205 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
5206 let toolbar_notify_count = Rc::new(RefCell::new(0));
5207
5208 workspace.update(cx, |workspace, cx| {
5209 workspace.add_item_to_active_pane(Box::new(item.clone()), cx);
5210 let toolbar_notification_count = toolbar_notify_count.clone();
5211 cx.observe(&toolbar, move |_, _, _| {
5212 *toolbar_notification_count.borrow_mut() += 1
5213 })
5214 .detach();
5215 });
5216
5217 pane.update(cx, |pane, _| {
5218 assert!(!pane.can_navigate_backward());
5219 assert!(!pane.can_navigate_forward());
5220 });
5221
5222 item.update(cx, |item, cx| {
5223 item.set_state("one".to_string(), cx);
5224 });
5225
5226 // Toolbar must be notified to re-render the navigation buttons
5227 assert_eq!(*toolbar_notify_count.borrow(), 1);
5228
5229 pane.update(cx, |pane, _| {
5230 assert!(pane.can_navigate_backward());
5231 assert!(!pane.can_navigate_forward());
5232 });
5233
5234 workspace
5235 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
5236 .await
5237 .unwrap();
5238
5239 assert_eq!(*toolbar_notify_count.borrow(), 2);
5240 pane.update(cx, |pane, _| {
5241 assert!(!pane.can_navigate_backward());
5242 assert!(pane.can_navigate_forward());
5243 });
5244 }
5245
5246 #[gpui::test]
5247 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
5248 init_test(cx);
5249 let fs = FakeFs::new(cx.executor());
5250
5251 let project = Project::test(fs, [], cx).await;
5252 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5253
5254 let panel = workspace.update(cx, |workspace, cx| {
5255 let panel = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
5256 workspace.add_panel(panel.clone(), cx);
5257
5258 workspace
5259 .right_dock()
5260 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5261
5262 panel
5263 });
5264
5265 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5266 pane.update(cx, |pane, cx| {
5267 let item = cx.new_view(|cx| TestItem::new(cx));
5268 pane.add_item(Box::new(item), true, true, None, cx);
5269 });
5270
5271 // Transfer focus from center to panel
5272 workspace.update(cx, |workspace, cx| {
5273 workspace.toggle_panel_focus::<TestPanel>(cx);
5274 });
5275
5276 workspace.update(cx, |workspace, cx| {
5277 assert!(workspace.right_dock().read(cx).is_open());
5278 assert!(!panel.is_zoomed(cx));
5279 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5280 });
5281
5282 // Transfer focus from panel to center
5283 workspace.update(cx, |workspace, cx| {
5284 workspace.toggle_panel_focus::<TestPanel>(cx);
5285 });
5286
5287 workspace.update(cx, |workspace, cx| {
5288 assert!(workspace.right_dock().read(cx).is_open());
5289 assert!(!panel.is_zoomed(cx));
5290 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5291 });
5292
5293 // Close the dock
5294 workspace.update(cx, |workspace, cx| {
5295 workspace.toggle_dock(DockPosition::Right, cx);
5296 });
5297
5298 workspace.update(cx, |workspace, cx| {
5299 assert!(!workspace.right_dock().read(cx).is_open());
5300 assert!(!panel.is_zoomed(cx));
5301 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5302 });
5303
5304 // Open the dock
5305 workspace.update(cx, |workspace, cx| {
5306 workspace.toggle_dock(DockPosition::Right, cx);
5307 });
5308
5309 workspace.update(cx, |workspace, cx| {
5310 assert!(workspace.right_dock().read(cx).is_open());
5311 assert!(!panel.is_zoomed(cx));
5312 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5313 });
5314
5315 // Focus and zoom panel
5316 panel.update(cx, |panel, cx| {
5317 cx.focus_self();
5318 panel.set_zoomed(true, cx)
5319 });
5320
5321 workspace.update(cx, |workspace, cx| {
5322 assert!(workspace.right_dock().read(cx).is_open());
5323 assert!(panel.is_zoomed(cx));
5324 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5325 });
5326
5327 // Transfer focus to the center closes the dock
5328 workspace.update(cx, |workspace, cx| {
5329 workspace.toggle_panel_focus::<TestPanel>(cx);
5330 });
5331
5332 workspace.update(cx, |workspace, cx| {
5333 assert!(!workspace.right_dock().read(cx).is_open());
5334 assert!(panel.is_zoomed(cx));
5335 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5336 });
5337
5338 // Transferring focus back to the panel keeps it zoomed
5339 workspace.update(cx, |workspace, cx| {
5340 workspace.toggle_panel_focus::<TestPanel>(cx);
5341 });
5342
5343 workspace.update(cx, |workspace, cx| {
5344 assert!(workspace.right_dock().read(cx).is_open());
5345 assert!(panel.is_zoomed(cx));
5346 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5347 });
5348
5349 // Close the dock while it is zoomed
5350 workspace.update(cx, |workspace, cx| {
5351 workspace.toggle_dock(DockPosition::Right, cx)
5352 });
5353
5354 workspace.update(cx, |workspace, cx| {
5355 assert!(!workspace.right_dock().read(cx).is_open());
5356 assert!(panel.is_zoomed(cx));
5357 assert!(workspace.zoomed.is_none());
5358 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5359 });
5360
5361 // Opening the dock, when it's zoomed, retains focus
5362 workspace.update(cx, |workspace, cx| {
5363 workspace.toggle_dock(DockPosition::Right, cx)
5364 });
5365
5366 workspace.update(cx, |workspace, cx| {
5367 assert!(workspace.right_dock().read(cx).is_open());
5368 assert!(panel.is_zoomed(cx));
5369 assert!(workspace.zoomed.is_some());
5370 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5371 });
5372
5373 // Unzoom and close the panel, zoom the active pane.
5374 panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
5375 workspace.update(cx, |workspace, cx| {
5376 workspace.toggle_dock(DockPosition::Right, cx)
5377 });
5378 pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
5379
5380 // Opening a dock unzooms the pane.
5381 workspace.update(cx, |workspace, cx| {
5382 workspace.toggle_dock(DockPosition::Right, cx)
5383 });
5384 workspace.update(cx, |workspace, cx| {
5385 let pane = pane.read(cx);
5386 assert!(!pane.is_zoomed());
5387 assert!(!pane.focus_handle(cx).is_focused(cx));
5388 assert!(workspace.right_dock().read(cx).is_open());
5389 assert!(workspace.zoomed.is_none());
5390 });
5391 }
5392
5393 struct TestModal(FocusHandle);
5394
5395 impl TestModal {
5396 fn new(cx: &mut ViewContext<Self>) -> Self {
5397 Self(cx.focus_handle())
5398 }
5399 }
5400
5401 impl EventEmitter<DismissEvent> for TestModal {}
5402
5403 impl FocusableView for TestModal {
5404 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
5405 self.0.clone()
5406 }
5407 }
5408
5409 impl ModalView for TestModal {}
5410
5411 impl Render for TestModal {
5412 fn render(&mut self, _cx: &mut ViewContext<TestModal>) -> impl IntoElement {
5413 div().track_focus(&self.0)
5414 }
5415 }
5416
5417 #[gpui::test]
5418 async fn test_panels(cx: &mut gpui::TestAppContext) {
5419 init_test(cx);
5420 let fs = FakeFs::new(cx.executor());
5421
5422 let project = Project::test(fs, [], cx).await;
5423 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5424
5425 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
5426 let panel_1 = cx.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
5427 workspace.add_panel(panel_1.clone(), cx);
5428 workspace
5429 .left_dock()
5430 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
5431 let panel_2 = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
5432 workspace.add_panel(panel_2.clone(), cx);
5433 workspace
5434 .right_dock()
5435 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5436
5437 let left_dock = workspace.left_dock();
5438 assert_eq!(
5439 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5440 panel_1.panel_id()
5441 );
5442 assert_eq!(
5443 left_dock.read(cx).active_panel_size(cx).unwrap(),
5444 panel_1.size(cx)
5445 );
5446
5447 left_dock.update(cx, |left_dock, cx| {
5448 left_dock.resize_active_panel(Some(px(1337.)), cx)
5449 });
5450 assert_eq!(
5451 workspace
5452 .right_dock()
5453 .read(cx)
5454 .visible_panel()
5455 .unwrap()
5456 .panel_id(),
5457 panel_2.panel_id(),
5458 );
5459
5460 (panel_1, panel_2)
5461 });
5462
5463 // Move panel_1 to the right
5464 panel_1.update(cx, |panel_1, cx| {
5465 panel_1.set_position(DockPosition::Right, cx)
5466 });
5467
5468 workspace.update(cx, |workspace, cx| {
5469 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
5470 // Since it was the only panel on the left, the left dock should now be closed.
5471 assert!(!workspace.left_dock().read(cx).is_open());
5472 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
5473 let right_dock = workspace.right_dock();
5474 assert_eq!(
5475 right_dock.read(cx).visible_panel().unwrap().panel_id(),
5476 panel_1.panel_id()
5477 );
5478 assert_eq!(
5479 right_dock.read(cx).active_panel_size(cx).unwrap(),
5480 px(1337.)
5481 );
5482
5483 // Now we move panel_2 to the left
5484 panel_2.set_position(DockPosition::Left, cx);
5485 });
5486
5487 workspace.update(cx, |workspace, cx| {
5488 // Since panel_2 was not visible on the right, we don't open the left dock.
5489 assert!(!workspace.left_dock().read(cx).is_open());
5490 // And the right dock is unaffected in its displaying of panel_1
5491 assert!(workspace.right_dock().read(cx).is_open());
5492 assert_eq!(
5493 workspace
5494 .right_dock()
5495 .read(cx)
5496 .visible_panel()
5497 .unwrap()
5498 .panel_id(),
5499 panel_1.panel_id(),
5500 );
5501 });
5502
5503 // Move panel_1 back to the left
5504 panel_1.update(cx, |panel_1, cx| {
5505 panel_1.set_position(DockPosition::Left, cx)
5506 });
5507
5508 workspace.update(cx, |workspace, cx| {
5509 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
5510 let left_dock = workspace.left_dock();
5511 assert!(left_dock.read(cx).is_open());
5512 assert_eq!(
5513 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5514 panel_1.panel_id()
5515 );
5516 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), px(1337.));
5517 // And the right dock should be closed as it no longer has any panels.
5518 assert!(!workspace.right_dock().read(cx).is_open());
5519
5520 // Now we move panel_1 to the bottom
5521 panel_1.set_position(DockPosition::Bottom, cx);
5522 });
5523
5524 workspace.update(cx, |workspace, cx| {
5525 // Since panel_1 was visible on the left, we close the left dock.
5526 assert!(!workspace.left_dock().read(cx).is_open());
5527 // The bottom dock is sized based on the panel's default size,
5528 // since the panel orientation changed from vertical to horizontal.
5529 let bottom_dock = workspace.bottom_dock();
5530 assert_eq!(
5531 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
5532 panel_1.size(cx),
5533 );
5534 // Close bottom dock and move panel_1 back to the left.
5535 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
5536 panel_1.set_position(DockPosition::Left, cx);
5537 });
5538
5539 // Emit activated event on panel 1
5540 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
5541
5542 // Now the left dock is open and panel_1 is active and focused.
5543 workspace.update(cx, |workspace, cx| {
5544 let left_dock = workspace.left_dock();
5545 assert!(left_dock.read(cx).is_open());
5546 assert_eq!(
5547 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5548 panel_1.panel_id(),
5549 );
5550 assert!(panel_1.focus_handle(cx).is_focused(cx));
5551 });
5552
5553 // Emit closed event on panel 2, which is not active
5554 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
5555
5556 // Wo don't close the left dock, because panel_2 wasn't the active panel
5557 workspace.update(cx, |workspace, cx| {
5558 let left_dock = workspace.left_dock();
5559 assert!(left_dock.read(cx).is_open());
5560 assert_eq!(
5561 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5562 panel_1.panel_id(),
5563 );
5564 });
5565
5566 // Emitting a ZoomIn event shows the panel as zoomed.
5567 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
5568 workspace.update(cx, |workspace, _| {
5569 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5570 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
5571 });
5572
5573 // Move panel to another dock while it is zoomed
5574 panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
5575 workspace.update(cx, |workspace, _| {
5576 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5577
5578 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5579 });
5580
5581 // This is a helper for getting a:
5582 // - valid focus on an element,
5583 // - that isn't a part of the panes and panels system of the Workspace,
5584 // - and doesn't trigger the 'on_focus_lost' API.
5585 let focus_other_view = {
5586 let workspace = workspace.clone();
5587 move |cx: &mut VisualTestContext| {
5588 workspace.update(cx, |workspace, cx| {
5589 if let Some(_) = workspace.active_modal::<TestModal>(cx) {
5590 workspace.toggle_modal(cx, TestModal::new);
5591 workspace.toggle_modal(cx, TestModal::new);
5592 } else {
5593 workspace.toggle_modal(cx, TestModal::new);
5594 }
5595 })
5596 }
5597 };
5598
5599 // If focus is transferred to another view that's not a panel or another pane, we still show
5600 // the panel as zoomed.
5601 focus_other_view(cx);
5602 workspace.update(cx, |workspace, _| {
5603 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5604 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5605 });
5606
5607 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
5608 workspace.update(cx, |_, cx| cx.focus_self());
5609 workspace.update(cx, |workspace, _| {
5610 assert_eq!(workspace.zoomed, None);
5611 assert_eq!(workspace.zoomed_position, None);
5612 });
5613
5614 // If focus is transferred again to another view that's not a panel or a pane, we won't
5615 // show the panel as zoomed because it wasn't zoomed before.
5616 focus_other_view(cx);
5617 workspace.update(cx, |workspace, _| {
5618 assert_eq!(workspace.zoomed, None);
5619 assert_eq!(workspace.zoomed_position, None);
5620 });
5621
5622 // When the panel is activated, it is zoomed again.
5623 cx.dispatch_action(ToggleRightDock);
5624 workspace.update(cx, |workspace, _| {
5625 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5626 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5627 });
5628
5629 // Emitting a ZoomOut event unzooms the panel.
5630 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
5631 workspace.update(cx, |workspace, _| {
5632 assert_eq!(workspace.zoomed, None);
5633 assert_eq!(workspace.zoomed_position, None);
5634 });
5635
5636 // Emit closed event on panel 1, which is active
5637 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
5638
5639 // Now the left dock is closed, because panel_1 was the active panel
5640 workspace.update(cx, |workspace, cx| {
5641 let right_dock = workspace.right_dock();
5642 assert!(!right_dock.read(cx).is_open());
5643 });
5644 }
5645
5646 pub fn init_test(cx: &mut TestAppContext) {
5647 cx.update(|cx| {
5648 let settings_store = SettingsStore::test(cx);
5649 cx.set_global(settings_store);
5650 theme::init(theme::LoadThemes::JustBase, cx);
5651 language::init(cx);
5652 crate::init_settings(cx);
5653 Project::init_settings(cx);
5654 });
5655 }
5656}