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