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