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