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