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