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