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