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