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