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| this.open_workspace_for_paths(paths, cx))
1391 .log_err()
1392 {
1393 task.await.log_err();
1394 }
1395 })
1396 .detach()
1397 }
1398
1399 pub fn open_workspace_for_paths(
1400 &mut self,
1401 paths: Vec<PathBuf>,
1402 cx: &mut ViewContext<Self>,
1403 ) -> Task<Result<()>> {
1404 let window = cx.window_handle().downcast::<Self>();
1405 let is_remote = self.project.read(cx).is_remote();
1406 let has_worktree = self.project.read(cx).worktrees().next().is_some();
1407 let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
1408 let window_to_replace = if is_remote || has_worktree || has_dirty_items {
1409 None
1410 } else {
1411 window
1412 };
1413 let app_state = self.app_state.clone();
1414
1415 cx.spawn(|_, mut cx| async move {
1416 cx.update(|cx| open_paths(&paths, &app_state, window_to_replace, cx))?
1417 .await?;
1418 Ok(())
1419 })
1420 }
1421
1422 #[allow(clippy::type_complexity)]
1423 pub fn open_paths(
1424 &mut self,
1425 mut abs_paths: Vec<PathBuf>,
1426 visible: OpenVisible,
1427 pane: Option<WeakView<Pane>>,
1428 cx: &mut ViewContext<Self>,
1429 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
1430 log::info!("open paths {abs_paths:?}");
1431
1432 let fs = self.app_state.fs.clone();
1433
1434 // Sort the paths to ensure we add worktrees for parents before their children.
1435 abs_paths.sort_unstable();
1436 cx.spawn(move |this, mut cx| async move {
1437 let mut tasks = Vec::with_capacity(abs_paths.len());
1438
1439 for abs_path in &abs_paths {
1440 let visible = match visible {
1441 OpenVisible::All => Some(true),
1442 OpenVisible::None => Some(false),
1443 OpenVisible::OnlyFiles => match fs.metadata(abs_path).await.log_err() {
1444 Some(Some(metadata)) => Some(!metadata.is_dir),
1445 Some(None) => {
1446 log::error!("No metadata for file {abs_path:?}");
1447 None
1448 }
1449 None => None,
1450 },
1451 OpenVisible::OnlyDirectories => match fs.metadata(abs_path).await.log_err() {
1452 Some(Some(metadata)) => Some(metadata.is_dir),
1453 Some(None) => {
1454 log::error!("No metadata for file {abs_path:?}");
1455 None
1456 }
1457 None => None,
1458 },
1459 };
1460 let project_path = match visible {
1461 Some(visible) => match this
1462 .update(&mut cx, |this, cx| {
1463 Workspace::project_path_for_path(
1464 this.project.clone(),
1465 abs_path,
1466 visible,
1467 cx,
1468 )
1469 })
1470 .log_err()
1471 {
1472 Some(project_path) => project_path.await.log_err(),
1473 None => None,
1474 },
1475 None => None,
1476 };
1477
1478 let this = this.clone();
1479 let abs_path = abs_path.clone();
1480 let fs = fs.clone();
1481 let pane = pane.clone();
1482 let task = cx.spawn(move |mut cx| async move {
1483 let (worktree, project_path) = project_path?;
1484 if fs.is_file(&abs_path).await {
1485 Some(
1486 this.update(&mut cx, |this, cx| {
1487 this.open_path(project_path, pane, true, cx)
1488 })
1489 .log_err()?
1490 .await,
1491 )
1492 } else {
1493 this.update(&mut cx, |workspace, cx| {
1494 let worktree = worktree.read(cx);
1495 let worktree_abs_path = worktree.abs_path();
1496 let entry_id = if abs_path == worktree_abs_path.as_ref() {
1497 worktree.root_entry()
1498 } else {
1499 abs_path
1500 .strip_prefix(worktree_abs_path.as_ref())
1501 .ok()
1502 .and_then(|relative_path| {
1503 worktree.entry_for_path(relative_path)
1504 })
1505 }
1506 .map(|entry| entry.id);
1507 if let Some(entry_id) = entry_id {
1508 workspace.project.update(cx, |_, cx| {
1509 cx.emit(project::Event::ActiveEntryChanged(Some(entry_id)));
1510 })
1511 }
1512 })
1513 .log_err()?;
1514 None
1515 }
1516 });
1517 tasks.push(task);
1518 }
1519
1520 futures::future::join_all(tasks).await
1521 })
1522 }
1523
1524 fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
1525 let paths = cx.prompt_for_paths(PathPromptOptions {
1526 files: false,
1527 directories: true,
1528 multiple: true,
1529 });
1530 cx.spawn(|this, mut cx| async move {
1531 if let Some(paths) = paths.await.log_err().flatten() {
1532 let results = this
1533 .update(&mut cx, |this, cx| {
1534 this.open_paths(paths, OpenVisible::All, None, cx)
1535 })?
1536 .await;
1537 for result in results.into_iter().flatten() {
1538 result.log_err();
1539 }
1540 }
1541 anyhow::Ok(())
1542 })
1543 .detach_and_log_err(cx);
1544 }
1545
1546 fn project_path_for_path(
1547 project: Model<Project>,
1548 abs_path: &Path,
1549 visible: bool,
1550 cx: &mut AppContext,
1551 ) -> Task<Result<(Model<Worktree>, ProjectPath)>> {
1552 let entry = project.update(cx, |project, cx| {
1553 project.find_or_create_local_worktree(abs_path, visible, cx)
1554 });
1555 cx.spawn(|mut cx| async move {
1556 let (worktree, path) = entry.await?;
1557 let worktree_id = worktree.update(&mut cx, |t, _| t.id())?;
1558 Ok((
1559 worktree,
1560 ProjectPath {
1561 worktree_id,
1562 path: path.into(),
1563 },
1564 ))
1565 })
1566 }
1567
1568 pub fn items<'a>(
1569 &'a self,
1570 cx: &'a AppContext,
1571 ) -> impl 'a + Iterator<Item = &Box<dyn ItemHandle>> {
1572 self.panes.iter().flat_map(|pane| pane.read(cx).items())
1573 }
1574
1575 pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<View<T>> {
1576 self.items_of_type(cx).max_by_key(|item| item.item_id())
1577 }
1578
1579 pub fn items_of_type<'a, T: Item>(
1580 &'a self,
1581 cx: &'a AppContext,
1582 ) -> impl 'a + Iterator<Item = View<T>> {
1583 self.panes
1584 .iter()
1585 .flat_map(|pane| pane.read(cx).items_of_type())
1586 }
1587
1588 pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
1589 self.active_pane().read(cx).active_item()
1590 }
1591
1592 pub fn active_item_as<I: 'static>(&self, cx: &AppContext) -> Option<View<I>> {
1593 let item = self.active_item(cx)?;
1594 item.to_any().downcast::<I>().ok()
1595 }
1596
1597 fn active_project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
1598 self.active_item(cx).and_then(|item| item.project_path(cx))
1599 }
1600
1601 pub fn save_active_item(
1602 &mut self,
1603 save_intent: SaveIntent,
1604 cx: &mut WindowContext,
1605 ) -> Task<Result<()>> {
1606 let project = self.project.clone();
1607 let pane = self.active_pane();
1608 let item_ix = pane.read(cx).active_item_index();
1609 let item = pane.read(cx).active_item();
1610 let pane = pane.downgrade();
1611
1612 cx.spawn(|mut cx| async move {
1613 if let Some(item) = item {
1614 Pane::save_item(project, &pane, item_ix, item.as_ref(), save_intent, &mut cx)
1615 .await
1616 .map(|_| ())
1617 } else {
1618 Ok(())
1619 }
1620 })
1621 }
1622
1623 pub fn close_inactive_items_and_panes(
1624 &mut self,
1625 _: &CloseInactiveTabsAndPanes,
1626 cx: &mut ViewContext<Self>,
1627 ) {
1628 self.close_all_internal(true, SaveIntent::Close, cx)
1629 .map(|task| task.detach_and_log_err(cx));
1630 }
1631
1632 pub fn close_all_items_and_panes(
1633 &mut self,
1634 action: &CloseAllItemsAndPanes,
1635 cx: &mut ViewContext<Self>,
1636 ) {
1637 self.close_all_internal(false, action.save_intent.unwrap_or(SaveIntent::Close), cx)
1638 .map(|task| task.detach_and_log_err(cx));
1639 }
1640
1641 fn close_all_internal(
1642 &mut self,
1643 retain_active_pane: bool,
1644 save_intent: SaveIntent,
1645 cx: &mut ViewContext<Self>,
1646 ) -> Option<Task<Result<()>>> {
1647 let current_pane = self.active_pane();
1648
1649 let mut tasks = Vec::new();
1650
1651 if retain_active_pane {
1652 if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| {
1653 pane.close_inactive_items(&CloseInactiveItems, cx)
1654 }) {
1655 tasks.push(current_pane_close);
1656 };
1657 }
1658
1659 for pane in self.panes() {
1660 if retain_active_pane && pane.entity_id() == current_pane.entity_id() {
1661 continue;
1662 }
1663
1664 if let Some(close_pane_items) = pane.update(cx, |pane: &mut Pane, cx| {
1665 pane.close_all_items(
1666 &CloseAllItems {
1667 save_intent: Some(save_intent),
1668 },
1669 cx,
1670 )
1671 }) {
1672 tasks.push(close_pane_items)
1673 }
1674 }
1675
1676 if tasks.is_empty() {
1677 None
1678 } else {
1679 Some(cx.spawn(|_, _| async move {
1680 for task in tasks {
1681 task.await?
1682 }
1683 Ok(())
1684 }))
1685 }
1686 }
1687
1688 pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext<Self>) {
1689 let dock = match dock_side {
1690 DockPosition::Left => &self.left_dock,
1691 DockPosition::Bottom => &self.bottom_dock,
1692 DockPosition::Right => &self.right_dock,
1693 };
1694 let mut focus_center = false;
1695 let mut reveal_dock = false;
1696 dock.update(cx, |dock, cx| {
1697 let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
1698 let was_visible = dock.is_open() && !other_is_zoomed;
1699 dock.set_open(!was_visible, cx);
1700
1701 if let Some(active_panel) = dock.active_panel() {
1702 if was_visible {
1703 if active_panel.focus_handle(cx).contains_focused(cx) {
1704 focus_center = true;
1705 }
1706 } else {
1707 let focus_handle = &active_panel.focus_handle(cx);
1708 cx.focus(focus_handle);
1709 reveal_dock = true;
1710 }
1711 }
1712 });
1713
1714 if reveal_dock {
1715 self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx);
1716 }
1717
1718 if focus_center {
1719 self.active_pane.update(cx, |pane, cx| pane.focus(cx))
1720 }
1721
1722 cx.notify();
1723 self.serialize_workspace(cx);
1724 }
1725
1726 pub fn close_all_docks(&mut self, cx: &mut ViewContext<Self>) {
1727 let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock];
1728
1729 for dock in docks {
1730 dock.update(cx, |dock, cx| {
1731 dock.set_open(false, cx);
1732 });
1733 }
1734
1735 cx.focus_self();
1736 cx.notify();
1737 self.serialize_workspace(cx);
1738 }
1739
1740 /// Transfer focus to the panel of the given type.
1741 pub fn focus_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) -> Option<View<T>> {
1742 let panel = self.focus_or_unfocus_panel::<T>(cx, |_, _| true)?;
1743 panel.to_any().downcast().ok()
1744 }
1745
1746 /// Focus the panel of the given type if it isn't already focused. If it is
1747 /// already focused, then transfer focus back to the workspace center.
1748 pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
1749 self.focus_or_unfocus_panel::<T>(cx, |panel, cx| {
1750 !panel.focus_handle(cx).contains_focused(cx)
1751 });
1752 }
1753
1754 /// Focus or unfocus the given panel type, depending on the given callback.
1755 fn focus_or_unfocus_panel<T: Panel>(
1756 &mut self,
1757 cx: &mut ViewContext<Self>,
1758 should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext<Dock>) -> bool,
1759 ) -> Option<Arc<dyn PanelHandle>> {
1760 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
1761 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
1762 let mut focus_center = false;
1763 let panel = dock.update(cx, |dock, cx| {
1764 dock.activate_panel(panel_index, cx);
1765
1766 let panel = dock.active_panel().cloned();
1767 if let Some(panel) = panel.as_ref() {
1768 if should_focus(&**panel, cx) {
1769 dock.set_open(true, cx);
1770 panel.focus_handle(cx).focus(cx);
1771 } else {
1772 focus_center = true;
1773 }
1774 }
1775 panel
1776 });
1777
1778 if focus_center {
1779 self.active_pane.update(cx, |pane, cx| pane.focus(cx))
1780 }
1781
1782 self.serialize_workspace(cx);
1783 cx.notify();
1784 return panel;
1785 }
1786 }
1787 None
1788 }
1789
1790 /// Open the panel of the given type
1791 pub fn open_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
1792 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
1793 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
1794 dock.update(cx, |dock, cx| {
1795 dock.activate_panel(panel_index, cx);
1796 dock.set_open(true, cx);
1797 });
1798 }
1799 }
1800 }
1801
1802 pub fn panel<T: Panel>(&self, cx: &WindowContext) -> Option<View<T>> {
1803 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
1804 let dock = dock.read(cx);
1805 if let Some(panel) = dock.panel::<T>() {
1806 return Some(panel);
1807 }
1808 }
1809 None
1810 }
1811
1812 fn dismiss_zoomed_items_to_reveal(
1813 &mut self,
1814 dock_to_reveal: Option<DockPosition>,
1815 cx: &mut ViewContext<Self>,
1816 ) {
1817 // If a center pane is zoomed, unzoom it.
1818 for pane in &self.panes {
1819 if pane != &self.active_pane || dock_to_reveal.is_some() {
1820 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
1821 }
1822 }
1823
1824 // If another dock is zoomed, hide it.
1825 let mut focus_center = false;
1826 for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
1827 dock.update(cx, |dock, cx| {
1828 if Some(dock.position()) != dock_to_reveal {
1829 if let Some(panel) = dock.active_panel() {
1830 if panel.is_zoomed(cx) {
1831 focus_center |= panel.focus_handle(cx).contains_focused(cx);
1832 dock.set_open(false, cx);
1833 }
1834 }
1835 }
1836 });
1837 }
1838
1839 if focus_center {
1840 self.active_pane.update(cx, |pane, cx| pane.focus(cx))
1841 }
1842
1843 if self.zoomed_position != dock_to_reveal {
1844 self.zoomed = None;
1845 self.zoomed_position = None;
1846 }
1847
1848 cx.notify();
1849 }
1850
1851 fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> View<Pane> {
1852 let pane = cx.new_view(|cx| {
1853 Pane::new(
1854 self.weak_handle(),
1855 self.project.clone(),
1856 self.pane_history_timestamp.clone(),
1857 None,
1858 cx,
1859 )
1860 });
1861 cx.subscribe(&pane, Self::handle_pane_event).detach();
1862 self.panes.push(pane.clone());
1863 cx.focus_view(&pane);
1864 cx.emit(Event::PaneAdded(pane.clone()));
1865 pane
1866 }
1867
1868 pub fn add_item_to_center(
1869 &mut self,
1870 item: Box<dyn ItemHandle>,
1871 cx: &mut ViewContext<Self>,
1872 ) -> bool {
1873 if let Some(center_pane) = self.last_active_center_pane.clone() {
1874 if let Some(center_pane) = center_pane.upgrade() {
1875 center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
1876 true
1877 } else {
1878 false
1879 }
1880 } else {
1881 false
1882 }
1883 }
1884
1885 pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut WindowContext) {
1886 if let Some(text) = item.telemetry_event_text(cx) {
1887 self.client()
1888 .telemetry()
1889 .report_app_event(format!("{}: open", text));
1890 }
1891
1892 self.active_pane
1893 .update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
1894 }
1895
1896 pub fn split_item(
1897 &mut self,
1898 split_direction: SplitDirection,
1899 item: Box<dyn ItemHandle>,
1900 cx: &mut ViewContext<Self>,
1901 ) {
1902 let new_pane = self.split_pane(self.active_pane.clone(), split_direction, cx);
1903 new_pane.update(cx, move |new_pane, cx| {
1904 new_pane.add_item(item, true, true, None, cx)
1905 })
1906 }
1907
1908 pub fn open_abs_path(
1909 &mut self,
1910 abs_path: PathBuf,
1911 visible: bool,
1912 cx: &mut ViewContext<Self>,
1913 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
1914 cx.spawn(|workspace, mut cx| async move {
1915 let open_paths_task_result = workspace
1916 .update(&mut cx, |workspace, cx| {
1917 workspace.open_paths(
1918 vec![abs_path.clone()],
1919 if visible {
1920 OpenVisible::All
1921 } else {
1922 OpenVisible::None
1923 },
1924 None,
1925 cx,
1926 )
1927 })
1928 .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
1929 .await;
1930 anyhow::ensure!(
1931 open_paths_task_result.len() == 1,
1932 "open abs path {abs_path:?} task returned incorrect number of results"
1933 );
1934 match open_paths_task_result
1935 .into_iter()
1936 .next()
1937 .expect("ensured single task result")
1938 {
1939 Some(open_result) => {
1940 open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
1941 }
1942 None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
1943 }
1944 })
1945 }
1946
1947 pub fn split_abs_path(
1948 &mut self,
1949 abs_path: PathBuf,
1950 visible: bool,
1951 cx: &mut ViewContext<Self>,
1952 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
1953 let project_path_task =
1954 Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
1955 cx.spawn(|this, mut cx| async move {
1956 let (_, path) = project_path_task.await?;
1957 this.update(&mut cx, |this, cx| this.split_path(path, cx))?
1958 .await
1959 })
1960 }
1961
1962 pub fn open_path(
1963 &mut self,
1964 path: impl Into<ProjectPath>,
1965 pane: Option<WeakView<Pane>>,
1966 focus_item: bool,
1967 cx: &mut WindowContext,
1968 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
1969 let pane = pane.unwrap_or_else(|| {
1970 self.last_active_center_pane.clone().unwrap_or_else(|| {
1971 self.panes
1972 .first()
1973 .expect("There must be an active pane")
1974 .downgrade()
1975 })
1976 });
1977
1978 let task = self.load_path(path.into(), cx);
1979 cx.spawn(move |mut cx| async move {
1980 let (project_entry_id, build_item) = task.await?;
1981 pane.update(&mut cx, |pane, cx| {
1982 pane.open_item(project_entry_id, focus_item, cx, build_item)
1983 })
1984 })
1985 }
1986
1987 pub fn split_path(
1988 &mut self,
1989 path: impl Into<ProjectPath>,
1990 cx: &mut ViewContext<Self>,
1991 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
1992 let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
1993 self.panes
1994 .first()
1995 .expect("There must be an active pane")
1996 .downgrade()
1997 });
1998
1999 if let Member::Pane(center_pane) = &self.center.root {
2000 if center_pane.read(cx).items_len() == 0 {
2001 return self.open_path(path, Some(pane), true, cx);
2002 }
2003 }
2004
2005 let task = self.load_path(path.into(), cx);
2006 cx.spawn(|this, mut cx| async move {
2007 let (project_entry_id, build_item) = task.await?;
2008 this.update(&mut cx, move |this, cx| -> Option<_> {
2009 let pane = pane.upgrade()?;
2010 let new_pane = this.split_pane(pane, SplitDirection::Right, cx);
2011 new_pane.update(cx, |new_pane, cx| {
2012 Some(new_pane.open_item(project_entry_id, true, cx, build_item))
2013 })
2014 })
2015 .map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))?
2016 })
2017 }
2018
2019 fn load_path(
2020 &mut self,
2021 path: ProjectPath,
2022 cx: &mut WindowContext,
2023 ) -> Task<
2024 Result<(
2025 Option<ProjectEntryId>,
2026 impl 'static + Send + FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
2027 )>,
2028 > {
2029 let project = self.project().clone();
2030 let project_item = project.update(cx, |project, cx| project.open_path(path, cx));
2031 cx.spawn(|mut cx| async move {
2032 let (project_entry_id, project_item) = project_item.await?;
2033 let build_item = cx.update(|cx| {
2034 cx.default_global::<ProjectItemBuilders>()
2035 .get(&project_item.entity_type())
2036 .ok_or_else(|| anyhow!("no item builder for project item"))
2037 .cloned()
2038 })??;
2039 let build_item =
2040 move |cx: &mut ViewContext<Pane>| build_item(project, project_item, cx);
2041 Ok((project_entry_id, build_item))
2042 })
2043 }
2044
2045 pub fn open_project_item<T>(
2046 &mut self,
2047 project_item: Model<T::Item>,
2048 cx: &mut ViewContext<Self>,
2049 ) -> View<T>
2050 where
2051 T: ProjectItem,
2052 {
2053 use project::Item as _;
2054
2055 let entry_id = project_item.read(cx).entry_id(cx);
2056 if let Some(item) = entry_id
2057 .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
2058 .and_then(|item| item.downcast())
2059 {
2060 self.activate_item(&item, cx);
2061 return item;
2062 }
2063
2064 let item = cx.new_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
2065 self.add_item(Box::new(item.clone()), cx);
2066 item
2067 }
2068
2069 pub fn split_project_item<T>(
2070 &mut self,
2071 project_item: Model<T::Item>,
2072 cx: &mut ViewContext<Self>,
2073 ) -> View<T>
2074 where
2075 T: ProjectItem,
2076 {
2077 use project::Item as _;
2078
2079 let entry_id = project_item.read(cx).entry_id(cx);
2080 if let Some(item) = entry_id
2081 .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
2082 .and_then(|item| item.downcast())
2083 {
2084 self.activate_item(&item, cx);
2085 return item;
2086 }
2087
2088 let item = cx.new_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
2089 self.split_item(SplitDirection::Right, Box::new(item.clone()), cx);
2090 item
2091 }
2092
2093 pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
2094 if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
2095 self.active_pane.update(cx, |pane, cx| {
2096 pane.add_item(Box::new(shared_screen), false, true, None, cx)
2097 });
2098 }
2099 }
2100
2101 pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut WindowContext) -> bool {
2102 let result = self.panes.iter().find_map(|pane| {
2103 pane.read(cx)
2104 .index_for_item(item)
2105 .map(|ix| (pane.clone(), ix))
2106 });
2107 if let Some((pane, ix)) = result {
2108 pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
2109 true
2110 } else {
2111 false
2112 }
2113 }
2114
2115 fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
2116 let panes = self.center.panes();
2117 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
2118 cx.focus_view(&pane);
2119 } else {
2120 self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, cx);
2121 }
2122 }
2123
2124 pub fn activate_next_pane(&mut self, cx: &mut WindowContext) {
2125 let panes = self.center.panes();
2126 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
2127 let next_ix = (ix + 1) % panes.len();
2128 let next_pane = panes[next_ix].clone();
2129 cx.focus_view(&next_pane);
2130 }
2131 }
2132
2133 pub fn activate_previous_pane(&mut self, cx: &mut WindowContext) {
2134 let panes = self.center.panes();
2135 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
2136 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
2137 let prev_pane = panes[prev_ix].clone();
2138 cx.focus_view(&prev_pane);
2139 }
2140 }
2141
2142 pub fn activate_pane_in_direction(
2143 &mut self,
2144 direction: SplitDirection,
2145 cx: &mut WindowContext,
2146 ) {
2147 use ActivateInDirectionTarget as Target;
2148 enum Origin {
2149 LeftDock,
2150 RightDock,
2151 BottomDock,
2152 Center,
2153 }
2154
2155 let origin: Origin = [
2156 (&self.left_dock, Origin::LeftDock),
2157 (&self.right_dock, Origin::RightDock),
2158 (&self.bottom_dock, Origin::BottomDock),
2159 ]
2160 .into_iter()
2161 .find_map(|(dock, origin)| {
2162 if dock.focus_handle(cx).contains_focused(cx) && dock.read(cx).is_open() {
2163 Some(origin)
2164 } else {
2165 None
2166 }
2167 })
2168 .unwrap_or(Origin::Center);
2169
2170 let get_last_active_pane = || {
2171 self.last_active_center_pane.as_ref().and_then(|p| {
2172 let p = p.upgrade()?;
2173 (p.read(cx).items_len() != 0).then_some(p)
2174 })
2175 };
2176
2177 let try_dock =
2178 |dock: &View<Dock>| dock.read(cx).is_open().then(|| Target::Dock(dock.clone()));
2179
2180 let target = match (origin, direction) {
2181 // We're in the center, so we first try to go to a different pane,
2182 // otherwise try to go to a dock.
2183 (Origin::Center, direction) => {
2184 if let Some(pane) = self.find_pane_in_direction(direction, cx) {
2185 Some(Target::Pane(pane))
2186 } else {
2187 match direction {
2188 SplitDirection::Up => None,
2189 SplitDirection::Down => try_dock(&self.bottom_dock),
2190 SplitDirection::Left => try_dock(&self.left_dock),
2191 SplitDirection::Right => try_dock(&self.right_dock),
2192 }
2193 }
2194 }
2195
2196 (Origin::LeftDock, SplitDirection::Right) => {
2197 if let Some(last_active_pane) = get_last_active_pane() {
2198 Some(Target::Pane(last_active_pane))
2199 } else {
2200 try_dock(&self.bottom_dock).or_else(|| try_dock(&self.right_dock))
2201 }
2202 }
2203
2204 (Origin::LeftDock, SplitDirection::Down)
2205 | (Origin::RightDock, SplitDirection::Down) => try_dock(&self.bottom_dock),
2206
2207 (Origin::BottomDock, SplitDirection::Up) => get_last_active_pane().map(Target::Pane),
2208 (Origin::BottomDock, SplitDirection::Left) => try_dock(&self.left_dock),
2209 (Origin::BottomDock, SplitDirection::Right) => try_dock(&self.right_dock),
2210
2211 (Origin::RightDock, SplitDirection::Left) => {
2212 if let Some(last_active_pane) = get_last_active_pane() {
2213 Some(Target::Pane(last_active_pane))
2214 } else {
2215 try_dock(&self.bottom_dock).or_else(|| try_dock(&self.left_dock))
2216 }
2217 }
2218
2219 _ => None,
2220 };
2221
2222 match target {
2223 Some(ActivateInDirectionTarget::Pane(pane)) => cx.focus_view(&pane),
2224 Some(ActivateInDirectionTarget::Dock(dock)) => {
2225 if let Some(panel) = dock.read(cx).active_panel() {
2226 panel.focus_handle(cx).focus(cx);
2227 } else {
2228 log::error!("Could not find a focus target when in switching focus in {direction} direction for a {:?} dock", dock.read(cx).position());
2229 }
2230 }
2231 None => {}
2232 }
2233 }
2234
2235 fn find_pane_in_direction(
2236 &mut self,
2237 direction: SplitDirection,
2238 cx: &WindowContext,
2239 ) -> Option<View<Pane>> {
2240 let Some(bounding_box) = self.center.bounding_box_for_pane(&self.active_pane) else {
2241 return None;
2242 };
2243 let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx);
2244 let center = match cursor {
2245 Some(cursor) if bounding_box.contains(&cursor) => cursor,
2246 _ => bounding_box.center(),
2247 };
2248
2249 let distance_to_next = pane_group::HANDLE_HITBOX_SIZE;
2250
2251 let target = match direction {
2252 SplitDirection::Left => {
2253 Point::new(bounding_box.left() - distance_to_next.into(), center.y)
2254 }
2255 SplitDirection::Right => {
2256 Point::new(bounding_box.right() + distance_to_next.into(), center.y)
2257 }
2258 SplitDirection::Up => {
2259 Point::new(center.x, bounding_box.top() - distance_to_next.into())
2260 }
2261 SplitDirection::Down => {
2262 Point::new(center.x, bounding_box.bottom() + distance_to_next.into())
2263 }
2264 };
2265 self.center.pane_at_pixel_position(target).cloned()
2266 }
2267
2268 pub fn swap_pane_in_direction(
2269 &mut self,
2270 direction: SplitDirection,
2271 cx: &mut ViewContext<Self>,
2272 ) {
2273 if let Some(to) = self
2274 .find_pane_in_direction(direction, cx)
2275 .map(|pane| pane.clone())
2276 {
2277 self.center.swap(&self.active_pane.clone(), &to);
2278 cx.notify();
2279 }
2280 }
2281
2282 fn handle_pane_focused(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
2283 if self.active_pane != pane {
2284 self.active_pane = pane.clone();
2285 self.status_bar.update(cx, |status_bar, cx| {
2286 status_bar.set_active_pane(&self.active_pane, cx);
2287 });
2288 self.active_item_path_changed(cx);
2289 self.last_active_center_pane = Some(pane.downgrade());
2290 }
2291
2292 self.dismiss_zoomed_items_to_reveal(None, cx);
2293 if pane.read(cx).is_zoomed() {
2294 self.zoomed = Some(pane.downgrade().into());
2295 } else {
2296 self.zoomed = None;
2297 }
2298 self.zoomed_position = None;
2299 self.update_active_view_for_followers(cx);
2300
2301 cx.notify();
2302 }
2303
2304 fn handle_pane_event(
2305 &mut self,
2306 pane: View<Pane>,
2307 event: &pane::Event,
2308 cx: &mut ViewContext<Self>,
2309 ) {
2310 match event {
2311 pane::Event::AddItem { item } => item.added_to_pane(self, pane, cx),
2312 pane::Event::Split(direction) => {
2313 self.split_and_clone(pane, *direction, cx);
2314 }
2315 pane::Event::Remove => self.remove_pane(pane, cx),
2316 pane::Event::ActivateItem { local } => {
2317 if *local {
2318 self.unfollow(&pane, cx);
2319 }
2320 if &pane == self.active_pane() {
2321 self.active_item_path_changed(cx);
2322 self.update_active_view_for_followers(cx);
2323 }
2324 }
2325 pane::Event::ChangeItemTitle => {
2326 if pane == self.active_pane {
2327 self.active_item_path_changed(cx);
2328 }
2329 self.update_window_edited(cx);
2330 }
2331 pane::Event::RemoveItem { item_id } => {
2332 self.update_window_edited(cx);
2333 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
2334 if entry.get().entity_id() == pane.entity_id() {
2335 entry.remove();
2336 }
2337 }
2338 }
2339 pane::Event::Focus => {
2340 self.handle_pane_focused(pane.clone(), cx);
2341 }
2342 pane::Event::ZoomIn => {
2343 if pane == self.active_pane {
2344 pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
2345 if pane.read(cx).has_focus(cx) {
2346 self.zoomed = Some(pane.downgrade().into());
2347 self.zoomed_position = None;
2348 }
2349 cx.notify();
2350 }
2351 }
2352 pane::Event::ZoomOut => {
2353 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
2354 if self.zoomed_position.is_none() {
2355 self.zoomed = None;
2356 }
2357 cx.notify();
2358 }
2359 }
2360
2361 self.serialize_workspace(cx);
2362 }
2363
2364 pub fn split_pane(
2365 &mut self,
2366 pane_to_split: View<Pane>,
2367 split_direction: SplitDirection,
2368 cx: &mut ViewContext<Self>,
2369 ) -> View<Pane> {
2370 let new_pane = self.add_pane(cx);
2371 self.center
2372 .split(&pane_to_split, &new_pane, split_direction)
2373 .unwrap();
2374 cx.notify();
2375 new_pane
2376 }
2377
2378 pub fn split_and_clone(
2379 &mut self,
2380 pane: View<Pane>,
2381 direction: SplitDirection,
2382 cx: &mut ViewContext<Self>,
2383 ) -> Option<View<Pane>> {
2384 let item = pane.read(cx).active_item()?;
2385 let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
2386 let new_pane = self.add_pane(cx);
2387 new_pane.update(cx, |pane, cx| pane.add_item(clone, true, true, None, cx));
2388 self.center.split(&pane, &new_pane, direction).unwrap();
2389 Some(new_pane)
2390 } else {
2391 None
2392 };
2393 cx.notify();
2394 maybe_pane_handle
2395 }
2396
2397 pub fn split_pane_with_item(
2398 &mut self,
2399 pane_to_split: WeakView<Pane>,
2400 split_direction: SplitDirection,
2401 from: WeakView<Pane>,
2402 item_id_to_move: EntityId,
2403 cx: &mut ViewContext<Self>,
2404 ) {
2405 let Some(pane_to_split) = pane_to_split.upgrade() else {
2406 return;
2407 };
2408 let Some(from) = from.upgrade() else {
2409 return;
2410 };
2411
2412 let new_pane = self.add_pane(cx);
2413 self.move_item(from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
2414 self.center
2415 .split(&pane_to_split, &new_pane, split_direction)
2416 .unwrap();
2417 cx.notify();
2418 }
2419
2420 pub fn split_pane_with_project_entry(
2421 &mut self,
2422 pane_to_split: WeakView<Pane>,
2423 split_direction: SplitDirection,
2424 project_entry: ProjectEntryId,
2425 cx: &mut ViewContext<Self>,
2426 ) -> Option<Task<Result<()>>> {
2427 let pane_to_split = pane_to_split.upgrade()?;
2428 let new_pane = self.add_pane(cx);
2429 self.center
2430 .split(&pane_to_split, &new_pane, split_direction)
2431 .unwrap();
2432
2433 let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
2434 let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
2435 Some(cx.foreground_executor().spawn(async move {
2436 task.await?;
2437 Ok(())
2438 }))
2439 }
2440
2441 pub fn move_item(
2442 &mut self,
2443 source: View<Pane>,
2444 destination: View<Pane>,
2445 item_id_to_move: EntityId,
2446 destination_index: usize,
2447 cx: &mut ViewContext<Self>,
2448 ) {
2449 let Some((item_ix, item_handle)) = source
2450 .read(cx)
2451 .items()
2452 .enumerate()
2453 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
2454 else {
2455 // Tab was closed during drag
2456 return;
2457 };
2458
2459 let item_handle = item_handle.clone();
2460
2461 if source != destination {
2462 // Close item from previous pane
2463 source.update(cx, |source, cx| {
2464 source.remove_item(item_ix, false, cx);
2465 });
2466 }
2467
2468 // This automatically removes duplicate items in the pane
2469 destination.update(cx, |destination, cx| {
2470 destination.add_item(item_handle, true, true, Some(destination_index), cx);
2471 destination.focus(cx)
2472 });
2473 }
2474
2475 fn remove_pane(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
2476 if self.center.remove(&pane).unwrap() {
2477 self.force_remove_pane(&pane, cx);
2478 self.unfollow(&pane, cx);
2479 self.last_leaders_by_pane.remove(&pane.downgrade());
2480 for removed_item in pane.read(cx).items() {
2481 self.panes_by_item.remove(&removed_item.item_id());
2482 }
2483
2484 cx.notify();
2485 } else {
2486 self.active_item_path_changed(cx);
2487 }
2488 }
2489
2490 pub fn panes(&self) -> &[View<Pane>] {
2491 &self.panes
2492 }
2493
2494 pub fn active_pane(&self) -> &View<Pane> {
2495 &self.active_pane
2496 }
2497
2498 pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<View<Pane>> {
2499 let weak_pane = self.panes_by_item.get(&handle.item_id())?;
2500 weak_pane.upgrade()
2501 }
2502
2503 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
2504 self.follower_states.retain(|_, state| {
2505 if state.leader_id == peer_id {
2506 for item in state.items_by_leader_view_id.values() {
2507 item.set_leader_peer_id(None, cx);
2508 }
2509 false
2510 } else {
2511 true
2512 }
2513 });
2514 cx.notify();
2515 }
2516
2517 pub fn start_following(
2518 &mut self,
2519 leader_id: PeerId,
2520 cx: &mut ViewContext<Self>,
2521 ) -> Option<Task<Result<()>>> {
2522 let pane = self.active_pane().clone();
2523
2524 self.last_leaders_by_pane
2525 .insert(pane.downgrade(), leader_id);
2526 self.unfollow(&pane, cx);
2527 self.follower_states.insert(
2528 pane.clone(),
2529 FollowerState {
2530 leader_id,
2531 active_view_id: None,
2532 items_by_leader_view_id: Default::default(),
2533 },
2534 );
2535 cx.notify();
2536
2537 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
2538 let project_id = self.project.read(cx).remote_id();
2539 let request = self.app_state.client.request(proto::Follow {
2540 room_id,
2541 project_id,
2542 leader_id: Some(leader_id),
2543 });
2544
2545 Some(cx.spawn(|this, mut cx| async move {
2546 let response = request.await?;
2547 this.update(&mut cx, |this, _| {
2548 let state = this
2549 .follower_states
2550 .get_mut(&pane)
2551 .ok_or_else(|| anyhow!("following interrupted"))?;
2552 state.active_view_id = if let Some(active_view_id) = response.active_view_id {
2553 Some(ViewId::from_proto(active_view_id)?)
2554 } else {
2555 None
2556 };
2557 Ok::<_, anyhow::Error>(())
2558 })??;
2559 if let Some(view) = response.active_view {
2560 Self::add_view_from_leader(this.clone(), leader_id, pane.clone(), &view, &mut cx)
2561 .await?;
2562 }
2563 Self::add_views_from_leader(
2564 this.clone(),
2565 leader_id,
2566 vec![pane],
2567 response.views,
2568 &mut cx,
2569 )
2570 .await?;
2571 this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
2572 Ok(())
2573 }))
2574 }
2575
2576 pub fn follow_next_collaborator(
2577 &mut self,
2578 _: &FollowNextCollaborator,
2579 cx: &mut ViewContext<Self>,
2580 ) {
2581 let collaborators = self.project.read(cx).collaborators();
2582 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
2583 let mut collaborators = collaborators.keys().copied();
2584 for peer_id in collaborators.by_ref() {
2585 if peer_id == leader_id {
2586 break;
2587 }
2588 }
2589 collaborators.next()
2590 } else if let Some(last_leader_id) =
2591 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
2592 {
2593 if collaborators.contains_key(last_leader_id) {
2594 Some(*last_leader_id)
2595 } else {
2596 None
2597 }
2598 } else {
2599 None
2600 };
2601
2602 let pane = self.active_pane.clone();
2603 let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next())
2604 else {
2605 return;
2606 };
2607 if Some(leader_id) == self.unfollow(&pane, cx) {
2608 return;
2609 }
2610 self.start_following(leader_id, cx)
2611 .map(|task| task.detach_and_log_err(cx));
2612 }
2613
2614 pub fn follow(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) {
2615 let Some(room) = ActiveCall::global(cx).read(cx).room() else {
2616 return;
2617 };
2618 let room = room.read(cx);
2619 let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
2620 return;
2621 };
2622
2623 let project = self.project.read(cx);
2624
2625 let other_project_id = match remote_participant.location {
2626 call::ParticipantLocation::External => None,
2627 call::ParticipantLocation::UnsharedProject => None,
2628 call::ParticipantLocation::SharedProject { project_id } => {
2629 if Some(project_id) == project.remote_id() {
2630 None
2631 } else {
2632 Some(project_id)
2633 }
2634 }
2635 };
2636
2637 // if they are active in another project, follow there.
2638 if let Some(project_id) = other_project_id {
2639 let app_state = self.app_state.clone();
2640 crate::join_remote_project(project_id, remote_participant.user.id, app_state, cx)
2641 .detach_and_log_err(cx);
2642 }
2643
2644 // if you're already following, find the right pane and focus it.
2645 for (pane, state) in &self.follower_states {
2646 if leader_id == state.leader_id {
2647 cx.focus_view(pane);
2648 return;
2649 }
2650 }
2651
2652 // Otherwise, follow.
2653 self.start_following(leader_id, cx)
2654 .map(|task| task.detach_and_log_err(cx));
2655 }
2656
2657 pub fn unfollow(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Self>) -> Option<PeerId> {
2658 let state = self.follower_states.remove(pane)?;
2659 let leader_id = state.leader_id;
2660 for (_, item) in state.items_by_leader_view_id {
2661 item.set_leader_peer_id(None, cx);
2662 }
2663
2664 if self
2665 .follower_states
2666 .values()
2667 .all(|state| state.leader_id != state.leader_id)
2668 {
2669 let project_id = self.project.read(cx).remote_id();
2670 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
2671 self.app_state
2672 .client
2673 .send(proto::Unfollow {
2674 room_id,
2675 project_id,
2676 leader_id: Some(leader_id),
2677 })
2678 .log_err();
2679 }
2680
2681 cx.notify();
2682 Some(leader_id)
2683 }
2684
2685 pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
2686 self.follower_states
2687 .values()
2688 .any(|state| state.leader_id == peer_id)
2689 }
2690
2691 fn active_item_path_changed(&mut self, cx: &mut WindowContext) {
2692 let active_entry = self.active_project_path(cx);
2693 self.project
2694 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
2695 self.update_window_title(cx);
2696 }
2697
2698 fn update_window_title(&mut self, cx: &mut WindowContext) {
2699 let project = self.project().read(cx);
2700 let mut title = String::new();
2701
2702 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
2703 let filename = path
2704 .path
2705 .file_name()
2706 .map(|s| s.to_string_lossy())
2707 .or_else(|| {
2708 Some(Cow::Borrowed(
2709 project
2710 .worktree_for_id(path.worktree_id, cx)?
2711 .read(cx)
2712 .root_name(),
2713 ))
2714 });
2715
2716 if let Some(filename) = filename {
2717 title.push_str(filename.as_ref());
2718 title.push_str(" — ");
2719 }
2720 }
2721
2722 for (i, name) in project.worktree_root_names(cx).enumerate() {
2723 if i > 0 {
2724 title.push_str(", ");
2725 }
2726 title.push_str(name);
2727 }
2728
2729 if title.is_empty() {
2730 title = "empty project".to_string();
2731 }
2732
2733 if project.is_remote() {
2734 title.push_str(" ↙");
2735 } else if project.is_shared() {
2736 title.push_str(" ↗");
2737 }
2738
2739 cx.set_window_title(&title);
2740 }
2741
2742 fn update_window_edited(&mut self, cx: &mut WindowContext) {
2743 let is_edited = !self.project.read(cx).is_disconnected()
2744 && self
2745 .items(cx)
2746 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
2747 if is_edited != self.window_edited {
2748 self.window_edited = is_edited;
2749 cx.set_window_edited(self.window_edited)
2750 }
2751 }
2752
2753 fn render_notifications(&self, _cx: &ViewContext<Self>) -> Option<Div> {
2754 if self.notifications.is_empty() {
2755 None
2756 } else {
2757 Some(
2758 div()
2759 .absolute()
2760 .z_index(100)
2761 .right_3()
2762 .bottom_3()
2763 .w_96()
2764 .h_full()
2765 .flex()
2766 .flex_col()
2767 .justify_end()
2768 .gap_2()
2769 .children(
2770 self.notifications
2771 .iter()
2772 .map(|(_, _, notification)| notification.to_any()),
2773 ),
2774 )
2775 }
2776 }
2777
2778 // RPC handlers
2779
2780 fn active_view_for_follower(&self, cx: &mut ViewContext<Self>) -> Option<proto::View> {
2781 let item = self.active_item(cx)?;
2782 let leader_id = self
2783 .pane_for(&*item)
2784 .and_then(|pane| self.leader_for_pane(&pane));
2785
2786 let item_handle = item.to_followable_item_handle(cx)?;
2787 let id = item_handle.remote_id(&self.app_state.client, cx)?;
2788 let variant = item_handle.to_state_proto(cx)?;
2789
2790 Some(proto::View {
2791 id: Some(id.to_proto()),
2792 leader_id,
2793 variant: Some(variant),
2794 })
2795 }
2796
2797 fn handle_follow(
2798 &mut self,
2799 follower_project_id: Option<u64>,
2800 cx: &mut ViewContext<Self>,
2801 ) -> proto::FollowResponse {
2802 let client = &self.app_state.client;
2803 let project_id = self.project.read(cx).remote_id();
2804
2805 let active_view = self.active_view_for_follower(cx);
2806 let active_view_id = active_view.as_ref().and_then(|view| view.id.clone());
2807
2808 cx.notify();
2809
2810 proto::FollowResponse {
2811 active_view,
2812 // TODO: once v0.124.0 is retired we can stop sending these
2813 active_view_id,
2814 views: self
2815 .panes()
2816 .iter()
2817 .flat_map(|pane| {
2818 let leader_id = self.leader_for_pane(pane);
2819 pane.read(cx).items().filter_map({
2820 let cx = &cx;
2821 move |item| {
2822 let item = item.to_followable_item_handle(cx)?;
2823
2824 // If the item belongs to a particular project, then it should
2825 // only be included if this project is shared, and the follower
2826 // is in the project.
2827 //
2828 // Some items, like channel notes, do not belong to a particular
2829 // project, so they should be included regardless of whether the
2830 // current project is shared, or what project the follower is in.
2831 if item.is_project_item(cx)
2832 && (project_id.is_none() || project_id != follower_project_id)
2833 {
2834 return None;
2835 }
2836
2837 let id = item.remote_id(client, cx)?.to_proto();
2838 let variant = item.to_state_proto(cx)?;
2839 Some(proto::View {
2840 id: Some(id),
2841 leader_id,
2842 variant: Some(variant),
2843 })
2844 }
2845 })
2846 })
2847 .collect(),
2848 }
2849 }
2850
2851 fn handle_update_followers(
2852 &mut self,
2853 leader_id: PeerId,
2854 message: proto::UpdateFollowers,
2855 _cx: &mut ViewContext<Self>,
2856 ) {
2857 self.leader_updates_tx
2858 .unbounded_send((leader_id, message))
2859 .ok();
2860 }
2861
2862 async fn process_leader_update(
2863 this: &WeakView<Self>,
2864 leader_id: PeerId,
2865 update: proto::UpdateFollowers,
2866 cx: &mut AsyncWindowContext,
2867 ) -> Result<()> {
2868 match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
2869 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
2870 let panes_missing_view = this.update(cx, |this, _| {
2871 let mut panes = vec![];
2872 for (pane, state) in &mut this.follower_states {
2873 if state.leader_id != leader_id {
2874 continue;
2875 }
2876
2877 state.active_view_id =
2878 if let Some(active_view_id) = update_active_view.id.clone() {
2879 Some(ViewId::from_proto(active_view_id)?)
2880 } else {
2881 None
2882 };
2883
2884 if state.active_view_id.is_some_and(|view_id| {
2885 !state.items_by_leader_view_id.contains_key(&view_id)
2886 }) {
2887 panes.push(pane.clone())
2888 }
2889 }
2890 anyhow::Ok(panes)
2891 })??;
2892
2893 if let Some(view) = update_active_view.view {
2894 for pane in panes_missing_view {
2895 Self::add_view_from_leader(this.clone(), leader_id, pane.clone(), &view, cx)
2896 .await?
2897 }
2898 }
2899 }
2900 proto::update_followers::Variant::UpdateView(update_view) => {
2901 let variant = update_view
2902 .variant
2903 .ok_or_else(|| anyhow!("missing update view variant"))?;
2904 let id = update_view
2905 .id
2906 .ok_or_else(|| anyhow!("missing update view id"))?;
2907 let mut tasks = Vec::new();
2908 this.update(cx, |this, cx| {
2909 let project = this.project.clone();
2910 for (_, state) in &mut this.follower_states {
2911 if state.leader_id == leader_id {
2912 let view_id = ViewId::from_proto(id.clone())?;
2913 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
2914 tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
2915 }
2916 }
2917 }
2918 anyhow::Ok(())
2919 })??;
2920 try_join_all(tasks).await.log_err();
2921 }
2922 proto::update_followers::Variant::CreateView(view) => {
2923 let panes = this.update(cx, |this, _| {
2924 this.follower_states
2925 .iter()
2926 .filter_map(|(pane, state)| (state.leader_id == leader_id).then_some(pane))
2927 .cloned()
2928 .collect()
2929 })?;
2930 Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?;
2931 }
2932 }
2933 this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
2934 Ok(())
2935 }
2936
2937 async fn add_view_from_leader(
2938 this: WeakView<Self>,
2939 leader_id: PeerId,
2940 pane: View<Pane>,
2941 view: &proto::View,
2942 cx: &mut AsyncWindowContext,
2943 ) -> Result<()> {
2944 let this = this.upgrade().context("workspace dropped")?;
2945
2946 let item_builders = cx.update(|cx| {
2947 cx.default_global::<FollowableItemBuilders>()
2948 .values()
2949 .map(|b| b.0)
2950 .collect::<Vec<_>>()
2951 })?;
2952
2953 let Some(id) = view.id.clone() else {
2954 return Err(anyhow!("no id for view")).into();
2955 };
2956 let id = ViewId::from_proto(id)?;
2957
2958 let mut variant = view.variant.clone();
2959 if variant.is_none() {
2960 Err(anyhow!("missing view variant"))?;
2961 }
2962
2963 let task = item_builders.iter().find_map(|build_item| {
2964 cx.update(|cx| build_item(pane.clone(), this.clone(), id, &mut variant, cx))
2965 .log_err()
2966 .flatten()
2967 });
2968 let Some(task) = task else {
2969 return Err(anyhow!(
2970 "failed to construct view from leader (maybe from a different version of zed?)"
2971 ));
2972 };
2973
2974 let item = task.await?;
2975
2976 this.update(cx, |this, cx| {
2977 let state = this.follower_states.get_mut(&pane)?;
2978 item.set_leader_peer_id(Some(leader_id), cx);
2979 state.items_by_leader_view_id.insert(id, item);
2980
2981 Some(())
2982 })?;
2983
2984 Ok(())
2985 }
2986
2987 async fn add_views_from_leader(
2988 this: WeakView<Self>,
2989 leader_id: PeerId,
2990 panes: Vec<View<Pane>>,
2991 views: Vec<proto::View>,
2992 cx: &mut AsyncWindowContext,
2993 ) -> Result<()> {
2994 let this = this.upgrade().context("workspace dropped")?;
2995
2996 let item_builders = cx.update(|cx| {
2997 cx.default_global::<FollowableItemBuilders>()
2998 .values()
2999 .map(|b| b.0)
3000 .collect::<Vec<_>>()
3001 })?;
3002
3003 let mut item_tasks_by_pane = HashMap::default();
3004 for pane in panes {
3005 let mut item_tasks = Vec::new();
3006 let mut leader_view_ids = Vec::new();
3007 for view in &views {
3008 let Some(id) = &view.id else {
3009 continue;
3010 };
3011 let id = ViewId::from_proto(id.clone())?;
3012 let mut variant = view.variant.clone();
3013 if variant.is_none() {
3014 Err(anyhow!("missing view variant"))?;
3015 }
3016 for build_item in &item_builders {
3017 let task = cx.update(|cx| {
3018 build_item(pane.clone(), this.clone(), id, &mut variant, cx)
3019 })?;
3020 if let Some(task) = task {
3021 item_tasks.push(task);
3022 leader_view_ids.push(id);
3023 break;
3024 } else if variant.is_none() {
3025 Err(anyhow!(
3026 "failed to construct view from leader (maybe from a different version of zed?)"
3027 ))?;
3028 }
3029 }
3030 }
3031
3032 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
3033 }
3034
3035 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
3036 let items = futures::future::try_join_all(item_tasks).await?;
3037 this.update(cx, |this, cx| {
3038 let state = this.follower_states.get_mut(&pane)?;
3039 for (id, item) in leader_view_ids.into_iter().zip(items) {
3040 item.set_leader_peer_id(Some(leader_id), cx);
3041 state.items_by_leader_view_id.insert(id, item);
3042 }
3043
3044 Some(())
3045 })?;
3046 }
3047 Ok(())
3048 }
3049
3050 pub fn update_active_view_for_followers(&mut self, cx: &mut WindowContext) {
3051 let mut is_project_item = true;
3052 let mut update = proto::UpdateActiveView::default();
3053 if cx.is_window_active() {
3054 if let Some(item) = self.active_item(cx) {
3055 if item.focus_handle(cx).contains_focused(cx) {
3056 let leader_id = self
3057 .pane_for(&*item)
3058 .and_then(|pane| self.leader_for_pane(&pane));
3059
3060 if let Some(item) = item.to_followable_item_handle(cx) {
3061 let id = item
3062 .remote_id(&self.app_state.client, cx)
3063 .map(|id| id.to_proto());
3064
3065 if let Some(id) = id.clone() {
3066 if let Some(variant) = item.to_state_proto(cx) {
3067 let view = Some(proto::View {
3068 id: Some(id.clone()),
3069 leader_id,
3070 variant: Some(variant),
3071 });
3072
3073 is_project_item = item.is_project_item(cx);
3074 update = proto::UpdateActiveView {
3075 view,
3076 // TODO: once v0.124.0 is retired we can stop sending these
3077 id: Some(id),
3078 leader_id,
3079 };
3080 }
3081 };
3082 }
3083 }
3084 }
3085 }
3086
3087 if &update.id != &self.last_active_view_id {
3088 self.last_active_view_id = update.id.clone();
3089 self.update_followers(
3090 is_project_item,
3091 proto::update_followers::Variant::UpdateActiveView(update),
3092 cx,
3093 );
3094 }
3095 }
3096
3097 fn update_followers(
3098 &self,
3099 project_only: bool,
3100 update: proto::update_followers::Variant,
3101 cx: &mut WindowContext,
3102 ) -> Option<()> {
3103 // If this update only applies to for followers in the current project,
3104 // then skip it unless this project is shared. If it applies to all
3105 // followers, regardless of project, then set `project_id` to none,
3106 // indicating that it goes to all followers.
3107 let project_id = if project_only {
3108 Some(self.project.read(cx).remote_id()?)
3109 } else {
3110 None
3111 };
3112 self.app_state().workspace_store.update(cx, |store, cx| {
3113 store.update_followers(project_id, update, cx)
3114 })
3115 }
3116
3117 pub fn leader_for_pane(&self, pane: &View<Pane>) -> Option<PeerId> {
3118 self.follower_states.get(pane).map(|state| state.leader_id)
3119 }
3120
3121 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
3122 cx.notify();
3123
3124 let call = self.active_call()?;
3125 let room = call.read(cx).room()?.read(cx);
3126 let participant = room.remote_participant_for_peer_id(leader_id)?;
3127 let mut items_to_activate = Vec::new();
3128
3129 let leader_in_this_app;
3130 let leader_in_this_project;
3131 match participant.location {
3132 call::ParticipantLocation::SharedProject { project_id } => {
3133 leader_in_this_app = true;
3134 leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
3135 }
3136 call::ParticipantLocation::UnsharedProject => {
3137 leader_in_this_app = true;
3138 leader_in_this_project = false;
3139 }
3140 call::ParticipantLocation::External => {
3141 leader_in_this_app = false;
3142 leader_in_this_project = false;
3143 }
3144 };
3145
3146 for (pane, state) in &self.follower_states {
3147 if state.leader_id != leader_id {
3148 continue;
3149 }
3150 if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
3151 if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
3152 if leader_in_this_project || !item.is_project_item(cx) {
3153 items_to_activate.push((pane.clone(), item.boxed_clone()));
3154 }
3155 }
3156 continue;
3157 }
3158
3159 if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
3160 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
3161 }
3162 }
3163
3164 for (pane, item) in items_to_activate {
3165 let pane_was_focused = pane.read(cx).has_focus(cx);
3166 if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
3167 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
3168 } else {
3169 pane.update(cx, |pane, cx| {
3170 pane.add_item(item.boxed_clone(), false, false, None, cx)
3171 });
3172 }
3173
3174 if pane_was_focused {
3175 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
3176 }
3177 }
3178
3179 None
3180 }
3181
3182 fn shared_screen_for_peer(
3183 &self,
3184 peer_id: PeerId,
3185 pane: &View<Pane>,
3186 cx: &mut WindowContext,
3187 ) -> Option<View<SharedScreen>> {
3188 let call = self.active_call()?;
3189 let room = call.read(cx).room()?.read(cx);
3190 let participant = room.remote_participant_for_peer_id(peer_id)?;
3191 let track = participant.video_tracks.values().next()?.clone();
3192 let user = participant.user.clone();
3193
3194 for item in pane.read(cx).items_of_type::<SharedScreen>() {
3195 if item.read(cx).peer_id == peer_id {
3196 return Some(item);
3197 }
3198 }
3199
3200 Some(cx.new_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
3201 }
3202
3203 pub fn on_window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
3204 if cx.is_window_active() {
3205 self.update_active_view_for_followers(cx);
3206 cx.background_executor()
3207 .spawn(persistence::DB.update_timestamp(self.database_id()))
3208 .detach();
3209 } else {
3210 for pane in &self.panes {
3211 pane.update(cx, |pane, cx| {
3212 if let Some(item) = pane.active_item() {
3213 item.workspace_deactivated(cx);
3214 }
3215 if matches!(
3216 WorkspaceSettings::get_global(cx).autosave,
3217 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
3218 ) {
3219 for item in pane.items() {
3220 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
3221 .detach_and_log_err(cx);
3222 }
3223 }
3224 });
3225 }
3226 }
3227 }
3228
3229 fn active_call(&self) -> Option<&Model<ActiveCall>> {
3230 self.active_call.as_ref().map(|(call, _)| call)
3231 }
3232
3233 fn on_active_call_event(
3234 &mut self,
3235 _: Model<ActiveCall>,
3236 event: &call::room::Event,
3237 cx: &mut ViewContext<Self>,
3238 ) {
3239 match event {
3240 call::room::Event::ParticipantLocationChanged { participant_id }
3241 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
3242 self.leader_updated(*participant_id, cx);
3243 }
3244 _ => {}
3245 }
3246 }
3247
3248 pub fn database_id(&self) -> WorkspaceId {
3249 self.database_id
3250 }
3251
3252 fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
3253 let project = self.project().read(cx);
3254
3255 if project.is_local() {
3256 Some(
3257 project
3258 .visible_worktrees(cx)
3259 .map(|worktree| worktree.read(cx).abs_path())
3260 .collect::<Vec<_>>()
3261 .into(),
3262 )
3263 } else {
3264 None
3265 }
3266 }
3267
3268 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
3269 match member {
3270 Member::Axis(PaneAxis { members, .. }) => {
3271 for child in members.iter() {
3272 self.remove_panes(child.clone(), cx)
3273 }
3274 }
3275 Member::Pane(pane) => {
3276 self.force_remove_pane(&pane, cx);
3277 }
3278 }
3279 }
3280
3281 fn force_remove_pane(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Workspace>) {
3282 self.panes.retain(|p| p != pane);
3283 self.panes
3284 .last()
3285 .unwrap()
3286 .update(cx, |pane, cx| pane.focus(cx));
3287 if self.last_active_center_pane == Some(pane.downgrade()) {
3288 self.last_active_center_pane = None;
3289 }
3290 cx.notify();
3291 }
3292
3293 fn schedule_serialize(&mut self, cx: &mut ViewContext<Self>) {
3294 self._schedule_serialize = Some(cx.spawn(|this, mut cx| async move {
3295 cx.background_executor()
3296 .timer(Duration::from_millis(100))
3297 .await;
3298 this.update(&mut cx, |this, cx| this.serialize_workspace(cx))
3299 .log_err();
3300 }));
3301 }
3302
3303 fn serialize_workspace(&self, cx: &mut WindowContext) {
3304 fn serialize_pane_handle(pane_handle: &View<Pane>, cx: &WindowContext) -> SerializedPane {
3305 let (items, active) = {
3306 let pane = pane_handle.read(cx);
3307 let active_item_id = pane.active_item().map(|item| item.item_id());
3308 (
3309 pane.items()
3310 .filter_map(|item_handle| {
3311 Some(SerializedItem {
3312 kind: Arc::from(item_handle.serialized_item_kind()?),
3313 item_id: item_handle.item_id().as_u64(),
3314 active: Some(item_handle.item_id()) == active_item_id,
3315 })
3316 })
3317 .collect::<Vec<_>>(),
3318 pane.has_focus(cx),
3319 )
3320 };
3321
3322 SerializedPane::new(items, active)
3323 }
3324
3325 fn build_serialized_pane_group(
3326 pane_group: &Member,
3327 cx: &WindowContext,
3328 ) -> SerializedPaneGroup {
3329 match pane_group {
3330 Member::Axis(PaneAxis {
3331 axis,
3332 members,
3333 flexes,
3334 bounding_boxes: _,
3335 }) => SerializedPaneGroup::Group {
3336 axis: SerializedAxis(*axis),
3337 children: members
3338 .iter()
3339 .map(|member| build_serialized_pane_group(member, cx))
3340 .collect::<Vec<_>>(),
3341 flexes: Some(flexes.lock().clone()),
3342 },
3343 Member::Pane(pane_handle) => {
3344 SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, cx))
3345 }
3346 }
3347 }
3348
3349 fn build_serialized_docks(this: &Workspace, cx: &mut WindowContext) -> DockStructure {
3350 let left_dock = this.left_dock.read(cx);
3351 let left_visible = left_dock.is_open();
3352 let left_active_panel = left_dock
3353 .visible_panel()
3354 .and_then(|panel| Some(panel.persistent_name().to_string()));
3355 let left_dock_zoom = left_dock
3356 .visible_panel()
3357 .map(|panel| panel.is_zoomed(cx))
3358 .unwrap_or(false);
3359
3360 let right_dock = this.right_dock.read(cx);
3361 let right_visible = right_dock.is_open();
3362 let right_active_panel = right_dock
3363 .visible_panel()
3364 .and_then(|panel| Some(panel.persistent_name().to_string()));
3365 let right_dock_zoom = right_dock
3366 .visible_panel()
3367 .map(|panel| panel.is_zoomed(cx))
3368 .unwrap_or(false);
3369
3370 let bottom_dock = this.bottom_dock.read(cx);
3371 let bottom_visible = bottom_dock.is_open();
3372 let bottom_active_panel = bottom_dock
3373 .visible_panel()
3374 .and_then(|panel| Some(panel.persistent_name().to_string()));
3375 let bottom_dock_zoom = bottom_dock
3376 .visible_panel()
3377 .map(|panel| panel.is_zoomed(cx))
3378 .unwrap_or(false);
3379
3380 DockStructure {
3381 left: DockData {
3382 visible: left_visible,
3383 active_panel: left_active_panel,
3384 zoom: left_dock_zoom,
3385 },
3386 right: DockData {
3387 visible: right_visible,
3388 active_panel: right_active_panel,
3389 zoom: right_dock_zoom,
3390 },
3391 bottom: DockData {
3392 visible: bottom_visible,
3393 active_panel: bottom_active_panel,
3394 zoom: bottom_dock_zoom,
3395 },
3396 }
3397 }
3398
3399 if let Some(location) = self.location(cx) {
3400 // Load bearing special case:
3401 // - with_local_workspace() relies on this to not have other stuff open
3402 // when you open your log
3403 if !location.paths().is_empty() {
3404 let center_group = build_serialized_pane_group(&self.center.root, cx);
3405 let docks = build_serialized_docks(self, cx);
3406
3407 let serialized_workspace = SerializedWorkspace {
3408 id: self.database_id,
3409 location,
3410 center_group,
3411 bounds: Default::default(),
3412 display: Default::default(),
3413 docks,
3414 };
3415
3416 cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace))
3417 .detach();
3418 }
3419 }
3420 }
3421
3422 pub(crate) fn load_workspace(
3423 serialized_workspace: SerializedWorkspace,
3424 paths_to_open: Vec<Option<ProjectPath>>,
3425 cx: &mut ViewContext<Workspace>,
3426 ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
3427 cx.spawn(|workspace, mut cx| async move {
3428 let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
3429
3430 let mut center_group = None;
3431 let mut center_items = None;
3432
3433 // Traverse the splits tree and add to things
3434 if let Some((group, active_pane, items)) = serialized_workspace
3435 .center_group
3436 .deserialize(
3437 &project,
3438 serialized_workspace.id,
3439 workspace.clone(),
3440 &mut cx,
3441 )
3442 .await
3443 {
3444 center_items = Some(items);
3445 center_group = Some((group, active_pane))
3446 }
3447
3448 let mut items_by_project_path = cx.update(|cx| {
3449 center_items
3450 .unwrap_or_default()
3451 .into_iter()
3452 .filter_map(|item| {
3453 let item = item?;
3454 let project_path = item.project_path(cx)?;
3455 Some((project_path, item))
3456 })
3457 .collect::<HashMap<_, _>>()
3458 })?;
3459
3460 let opened_items = paths_to_open
3461 .into_iter()
3462 .map(|path_to_open| {
3463 path_to_open
3464 .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
3465 })
3466 .collect::<Vec<_>>();
3467
3468 // Remove old panes from workspace panes list
3469 workspace.update(&mut cx, |workspace, cx| {
3470 if let Some((center_group, active_pane)) = center_group {
3471 workspace.remove_panes(workspace.center.root.clone(), cx);
3472
3473 // Swap workspace center group
3474 workspace.center = PaneGroup::with_root(center_group);
3475 workspace.last_active_center_pane = active_pane.as_ref().map(|p| p.downgrade());
3476 if let Some(active_pane) = active_pane {
3477 workspace.active_pane = active_pane;
3478 cx.focus_self();
3479 } else {
3480 workspace.active_pane = workspace.center.first_pane().clone();
3481 }
3482 }
3483
3484 let docks = serialized_workspace.docks;
3485
3486 let right = docks.right.clone();
3487 workspace
3488 .right_dock
3489 .update(cx, |dock, _| dock.serialized_dock = Some(right));
3490 let left = docks.left.clone();
3491 workspace
3492 .left_dock
3493 .update(cx, |dock, _| dock.serialized_dock = Some(left));
3494 let bottom = docks.bottom.clone();
3495 workspace
3496 .bottom_dock
3497 .update(cx, |dock, _| dock.serialized_dock = Some(bottom));
3498
3499 cx.notify();
3500 })?;
3501
3502 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
3503 workspace.update(&mut cx, |workspace, cx| workspace.serialize_workspace(cx))?;
3504
3505 Ok(opened_items)
3506 })
3507 }
3508
3509 fn actions(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
3510 self.add_workspace_actions_listeners(div, cx)
3511 .on_action(cx.listener(Self::close_inactive_items_and_panes))
3512 .on_action(cx.listener(Self::close_all_items_and_panes))
3513 .on_action(cx.listener(Self::save_all))
3514 .on_action(cx.listener(Self::send_keystrokes))
3515 .on_action(cx.listener(Self::add_folder_to_project))
3516 .on_action(cx.listener(Self::follow_next_collaborator))
3517 .on_action(cx.listener(|workspace, _: &Unfollow, cx| {
3518 let pane = workspace.active_pane().clone();
3519 workspace.unfollow(&pane, cx);
3520 }))
3521 .on_action(cx.listener(|workspace, action: &Save, cx| {
3522 workspace
3523 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
3524 .detach_and_log_err(cx);
3525 }))
3526 .on_action(cx.listener(|workspace, _: &SaveAs, cx| {
3527 workspace
3528 .save_active_item(SaveIntent::SaveAs, cx)
3529 .detach_and_log_err(cx);
3530 }))
3531 .on_action(cx.listener(|workspace, _: &ActivatePreviousPane, cx| {
3532 workspace.activate_previous_pane(cx)
3533 }))
3534 .on_action(
3535 cx.listener(|workspace, _: &ActivateNextPane, cx| workspace.activate_next_pane(cx)),
3536 )
3537 .on_action(
3538 cx.listener(|workspace, action: &ActivatePaneInDirection, cx| {
3539 workspace.activate_pane_in_direction(action.0, cx)
3540 }),
3541 )
3542 .on_action(cx.listener(|workspace, action: &SwapPaneInDirection, cx| {
3543 workspace.swap_pane_in_direction(action.0, cx)
3544 }))
3545 .on_action(cx.listener(|this, _: &ToggleLeftDock, cx| {
3546 this.toggle_dock(DockPosition::Left, cx);
3547 }))
3548 .on_action(
3549 cx.listener(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
3550 workspace.toggle_dock(DockPosition::Right, cx);
3551 }),
3552 )
3553 .on_action(
3554 cx.listener(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
3555 workspace.toggle_dock(DockPosition::Bottom, cx);
3556 }),
3557 )
3558 .on_action(
3559 cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
3560 workspace.close_all_docks(cx);
3561 }),
3562 )
3563 .on_action(cx.listener(Workspace::open))
3564 .on_action(cx.listener(Workspace::close_window))
3565 .on_action(cx.listener(Workspace::activate_pane_at_index))
3566 .on_action(
3567 cx.listener(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
3568 workspace.reopen_closed_item(cx).detach();
3569 }),
3570 )
3571 .on_action(|_: &ToggleGraphicsProfiler, cx| cx.toggle_graphics_profiler())
3572 }
3573
3574 #[cfg(any(test, feature = "test-support"))]
3575 pub fn test_new(project: Model<Project>, cx: &mut ViewContext<Self>) -> Self {
3576 use node_runtime::FakeNodeRuntime;
3577
3578 let client = project.read(cx).client();
3579 let user_store = project.read(cx).user_store();
3580
3581 let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
3582 cx.activate_window();
3583 let app_state = Arc::new(AppState {
3584 languages: project.read(cx).languages().clone(),
3585 workspace_store,
3586 client,
3587 user_store,
3588 fs: project.read(cx).fs().clone(),
3589 build_window_options: |_, _, _| Default::default(),
3590 node_runtime: FakeNodeRuntime::new(),
3591 });
3592 let workspace = Self::new(0, project, app_state, cx);
3593 workspace.active_pane.update(cx, |pane, cx| pane.focus(cx));
3594 workspace
3595 }
3596
3597 pub fn register_action<A: Action>(
3598 &mut self,
3599 callback: impl Fn(&mut Self, &A, &mut ViewContext<Self>) + 'static,
3600 ) -> &mut Self {
3601 let callback = Arc::new(callback);
3602
3603 self.workspace_actions.push(Box::new(move |div, cx| {
3604 let callback = callback.clone();
3605 div.on_action(
3606 cx.listener(move |workspace, event, cx| (callback.clone())(workspace, event, cx)),
3607 )
3608 }));
3609 self
3610 }
3611
3612 fn add_workspace_actions_listeners(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
3613 let mut div = div
3614 .on_action(cx.listener(Self::close_inactive_items_and_panes))
3615 .on_action(cx.listener(Self::close_all_items_and_panes))
3616 .on_action(cx.listener(Self::add_folder_to_project))
3617 .on_action(cx.listener(Self::save_all))
3618 .on_action(cx.listener(Self::open));
3619 for action in self.workspace_actions.iter() {
3620 div = (action)(div, cx)
3621 }
3622 div
3623 }
3624
3625 pub fn has_active_modal(&self, cx: &WindowContext<'_>) -> bool {
3626 self.modal_layer.read(cx).has_active_modal()
3627 }
3628
3629 pub fn active_modal<V: ManagedView + 'static>(&mut self, cx: &AppContext) -> Option<View<V>> {
3630 self.modal_layer.read(cx).active_modal()
3631 }
3632
3633 pub fn toggle_modal<V: ModalView, B>(&mut self, cx: &mut WindowContext, build: B)
3634 where
3635 B: FnOnce(&mut ViewContext<V>) -> V,
3636 {
3637 self.modal_layer
3638 .update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build))
3639 }
3640}
3641
3642fn window_bounds_env_override(cx: &AsyncAppContext) -> Option<WindowBounds> {
3643 let display_origin = cx
3644 .update(|cx| Some(cx.displays().first()?.bounds().origin))
3645 .ok()??;
3646 ZED_WINDOW_POSITION
3647 .zip(*ZED_WINDOW_SIZE)
3648 .map(|(position, size)| {
3649 WindowBounds::Fixed(Bounds {
3650 origin: display_origin + position,
3651 size,
3652 })
3653 })
3654}
3655
3656fn open_items(
3657 serialized_workspace: Option<SerializedWorkspace>,
3658 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
3659 app_state: Arc<AppState>,
3660 cx: &mut ViewContext<Workspace>,
3661) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> {
3662 let restored_items = serialized_workspace.map(|serialized_workspace| {
3663 Workspace::load_workspace(
3664 serialized_workspace,
3665 project_paths_to_open
3666 .iter()
3667 .map(|(_, project_path)| project_path)
3668 .cloned()
3669 .collect(),
3670 cx,
3671 )
3672 });
3673
3674 cx.spawn(|workspace, mut cx| async move {
3675 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
3676
3677 if let Some(restored_items) = restored_items {
3678 let restored_items = restored_items.await?;
3679
3680 let restored_project_paths = restored_items
3681 .iter()
3682 .filter_map(|item| {
3683 cx.update(|cx| item.as_ref()?.project_path(cx))
3684 .ok()
3685 .flatten()
3686 })
3687 .collect::<HashSet<_>>();
3688
3689 for restored_item in restored_items {
3690 opened_items.push(restored_item.map(Ok));
3691 }
3692
3693 project_paths_to_open
3694 .iter_mut()
3695 .for_each(|(_, project_path)| {
3696 if let Some(project_path_to_open) = project_path {
3697 if restored_project_paths.contains(project_path_to_open) {
3698 *project_path = None;
3699 }
3700 }
3701 });
3702 } else {
3703 for _ in 0..project_paths_to_open.len() {
3704 opened_items.push(None);
3705 }
3706 }
3707 assert!(opened_items.len() == project_paths_to_open.len());
3708
3709 let tasks =
3710 project_paths_to_open
3711 .into_iter()
3712 .enumerate()
3713 .map(|(i, (abs_path, project_path))| {
3714 let workspace = workspace.clone();
3715 cx.spawn(|mut cx| {
3716 let fs = app_state.fs.clone();
3717 async move {
3718 let file_project_path = project_path?;
3719 if fs.is_file(&abs_path).await {
3720 Some((
3721 i,
3722 workspace
3723 .update(&mut cx, |workspace, cx| {
3724 workspace.open_path(file_project_path, None, true, cx)
3725 })
3726 .log_err()?
3727 .await,
3728 ))
3729 } else {
3730 None
3731 }
3732 }
3733 })
3734 });
3735
3736 let tasks = tasks.collect::<Vec<_>>();
3737
3738 let tasks = futures::future::join_all(tasks.into_iter());
3739 for maybe_opened_path in tasks.await.into_iter() {
3740 if let Some((i, path_open_result)) = maybe_opened_path {
3741 opened_items[i] = Some(path_open_result);
3742 }
3743 }
3744
3745 Ok(opened_items)
3746 })
3747}
3748
3749enum ActivateInDirectionTarget {
3750 Pane(View<Pane>),
3751 Dock(View<Dock>),
3752}
3753
3754fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncAppContext) {
3755 const REPORT_ISSUE_URL: &str = "https://github.com/zed-industries/zed/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
3756
3757 workspace
3758 .update(cx, |workspace, cx| {
3759 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
3760 workspace.show_notification_once(0, cx, |cx| {
3761 cx.new_view(|_| {
3762 MessageNotification::new("Failed to load the database file.")
3763 .with_click_message("Click to let us know about this error")
3764 .on_click(|cx| cx.open_url(REPORT_ISSUE_URL))
3765 })
3766 });
3767 }
3768 })
3769 .log_err();
3770}
3771
3772impl FocusableView for Workspace {
3773 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
3774 self.active_pane.focus_handle(cx)
3775 }
3776}
3777
3778#[derive(Clone, Render)]
3779struct DraggedDock(DockPosition);
3780
3781impl Render for Workspace {
3782 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3783 let mut context = KeyContext::default();
3784 context.add("Workspace");
3785
3786 let (ui_font, ui_font_size) = {
3787 let theme_settings = ThemeSettings::get_global(cx);
3788 (
3789 theme_settings.ui_font.family.clone(),
3790 theme_settings.ui_font_size.clone(),
3791 )
3792 };
3793
3794 let theme = cx.theme().clone();
3795 let colors = theme.colors();
3796 cx.set_rem_size(ui_font_size);
3797
3798 self.actions(div(), cx)
3799 .key_context(context)
3800 .relative()
3801 .size_full()
3802 .flex()
3803 .flex_col()
3804 .font(ui_font)
3805 .gap_0()
3806 .justify_start()
3807 .items_start()
3808 .text_color(colors.text)
3809 .bg(colors.background)
3810 .border()
3811 .border_color(colors.border)
3812 .children(self.titlebar_item.clone())
3813 .child(
3814 div()
3815 .id("workspace")
3816 .relative()
3817 .flex_1()
3818 .w_full()
3819 .flex()
3820 .flex_col()
3821 .overflow_hidden()
3822 .border_t()
3823 .border_b()
3824 .border_color(colors.border)
3825 .child(
3826 canvas({
3827 let this = cx.view().clone();
3828 move |bounds, cx| {
3829 this.update(cx, |this, _cx| {
3830 this.bounds = *bounds;
3831 })
3832 }
3833 })
3834 .absolute()
3835 .size_full(),
3836 )
3837 .on_drag_move(
3838 cx.listener(|workspace, e: &DragMoveEvent<DraggedDock>, cx| {
3839 match e.drag(cx).0 {
3840 DockPosition::Left => {
3841 let size = workspace.bounds.left() + e.event.position.x;
3842 workspace.left_dock.update(cx, |left_dock, cx| {
3843 left_dock.resize_active_panel(Some(size), cx);
3844 });
3845 }
3846 DockPosition::Right => {
3847 let size = workspace.bounds.right() - e.event.position.x;
3848 workspace.right_dock.update(cx, |right_dock, cx| {
3849 right_dock.resize_active_panel(Some(size), cx);
3850 });
3851 }
3852 DockPosition::Bottom => {
3853 let size = workspace.bounds.bottom() - e.event.position.y;
3854 workspace.bottom_dock.update(cx, |bottom_dock, cx| {
3855 bottom_dock.resize_active_panel(Some(size), cx);
3856 });
3857 }
3858 }
3859 }),
3860 )
3861 .child(self.modal_layer.clone())
3862 .child(
3863 div()
3864 .flex()
3865 .flex_row()
3866 .h_full()
3867 // Left Dock
3868 .children(self.zoomed_position.ne(&Some(DockPosition::Left)).then(
3869 || {
3870 div()
3871 .flex()
3872 .flex_none()
3873 .overflow_hidden()
3874 .child(self.left_dock.clone())
3875 },
3876 ))
3877 // Panes
3878 .child(
3879 div()
3880 .flex()
3881 .flex_col()
3882 .flex_1()
3883 .overflow_hidden()
3884 .child(self.center.render(
3885 &self.project,
3886 &self.follower_states,
3887 self.active_call(),
3888 &self.active_pane,
3889 self.zoomed.as_ref(),
3890 &self.app_state,
3891 cx,
3892 ))
3893 .children(
3894 self.zoomed_position
3895 .ne(&Some(DockPosition::Bottom))
3896 .then(|| self.bottom_dock.clone()),
3897 ),
3898 )
3899 // Right Dock
3900 .children(self.zoomed_position.ne(&Some(DockPosition::Right)).then(
3901 || {
3902 div()
3903 .flex()
3904 .flex_none()
3905 .overflow_hidden()
3906 .child(self.right_dock.clone())
3907 },
3908 )),
3909 )
3910 .children(self.render_notifications(cx))
3911 .children(self.zoomed.as_ref().and_then(|view| {
3912 let zoomed_view = view.upgrade()?;
3913 let div = div()
3914 .z_index(1)
3915 .absolute()
3916 .overflow_hidden()
3917 .border_color(colors.border)
3918 .bg(colors.background)
3919 .child(zoomed_view)
3920 .inset_0()
3921 .shadow_lg();
3922
3923 Some(match self.zoomed_position {
3924 Some(DockPosition::Left) => div.right_2().border_r(),
3925 Some(DockPosition::Right) => div.left_2().border_l(),
3926 Some(DockPosition::Bottom) => div.top_2().border_t(),
3927 None => div.top_2().bottom_2().left_2().right_2().border(),
3928 })
3929 })),
3930 )
3931 .child(self.status_bar.clone())
3932 .children(if self.project.read(cx).is_disconnected() {
3933 Some(DisconnectedOverlay)
3934 } else {
3935 None
3936 })
3937 }
3938}
3939
3940impl WorkspaceStore {
3941 pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
3942 Self {
3943 workspaces: Default::default(),
3944 _subscriptions: vec![
3945 client.add_request_handler(cx.weak_model(), Self::handle_follow),
3946 client.add_message_handler(cx.weak_model(), Self::handle_update_followers),
3947 ],
3948 client,
3949 }
3950 }
3951
3952 pub fn update_followers(
3953 &self,
3954 project_id: Option<u64>,
3955 update: proto::update_followers::Variant,
3956 cx: &AppContext,
3957 ) -> Option<()> {
3958 let active_call = ActiveCall::try_global(cx)?;
3959 let room_id = active_call.read(cx).room()?.read(cx).id();
3960 self.client
3961 .send(proto::UpdateFollowers {
3962 room_id,
3963 project_id,
3964 variant: Some(update),
3965 })
3966 .log_err()
3967 }
3968
3969 pub async fn handle_follow(
3970 this: Model<Self>,
3971 envelope: TypedEnvelope<proto::Follow>,
3972 _: Arc<Client>,
3973 mut cx: AsyncAppContext,
3974 ) -> Result<proto::FollowResponse> {
3975 this.update(&mut cx, |this, cx| {
3976 let follower = Follower {
3977 project_id: envelope.payload.project_id,
3978 peer_id: envelope.original_sender_id()?,
3979 };
3980 let active_project = ActiveCall::global(cx).read(cx).location().cloned();
3981
3982 let mut response = proto::FollowResponse::default();
3983 this.workspaces.retain(|workspace| {
3984 workspace
3985 .update(cx, |workspace, cx| {
3986 let handler_response = workspace.handle_follow(follower.project_id, cx);
3987 if response.views.is_empty() {
3988 response.views = handler_response.views;
3989 } else {
3990 response.views.extend_from_slice(&handler_response.views);
3991 }
3992
3993 if let Some(active_view_id) = handler_response.active_view_id.clone() {
3994 if response.active_view_id.is_none()
3995 || Some(workspace.project.downgrade()) == active_project
3996 {
3997 response.active_view_id = Some(active_view_id);
3998 }
3999 }
4000
4001 if let Some(active_view) = handler_response.active_view.clone() {
4002 if workspace.project.read(cx).remote_id() == follower.project_id {
4003 response.active_view = Some(active_view)
4004 }
4005 }
4006 })
4007 .is_ok()
4008 });
4009
4010 Ok(response)
4011 })?
4012 }
4013
4014 async fn handle_update_followers(
4015 this: Model<Self>,
4016 envelope: TypedEnvelope<proto::UpdateFollowers>,
4017 _: Arc<Client>,
4018 mut cx: AsyncAppContext,
4019 ) -> Result<()> {
4020 let leader_id = envelope.original_sender_id()?;
4021 let update = envelope.payload;
4022
4023 this.update(&mut cx, |this, cx| {
4024 this.workspaces.retain(|workspace| {
4025 workspace
4026 .update(cx, |workspace, cx| {
4027 let project_id = workspace.project.read(cx).remote_id();
4028 if update.project_id != project_id && update.project_id.is_some() {
4029 return;
4030 }
4031 workspace.handle_update_followers(leader_id, update.clone(), cx);
4032 })
4033 .is_ok()
4034 });
4035 Ok(())
4036 })?
4037 }
4038}
4039
4040impl ViewId {
4041 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
4042 Ok(Self {
4043 creator: message
4044 .creator
4045 .ok_or_else(|| anyhow!("creator is missing"))?,
4046 id: message.id,
4047 })
4048 }
4049
4050 pub(crate) fn to_proto(&self) -> proto::ViewId {
4051 proto::ViewId {
4052 creator: Some(self.creator),
4053 id: self.id,
4054 }
4055 }
4056}
4057
4058pub trait WorkspaceHandle {
4059 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
4060}
4061
4062impl WorkspaceHandle for View<Workspace> {
4063 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
4064 self.read(cx)
4065 .worktrees(cx)
4066 .flat_map(|worktree| {
4067 let worktree_id = worktree.read(cx).id();
4068 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
4069 worktree_id,
4070 path: f.path.clone(),
4071 })
4072 })
4073 .collect::<Vec<_>>()
4074 }
4075}
4076
4077impl std::fmt::Debug for OpenPaths {
4078 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4079 f.debug_struct("OpenPaths")
4080 .field("paths", &self.paths)
4081 .finish()
4082 }
4083}
4084
4085pub fn activate_workspace_for_project(
4086 cx: &mut AppContext,
4087 predicate: impl Fn(&Project, &AppContext) -> bool + Send + 'static,
4088) -> Option<WindowHandle<Workspace>> {
4089 for window in cx.windows() {
4090 let Some(workspace) = window.downcast::<Workspace>() else {
4091 continue;
4092 };
4093
4094 let predicate = workspace
4095 .update(cx, |workspace, cx| {
4096 let project = workspace.project.read(cx);
4097 if predicate(project, cx) {
4098 cx.activate_window();
4099 true
4100 } else {
4101 false
4102 }
4103 })
4104 .log_err()
4105 .unwrap_or(false);
4106
4107 if predicate {
4108 return Some(workspace);
4109 }
4110 }
4111
4112 None
4113}
4114
4115pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
4116 DB.last_workspace().await.log_err().flatten()
4117}
4118
4119actions!(collab, [OpenChannelNotes]);
4120
4121async fn join_channel_internal(
4122 channel_id: u64,
4123 app_state: &Arc<AppState>,
4124 requesting_window: Option<WindowHandle<Workspace>>,
4125 active_call: &Model<ActiveCall>,
4126 cx: &mut AsyncAppContext,
4127) -> Result<bool> {
4128 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
4129 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
4130 return (false, None);
4131 };
4132
4133 let already_in_channel = room.channel_id() == Some(channel_id);
4134 let should_prompt = room.is_sharing_project()
4135 && room.remote_participants().len() > 0
4136 && !already_in_channel;
4137 let open_room = if already_in_channel {
4138 active_call.room().cloned()
4139 } else {
4140 None
4141 };
4142 (should_prompt, open_room)
4143 })?;
4144
4145 if let Some(room) = open_room {
4146 let task = room.update(cx, |room, cx| {
4147 if let Some((project, host)) = room.most_active_project(cx) {
4148 return Some(join_remote_project(project, host, app_state.clone(), cx));
4149 }
4150
4151 None
4152 })?;
4153 if let Some(task) = task {
4154 task.await?;
4155 }
4156 return anyhow::Ok(true);
4157 }
4158
4159 if should_prompt {
4160 if let Some(workspace) = requesting_window {
4161 let answer = workspace
4162 .update(cx, |_, cx| {
4163 cx.prompt(
4164 PromptLevel::Warning,
4165 "Do you want to switch channels?",
4166 Some("Leaving this call will unshare your current project."),
4167 &["Yes, Join Channel", "Cancel"],
4168 )
4169 })?
4170 .await;
4171
4172 if answer == Ok(1) {
4173 return Ok(false);
4174 }
4175 } else {
4176 return Ok(false); // unreachable!() hopefully
4177 }
4178 }
4179
4180 let client = cx.update(|cx| active_call.read(cx).client())?;
4181
4182 let mut client_status = client.status();
4183
4184 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
4185 'outer: loop {
4186 let Some(status) = client_status.recv().await else {
4187 return Err(anyhow!("error connecting"));
4188 };
4189
4190 match status {
4191 Status::Connecting
4192 | Status::Authenticating
4193 | Status::Reconnecting
4194 | Status::Reauthenticating => continue,
4195 Status::Connected { .. } => break 'outer,
4196 Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
4197 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
4198 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
4199 return Err(ErrorCode::Disconnected.into());
4200 }
4201 }
4202 }
4203
4204 let room = active_call
4205 .update(cx, |active_call, cx| {
4206 active_call.join_channel(channel_id, cx)
4207 })?
4208 .await?;
4209
4210 let Some(room) = room else {
4211 return anyhow::Ok(true);
4212 };
4213
4214 room.update(cx, |room, _| room.room_update_completed())?
4215 .await;
4216
4217 let task = room.update(cx, |room, cx| {
4218 if let Some((project, host)) = room.most_active_project(cx) {
4219 return Some(join_remote_project(project, host, app_state.clone(), cx));
4220 }
4221
4222 // if you are the first to join a channel, share your project
4223 if room.remote_participants().len() == 0 && !room.local_participant_is_guest() {
4224 if let Some(workspace) = requesting_window {
4225 let project = workspace.update(cx, |workspace, cx| {
4226 if !CallSettings::get_global(cx).share_on_join {
4227 return None;
4228 }
4229 let project = workspace.project.read(cx);
4230 if project.is_local()
4231 && project.visible_worktrees(cx).any(|tree| {
4232 tree.read(cx)
4233 .root_entry()
4234 .map_or(false, |entry| entry.is_dir())
4235 })
4236 {
4237 Some(workspace.project.clone())
4238 } else {
4239 None
4240 }
4241 });
4242 if let Ok(Some(project)) = project {
4243 return Some(cx.spawn(|room, mut cx| async move {
4244 room.update(&mut cx, |room, cx| room.share_project(project, cx))?
4245 .await?;
4246 Ok(())
4247 }));
4248 }
4249 }
4250 }
4251
4252 None
4253 })?;
4254 if let Some(task) = task {
4255 task.await?;
4256 return anyhow::Ok(true);
4257 }
4258 anyhow::Ok(false)
4259}
4260
4261pub fn join_channel(
4262 channel_id: u64,
4263 app_state: Arc<AppState>,
4264 requesting_window: Option<WindowHandle<Workspace>>,
4265 cx: &mut AppContext,
4266) -> Task<Result<()>> {
4267 let active_call = ActiveCall::global(cx);
4268 cx.spawn(|mut cx| async move {
4269 let result = join_channel_internal(
4270 channel_id,
4271 &app_state,
4272 requesting_window,
4273 &active_call,
4274 &mut cx,
4275 )
4276 .await;
4277
4278 // join channel succeeded, and opened a window
4279 if matches!(result, Ok(true)) {
4280 return anyhow::Ok(());
4281 }
4282
4283 // find an existing workspace to focus and show call controls
4284 let mut active_window =
4285 requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
4286 if active_window.is_none() {
4287 // no open workspaces, make one to show the error in (blergh)
4288 let (window_handle, _) = cx
4289 .update(|cx| {
4290 Workspace::new_local(vec![], app_state.clone(), requesting_window, cx)
4291 })?
4292 .await?;
4293
4294 if result.is_ok() {
4295 cx.update(|cx| {
4296 cx.dispatch_action(&OpenChannelNotes);
4297 }).log_err();
4298 }
4299
4300 active_window = Some(window_handle);
4301 }
4302
4303 if let Err(err) = result {
4304 log::error!("failed to join channel: {}", err);
4305 if let Some(active_window) = active_window {
4306 active_window
4307 .update(&mut cx, |_, cx| {
4308 let detail: SharedString = match err.error_code() {
4309 ErrorCode::SignedOut => {
4310 "Please sign in to continue.".into()
4311 }
4312 ErrorCode::UpgradeRequired => {
4313 "Your are running an unsupported version of Zed. Please update to continue.".into()
4314 }
4315 ErrorCode::NoSuchChannel => {
4316 "No matching channel was found. Please check the link and try again.".into()
4317 }
4318 ErrorCode::Forbidden => {
4319 "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
4320 }
4321 ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
4322 _ => format!("{}\n\nPlease try again.", err).into(),
4323 };
4324 cx.prompt(
4325 PromptLevel::Critical,
4326 "Failed to join channel",
4327 Some(&detail),
4328 &["Ok"],
4329 )
4330 })?
4331 .await
4332 .ok();
4333 }
4334 }
4335
4336 // return ok, we showed the error to the user.
4337 return anyhow::Ok(());
4338 })
4339}
4340
4341pub async fn get_any_active_workspace(
4342 app_state: Arc<AppState>,
4343 mut cx: AsyncAppContext,
4344) -> anyhow::Result<WindowHandle<Workspace>> {
4345 // find an existing workspace to focus and show call controls
4346 let active_window = activate_any_workspace_window(&mut cx);
4347 if active_window.is_none() {
4348 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, cx))?
4349 .await?;
4350 }
4351 activate_any_workspace_window(&mut cx).context("could not open zed")
4352}
4353
4354fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandle<Workspace>> {
4355 cx.update(|cx| {
4356 for window in cx.windows() {
4357 if let Some(workspace_window) = window.downcast::<Workspace>() {
4358 workspace_window
4359 .update(cx, |_, cx| cx.activate_window())
4360 .ok();
4361 return Some(workspace_window);
4362 }
4363 }
4364 None
4365 })
4366 .ok()
4367 .flatten()
4368}
4369
4370#[allow(clippy::type_complexity)]
4371pub fn open_paths(
4372 abs_paths: &[PathBuf],
4373 app_state: &Arc<AppState>,
4374 requesting_window: Option<WindowHandle<Workspace>>,
4375 cx: &mut AppContext,
4376) -> Task<
4377 anyhow::Result<(
4378 WindowHandle<Workspace>,
4379 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
4380 )>,
4381> {
4382 let app_state = app_state.clone();
4383 let abs_paths = abs_paths.to_vec();
4384 // Open paths in existing workspace if possible
4385 let existing = activate_workspace_for_project(cx, {
4386 let abs_paths = abs_paths.clone();
4387 move |project, cx| project.contains_paths(&abs_paths, cx)
4388 });
4389 cx.spawn(move |mut cx| async move {
4390 if let Some(existing) = existing {
4391 Ok((
4392 existing.clone(),
4393 existing
4394 .update(&mut cx, |workspace, cx| {
4395 workspace.open_paths(abs_paths, OpenVisible::All, None, cx)
4396 })?
4397 .await,
4398 ))
4399 } else {
4400 cx.update(move |cx| {
4401 Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx)
4402 })?
4403 .await
4404 }
4405 })
4406}
4407
4408pub fn open_new(
4409 app_state: &Arc<AppState>,
4410 cx: &mut AppContext,
4411 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static + Send,
4412) -> Task<()> {
4413 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
4414 cx.spawn(|mut cx| async move {
4415 if let Some((workspace, opened_paths)) = task.await.log_err() {
4416 workspace
4417 .update(&mut cx, |workspace, cx| {
4418 if opened_paths.is_empty() {
4419 init(workspace, cx)
4420 }
4421 })
4422 .log_err();
4423 }
4424 })
4425}
4426
4427pub fn create_and_open_local_file(
4428 path: &'static Path,
4429 cx: &mut ViewContext<Workspace>,
4430 default_content: impl 'static + Send + FnOnce() -> Rope,
4431) -> Task<Result<Box<dyn ItemHandle>>> {
4432 cx.spawn(|workspace, mut cx| async move {
4433 let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
4434 if !fs.is_file(path).await {
4435 fs.create_file(path, Default::default()).await?;
4436 fs.save(path, &default_content(), Default::default())
4437 .await?;
4438 }
4439
4440 let mut items = workspace
4441 .update(&mut cx, |workspace, cx| {
4442 workspace.with_local_workspace(cx, |workspace, cx| {
4443 workspace.open_paths(vec![path.to_path_buf()], OpenVisible::None, None, cx)
4444 })
4445 })?
4446 .await?
4447 .await;
4448
4449 let item = items.pop().flatten();
4450 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
4451 })
4452}
4453
4454pub fn join_remote_project(
4455 project_id: u64,
4456 follow_user_id: u64,
4457 app_state: Arc<AppState>,
4458 cx: &mut AppContext,
4459) -> Task<Result<()>> {
4460 let windows = cx.windows();
4461 cx.spawn(|mut cx| async move {
4462 let existing_workspace = windows.into_iter().find_map(|window| {
4463 window.downcast::<Workspace>().and_then(|window| {
4464 window
4465 .update(&mut cx, |workspace, cx| {
4466 if workspace.project().read(cx).remote_id() == Some(project_id) {
4467 Some(window)
4468 } else {
4469 None
4470 }
4471 })
4472 .unwrap_or(None)
4473 })
4474 });
4475
4476 let workspace = if let Some(existing_workspace) = existing_workspace {
4477 existing_workspace
4478 } else {
4479 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
4480 let room = active_call
4481 .read_with(&cx, |call, _| call.room().cloned())?
4482 .ok_or_else(|| anyhow!("not in a call"))?;
4483 let project = room
4484 .update(&mut cx, |room, cx| {
4485 room.join_project(
4486 project_id,
4487 app_state.languages.clone(),
4488 app_state.fs.clone(),
4489 cx,
4490 )
4491 })?
4492 .await?;
4493
4494 let window_bounds_override = window_bounds_env_override(&cx);
4495 cx.update(|cx| {
4496 let options = (app_state.build_window_options)(window_bounds_override, None, cx);
4497 cx.open_window(options, |cx| {
4498 cx.new_view(|cx| Workspace::new(0, project, app_state.clone(), cx))
4499 })
4500 })?
4501 };
4502
4503 workspace.update(&mut cx, |workspace, cx| {
4504 cx.activate(true);
4505 cx.activate_window();
4506
4507 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
4508 let follow_peer_id = room
4509 .read(cx)
4510 .remote_participants()
4511 .iter()
4512 .find(|(_, participant)| participant.user.id == follow_user_id)
4513 .map(|(_, p)| p.peer_id)
4514 .or_else(|| {
4515 // If we couldn't follow the given user, follow the host instead.
4516 let collaborator = workspace
4517 .project()
4518 .read(cx)
4519 .collaborators()
4520 .values()
4521 .find(|collaborator| collaborator.replica_id == 0)?;
4522 Some(collaborator.peer_id)
4523 });
4524
4525 if let Some(follow_peer_id) = follow_peer_id {
4526 workspace.follow(follow_peer_id, cx);
4527 }
4528 }
4529 })?;
4530
4531 anyhow::Ok(())
4532 })
4533}
4534
4535pub fn restart(_: &Restart, cx: &mut AppContext) {
4536 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
4537 let mut workspace_windows = cx
4538 .windows()
4539 .into_iter()
4540 .filter_map(|window| window.downcast::<Workspace>())
4541 .collect::<Vec<_>>();
4542
4543 // If multiple windows have unsaved changes, and need a save prompt,
4544 // prompt in the active window before switching to a different window.
4545 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
4546
4547 let mut prompt = None;
4548 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
4549 prompt = window
4550 .update(cx, |_, cx| {
4551 cx.prompt(
4552 PromptLevel::Info,
4553 "Are you sure you want to restart?",
4554 None,
4555 &["Restart", "Cancel"],
4556 )
4557 })
4558 .ok();
4559 }
4560
4561 cx.spawn(|mut cx| async move {
4562 if let Some(prompt) = prompt {
4563 let answer = prompt.await?;
4564 if answer != 0 {
4565 return Ok(());
4566 }
4567 }
4568
4569 // If the user cancels any save prompt, then keep the app open.
4570 for window in workspace_windows {
4571 if let Ok(should_close) = window.update(&mut cx, |workspace, cx| {
4572 workspace.prepare_to_close(true, cx)
4573 }) {
4574 if !should_close.await? {
4575 return Ok(());
4576 }
4577 }
4578 }
4579
4580 cx.update(|cx| cx.restart())
4581 })
4582 .detach_and_log_err(cx);
4583}
4584
4585fn parse_pixel_position_env_var(value: &str) -> Option<Point<GlobalPixels>> {
4586 let mut parts = value.split(',');
4587 let x: usize = parts.next()?.parse().ok()?;
4588 let y: usize = parts.next()?.parse().ok()?;
4589 Some(point((x as f64).into(), (y as f64).into()))
4590}
4591
4592fn parse_pixel_size_env_var(value: &str) -> Option<Size<GlobalPixels>> {
4593 let mut parts = value.split(',');
4594 let width: usize = parts.next()?.parse().ok()?;
4595 let height: usize = parts.next()?.parse().ok()?;
4596 Some(size((width as f64).into(), (height as f64).into()))
4597}
4598
4599pub fn titlebar_height(cx: &mut WindowContext) -> Pixels {
4600 (1.75 * cx.rem_size()).max(px(32.))
4601}
4602
4603struct DisconnectedOverlay;
4604
4605impl Element for DisconnectedOverlay {
4606 type State = AnyElement;
4607
4608 fn request_layout(
4609 &mut self,
4610 _: Option<Self::State>,
4611 cx: &mut ElementContext,
4612 ) -> (LayoutId, Self::State) {
4613 let mut background = cx.theme().colors().elevated_surface_background;
4614 background.fade_out(0.2);
4615 let mut overlay = div()
4616 .bg(background)
4617 .absolute()
4618 .left_0()
4619 .top(titlebar_height(cx))
4620 .size_full()
4621 .flex()
4622 .items_center()
4623 .justify_center()
4624 .capture_any_mouse_down(|_, cx| cx.stop_propagation())
4625 .capture_any_mouse_up(|_, cx| cx.stop_propagation())
4626 .child(Label::new(
4627 "Your connection to the remote project has been lost.",
4628 ))
4629 .into_any();
4630 (overlay.request_layout(cx), overlay)
4631 }
4632
4633 fn paint(
4634 &mut self,
4635 bounds: Bounds<Pixels>,
4636 overlay: &mut Self::State,
4637 cx: &mut ElementContext,
4638 ) {
4639 cx.with_z_index(u16::MAX, |cx| {
4640 cx.add_opaque_layer(bounds);
4641 overlay.paint(cx);
4642 })
4643 }
4644}
4645
4646impl IntoElement for DisconnectedOverlay {
4647 type Element = Self;
4648
4649 fn element_id(&self) -> Option<ui::prelude::ElementId> {
4650 None
4651 }
4652
4653 fn into_element(self) -> Self::Element {
4654 self
4655 }
4656}
4657
4658#[cfg(test)]
4659mod tests {
4660 use std::{cell::RefCell, rc::Rc};
4661
4662 use super::*;
4663 use crate::{
4664 dock::{test::TestPanel, PanelEvent},
4665 item::{
4666 test::{TestItem, TestProjectItem},
4667 ItemEvent,
4668 },
4669 };
4670 use fs::FakeFs;
4671 use gpui::{px, DismissEvent, TestAppContext, VisualTestContext};
4672 use project::{Project, ProjectEntryId};
4673 use serde_json::json;
4674 use settings::SettingsStore;
4675
4676 #[gpui::test]
4677 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
4678 init_test(cx);
4679
4680 let fs = FakeFs::new(cx.executor());
4681 let project = Project::test(fs, [], cx).await;
4682 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4683
4684 // Adding an item with no ambiguity renders the tab without detail.
4685 let item1 = cx.new_view(|cx| {
4686 let mut item = TestItem::new(cx);
4687 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
4688 item
4689 });
4690 workspace.update(cx, |workspace, cx| {
4691 workspace.add_item(Box::new(item1.clone()), cx);
4692 });
4693 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
4694
4695 // Adding an item that creates ambiguity increases the level of detail on
4696 // both tabs.
4697 let item2 = cx.new_view(|cx| {
4698 let mut item = TestItem::new(cx);
4699 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4700 item
4701 });
4702 workspace.update(cx, |workspace, cx| {
4703 workspace.add_item(Box::new(item2.clone()), cx);
4704 });
4705 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4706 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4707
4708 // Adding an item that creates ambiguity increases the level of detail only
4709 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
4710 // we stop at the highest detail available.
4711 let item3 = cx.new_view(|cx| {
4712 let mut item = TestItem::new(cx);
4713 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4714 item
4715 });
4716 workspace.update(cx, |workspace, cx| {
4717 workspace.add_item(Box::new(item3.clone()), cx);
4718 });
4719 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4720 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4721 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4722 }
4723
4724 #[gpui::test]
4725 async fn test_tracking_active_path(cx: &mut TestAppContext) {
4726 init_test(cx);
4727
4728 let fs = FakeFs::new(cx.executor());
4729 fs.insert_tree(
4730 "/root1",
4731 json!({
4732 "one.txt": "",
4733 "two.txt": "",
4734 }),
4735 )
4736 .await;
4737 fs.insert_tree(
4738 "/root2",
4739 json!({
4740 "three.txt": "",
4741 }),
4742 )
4743 .await;
4744
4745 let project = Project::test(fs, ["root1".as_ref()], cx).await;
4746 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4747 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4748 let worktree_id = project.update(cx, |project, cx| {
4749 project.worktrees().next().unwrap().read(cx).id()
4750 });
4751
4752 let item1 = cx.new_view(|cx| {
4753 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
4754 });
4755 let item2 = cx.new_view(|cx| {
4756 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
4757 });
4758
4759 // Add an item to an empty pane
4760 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
4761 project.update(cx, |project, cx| {
4762 assert_eq!(
4763 project.active_entry(),
4764 project
4765 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4766 .map(|e| e.id)
4767 );
4768 });
4769 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
4770
4771 // Add a second item to a non-empty pane
4772 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
4773 assert_eq!(cx.window_title().as_deref(), Some("two.txt — root1"));
4774 project.update(cx, |project, cx| {
4775 assert_eq!(
4776 project.active_entry(),
4777 project
4778 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
4779 .map(|e| e.id)
4780 );
4781 });
4782
4783 // Close the active item
4784 pane.update(cx, |pane, cx| {
4785 pane.close_active_item(&Default::default(), cx).unwrap()
4786 })
4787 .await
4788 .unwrap();
4789 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
4790 project.update(cx, |project, cx| {
4791 assert_eq!(
4792 project.active_entry(),
4793 project
4794 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4795 .map(|e| e.id)
4796 );
4797 });
4798
4799 // Add a project folder
4800 project
4801 .update(cx, |project, cx| {
4802 project.find_or_create_local_worktree("/root2", true, cx)
4803 })
4804 .await
4805 .unwrap();
4806 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1, root2"));
4807
4808 // Remove a project folder
4809 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
4810 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root2"));
4811 }
4812
4813 #[gpui::test]
4814 async fn test_close_window(cx: &mut TestAppContext) {
4815 init_test(cx);
4816
4817 let fs = FakeFs::new(cx.executor());
4818 fs.insert_tree("/root", json!({ "one": "" })).await;
4819
4820 let project = Project::test(fs, ["root".as_ref()], cx).await;
4821 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4822
4823 // When there are no dirty items, there's nothing to do.
4824 let item1 = cx.new_view(|cx| TestItem::new(cx));
4825 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
4826 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4827 assert!(task.await.unwrap());
4828
4829 // When there are dirty untitled items, prompt to save each one. If the user
4830 // cancels any prompt, then abort.
4831 let item2 = cx.new_view(|cx| TestItem::new(cx).with_dirty(true));
4832 let item3 = cx.new_view(|cx| {
4833 TestItem::new(cx)
4834 .with_dirty(true)
4835 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4836 });
4837 workspace.update(cx, |w, cx| {
4838 w.add_item(Box::new(item2.clone()), cx);
4839 w.add_item(Box::new(item3.clone()), cx);
4840 });
4841 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4842 cx.executor().run_until_parked();
4843 cx.simulate_prompt_answer(2); // cancel save all
4844 cx.executor().run_until_parked();
4845 cx.simulate_prompt_answer(2); // cancel save all
4846 cx.executor().run_until_parked();
4847 assert!(!cx.has_pending_prompt());
4848 assert!(!task.await.unwrap());
4849 }
4850
4851 #[gpui::test]
4852 async fn test_close_pane_items(cx: &mut TestAppContext) {
4853 init_test(cx);
4854
4855 let fs = FakeFs::new(cx.executor());
4856
4857 let project = Project::test(fs, None, cx).await;
4858 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4859
4860 let item1 = cx.new_view(|cx| {
4861 TestItem::new(cx)
4862 .with_dirty(true)
4863 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4864 });
4865 let item2 = cx.new_view(|cx| {
4866 TestItem::new(cx)
4867 .with_dirty(true)
4868 .with_conflict(true)
4869 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
4870 });
4871 let item3 = cx.new_view(|cx| {
4872 TestItem::new(cx)
4873 .with_dirty(true)
4874 .with_conflict(true)
4875 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
4876 });
4877 let item4 = cx.new_view(|cx| {
4878 TestItem::new(cx)
4879 .with_dirty(true)
4880 .with_project_items(&[TestProjectItem::new_untitled(cx)])
4881 });
4882 let pane = workspace.update(cx, |workspace, cx| {
4883 workspace.add_item(Box::new(item1.clone()), cx);
4884 workspace.add_item(Box::new(item2.clone()), cx);
4885 workspace.add_item(Box::new(item3.clone()), cx);
4886 workspace.add_item(Box::new(item4.clone()), cx);
4887 workspace.active_pane().clone()
4888 });
4889
4890 let close_items = pane.update(cx, |pane, cx| {
4891 pane.activate_item(1, true, true, cx);
4892 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
4893 let item1_id = item1.item_id();
4894 let item3_id = item3.item_id();
4895 let item4_id = item4.item_id();
4896 pane.close_items(cx, SaveIntent::Close, move |id| {
4897 [item1_id, item3_id, item4_id].contains(&id)
4898 })
4899 });
4900 cx.executor().run_until_parked();
4901
4902 assert!(cx.has_pending_prompt());
4903 // Ignore "Save all" prompt
4904 cx.simulate_prompt_answer(2);
4905 cx.executor().run_until_parked();
4906 // There's a prompt to save item 1.
4907 pane.update(cx, |pane, _| {
4908 assert_eq!(pane.items_len(), 4);
4909 assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
4910 });
4911 // Confirm saving item 1.
4912 cx.simulate_prompt_answer(0);
4913 cx.executor().run_until_parked();
4914
4915 // Item 1 is saved. There's a prompt to save item 3.
4916 pane.update(cx, |pane, cx| {
4917 assert_eq!(item1.read(cx).save_count, 1);
4918 assert_eq!(item1.read(cx).save_as_count, 0);
4919 assert_eq!(item1.read(cx).reload_count, 0);
4920 assert_eq!(pane.items_len(), 3);
4921 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
4922 });
4923 assert!(cx.has_pending_prompt());
4924
4925 // Cancel saving item 3.
4926 cx.simulate_prompt_answer(1);
4927 cx.executor().run_until_parked();
4928
4929 // Item 3 is reloaded. There's a prompt to save item 4.
4930 pane.update(cx, |pane, cx| {
4931 assert_eq!(item3.read(cx).save_count, 0);
4932 assert_eq!(item3.read(cx).save_as_count, 0);
4933 assert_eq!(item3.read(cx).reload_count, 1);
4934 assert_eq!(pane.items_len(), 2);
4935 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
4936 });
4937 assert!(cx.has_pending_prompt());
4938
4939 // Confirm saving item 4.
4940 cx.simulate_prompt_answer(0);
4941 cx.executor().run_until_parked();
4942
4943 // There's a prompt for a path for item 4.
4944 cx.simulate_new_path_selection(|_| Some(Default::default()));
4945 close_items.await.unwrap();
4946
4947 // The requested items are closed.
4948 pane.update(cx, |pane, cx| {
4949 assert_eq!(item4.read(cx).save_count, 0);
4950 assert_eq!(item4.read(cx).save_as_count, 1);
4951 assert_eq!(item4.read(cx).reload_count, 0);
4952 assert_eq!(pane.items_len(), 1);
4953 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
4954 });
4955 }
4956
4957 #[gpui::test]
4958 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
4959 init_test(cx);
4960
4961 let fs = FakeFs::new(cx.executor());
4962 let project = Project::test(fs, [], cx).await;
4963 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4964
4965 // Create several workspace items with single project entries, and two
4966 // workspace items with multiple project entries.
4967 let single_entry_items = (0..=4)
4968 .map(|project_entry_id| {
4969 cx.new_view(|cx| {
4970 TestItem::new(cx)
4971 .with_dirty(true)
4972 .with_project_items(&[TestProjectItem::new(
4973 project_entry_id,
4974 &format!("{project_entry_id}.txt"),
4975 cx,
4976 )])
4977 })
4978 })
4979 .collect::<Vec<_>>();
4980 let item_2_3 = cx.new_view(|cx| {
4981 TestItem::new(cx)
4982 .with_dirty(true)
4983 .with_singleton(false)
4984 .with_project_items(&[
4985 single_entry_items[2].read(cx).project_items[0].clone(),
4986 single_entry_items[3].read(cx).project_items[0].clone(),
4987 ])
4988 });
4989 let item_3_4 = cx.new_view(|cx| {
4990 TestItem::new(cx)
4991 .with_dirty(true)
4992 .with_singleton(false)
4993 .with_project_items(&[
4994 single_entry_items[3].read(cx).project_items[0].clone(),
4995 single_entry_items[4].read(cx).project_items[0].clone(),
4996 ])
4997 });
4998
4999 // Create two panes that contain the following project entries:
5000 // left pane:
5001 // multi-entry items: (2, 3)
5002 // single-entry items: 0, 1, 2, 3, 4
5003 // right pane:
5004 // single-entry items: 1
5005 // multi-entry items: (3, 4)
5006 let left_pane = workspace.update(cx, |workspace, cx| {
5007 let left_pane = workspace.active_pane().clone();
5008 workspace.add_item(Box::new(item_2_3.clone()), cx);
5009 for item in single_entry_items {
5010 workspace.add_item(Box::new(item), cx);
5011 }
5012 left_pane.update(cx, |pane, cx| {
5013 pane.activate_item(2, true, true, cx);
5014 });
5015
5016 let right_pane = workspace
5017 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
5018 .unwrap();
5019
5020 right_pane.update(cx, |pane, cx| {
5021 pane.add_item(Box::new(item_3_4.clone()), true, true, None, cx);
5022 });
5023
5024 left_pane
5025 });
5026
5027 cx.focus_view(&left_pane);
5028
5029 // When closing all of the items in the left pane, we should be prompted twice:
5030 // once for project entry 0, and once for project entry 2. Project entries 1,
5031 // 3, and 4 are all still open in the other paten. After those two
5032 // prompts, the task should complete.
5033
5034 let close = left_pane.update(cx, |pane, cx| {
5035 pane.close_all_items(&CloseAllItems::default(), cx).unwrap()
5036 });
5037 cx.executor().run_until_parked();
5038
5039 // Discard "Save all" prompt
5040 cx.simulate_prompt_answer(2);
5041
5042 cx.executor().run_until_parked();
5043 left_pane.update(cx, |pane, cx| {
5044 assert_eq!(
5045 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
5046 &[ProjectEntryId::from_proto(0)]
5047 );
5048 });
5049 cx.simulate_prompt_answer(0);
5050
5051 cx.executor().run_until_parked();
5052 left_pane.update(cx, |pane, cx| {
5053 assert_eq!(
5054 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
5055 &[ProjectEntryId::from_proto(2)]
5056 );
5057 });
5058 cx.simulate_prompt_answer(0);
5059
5060 cx.executor().run_until_parked();
5061 close.await.unwrap();
5062 left_pane.update(cx, |pane, _| {
5063 assert_eq!(pane.items_len(), 0);
5064 });
5065 }
5066
5067 #[gpui::test]
5068 async fn test_autosave(cx: &mut gpui::TestAppContext) {
5069 init_test(cx);
5070
5071 let fs = FakeFs::new(cx.executor());
5072 let project = Project::test(fs, [], cx).await;
5073 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5074 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5075
5076 let item = cx.new_view(|cx| {
5077 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5078 });
5079 let item_id = item.entity_id();
5080 workspace.update(cx, |workspace, cx| {
5081 workspace.add_item(Box::new(item.clone()), cx);
5082 });
5083
5084 // Autosave on window change.
5085 item.update(cx, |item, cx| {
5086 cx.update_global(|settings: &mut SettingsStore, cx| {
5087 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5088 settings.autosave = Some(AutosaveSetting::OnWindowChange);
5089 })
5090 });
5091 item.is_dirty = true;
5092 });
5093
5094 // Deactivating the window saves the file.
5095 cx.deactivate_window();
5096 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
5097
5098 // Autosave on focus change.
5099 item.update(cx, |item, cx| {
5100 cx.focus_self();
5101 cx.update_global(|settings: &mut SettingsStore, cx| {
5102 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5103 settings.autosave = Some(AutosaveSetting::OnFocusChange);
5104 })
5105 });
5106 item.is_dirty = true;
5107 });
5108
5109 // Blurring the item saves the file.
5110 item.update(cx, |_, cx| cx.blur());
5111 cx.executor().run_until_parked();
5112 item.update(cx, |item, _| assert_eq!(item.save_count, 2));
5113
5114 // Deactivating the window still saves the file.
5115 cx.update(|cx| cx.activate_window());
5116 item.update(cx, |item, cx| {
5117 cx.focus_self();
5118 item.is_dirty = true;
5119 });
5120 cx.deactivate_window();
5121
5122 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
5123
5124 // Autosave after delay.
5125 item.update(cx, |item, cx| {
5126 cx.update_global(|settings: &mut SettingsStore, cx| {
5127 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5128 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
5129 })
5130 });
5131 item.is_dirty = true;
5132 cx.emit(ItemEvent::Edit);
5133 });
5134
5135 // Delay hasn't fully expired, so the file is still dirty and unsaved.
5136 cx.executor().advance_clock(Duration::from_millis(250));
5137 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
5138
5139 // After delay expires, the file is saved.
5140 cx.executor().advance_clock(Duration::from_millis(250));
5141 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
5142
5143 // Autosave on focus change, ensuring closing the tab counts as such.
5144 item.update(cx, |item, cx| {
5145 cx.update_global(|settings: &mut SettingsStore, cx| {
5146 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5147 settings.autosave = Some(AutosaveSetting::OnFocusChange);
5148 })
5149 });
5150 item.is_dirty = true;
5151 });
5152
5153 pane.update(cx, |pane, cx| {
5154 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
5155 })
5156 .await
5157 .unwrap();
5158 assert!(!cx.has_pending_prompt());
5159 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
5160
5161 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
5162 workspace.update(cx, |workspace, cx| {
5163 workspace.add_item(Box::new(item.clone()), cx);
5164 });
5165 item.update(cx, |item, cx| {
5166 item.project_items[0].update(cx, |item, _| {
5167 item.entry_id = None;
5168 });
5169 item.is_dirty = true;
5170 cx.blur();
5171 });
5172 cx.run_until_parked();
5173 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
5174
5175 // Ensure autosave is prevented for deleted files also when closing the buffer.
5176 let _close_items = pane.update(cx, |pane, cx| {
5177 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
5178 });
5179 cx.run_until_parked();
5180 assert!(cx.has_pending_prompt());
5181 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
5182 }
5183
5184 #[gpui::test]
5185 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
5186 init_test(cx);
5187
5188 let fs = FakeFs::new(cx.executor());
5189
5190 let project = Project::test(fs, [], cx).await;
5191 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5192
5193 let item = cx.new_view(|cx| {
5194 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5195 });
5196 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5197 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
5198 let toolbar_notify_count = Rc::new(RefCell::new(0));
5199
5200 workspace.update(cx, |workspace, cx| {
5201 workspace.add_item(Box::new(item.clone()), cx);
5202 let toolbar_notification_count = toolbar_notify_count.clone();
5203 cx.observe(&toolbar, move |_, _, _| {
5204 *toolbar_notification_count.borrow_mut() += 1
5205 })
5206 .detach();
5207 });
5208
5209 pane.update(cx, |pane, _| {
5210 assert!(!pane.can_navigate_backward());
5211 assert!(!pane.can_navigate_forward());
5212 });
5213
5214 item.update(cx, |item, cx| {
5215 item.set_state("one".to_string(), cx);
5216 });
5217
5218 // Toolbar must be notified to re-render the navigation buttons
5219 assert_eq!(*toolbar_notify_count.borrow(), 1);
5220
5221 pane.update(cx, |pane, _| {
5222 assert!(pane.can_navigate_backward());
5223 assert!(!pane.can_navigate_forward());
5224 });
5225
5226 workspace
5227 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
5228 .await
5229 .unwrap();
5230
5231 assert_eq!(*toolbar_notify_count.borrow(), 2);
5232 pane.update(cx, |pane, _| {
5233 assert!(!pane.can_navigate_backward());
5234 assert!(pane.can_navigate_forward());
5235 });
5236 }
5237
5238 #[gpui::test]
5239 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
5240 init_test(cx);
5241 let fs = FakeFs::new(cx.executor());
5242
5243 let project = Project::test(fs, [], cx).await;
5244 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5245
5246 let panel = workspace.update(cx, |workspace, cx| {
5247 let panel = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
5248 workspace.add_panel(panel.clone(), cx);
5249
5250 workspace
5251 .right_dock()
5252 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5253
5254 panel
5255 });
5256
5257 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5258 pane.update(cx, |pane, cx| {
5259 let item = cx.new_view(|cx| TestItem::new(cx));
5260 pane.add_item(Box::new(item), true, true, None, cx);
5261 });
5262
5263 // Transfer focus from center to panel
5264 workspace.update(cx, |workspace, cx| {
5265 workspace.toggle_panel_focus::<TestPanel>(cx);
5266 });
5267
5268 workspace.update(cx, |workspace, cx| {
5269 assert!(workspace.right_dock().read(cx).is_open());
5270 assert!(!panel.is_zoomed(cx));
5271 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5272 });
5273
5274 // Transfer focus from panel to center
5275 workspace.update(cx, |workspace, cx| {
5276 workspace.toggle_panel_focus::<TestPanel>(cx);
5277 });
5278
5279 workspace.update(cx, |workspace, cx| {
5280 assert!(workspace.right_dock().read(cx).is_open());
5281 assert!(!panel.is_zoomed(cx));
5282 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5283 });
5284
5285 // Close the dock
5286 workspace.update(cx, |workspace, cx| {
5287 workspace.toggle_dock(DockPosition::Right, cx);
5288 });
5289
5290 workspace.update(cx, |workspace, cx| {
5291 assert!(!workspace.right_dock().read(cx).is_open());
5292 assert!(!panel.is_zoomed(cx));
5293 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5294 });
5295
5296 // Open the dock
5297 workspace.update(cx, |workspace, cx| {
5298 workspace.toggle_dock(DockPosition::Right, cx);
5299 });
5300
5301 workspace.update(cx, |workspace, cx| {
5302 assert!(workspace.right_dock().read(cx).is_open());
5303 assert!(!panel.is_zoomed(cx));
5304 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5305 });
5306
5307 // Focus and zoom panel
5308 panel.update(cx, |panel, cx| {
5309 cx.focus_self();
5310 panel.set_zoomed(true, cx)
5311 });
5312
5313 workspace.update(cx, |workspace, cx| {
5314 assert!(workspace.right_dock().read(cx).is_open());
5315 assert!(panel.is_zoomed(cx));
5316 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5317 });
5318
5319 // Transfer focus to the center closes the dock
5320 workspace.update(cx, |workspace, cx| {
5321 workspace.toggle_panel_focus::<TestPanel>(cx);
5322 });
5323
5324 workspace.update(cx, |workspace, cx| {
5325 assert!(!workspace.right_dock().read(cx).is_open());
5326 assert!(panel.is_zoomed(cx));
5327 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5328 });
5329
5330 // Transferring focus back to the panel keeps it zoomed
5331 workspace.update(cx, |workspace, cx| {
5332 workspace.toggle_panel_focus::<TestPanel>(cx);
5333 });
5334
5335 workspace.update(cx, |workspace, cx| {
5336 assert!(workspace.right_dock().read(cx).is_open());
5337 assert!(panel.is_zoomed(cx));
5338 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5339 });
5340
5341 // Close the dock while it is zoomed
5342 workspace.update(cx, |workspace, cx| {
5343 workspace.toggle_dock(DockPosition::Right, cx)
5344 });
5345
5346 workspace.update(cx, |workspace, cx| {
5347 assert!(!workspace.right_dock().read(cx).is_open());
5348 assert!(panel.is_zoomed(cx));
5349 assert!(workspace.zoomed.is_none());
5350 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5351 });
5352
5353 // Opening the dock, when it's zoomed, retains focus
5354 workspace.update(cx, |workspace, cx| {
5355 workspace.toggle_dock(DockPosition::Right, cx)
5356 });
5357
5358 workspace.update(cx, |workspace, cx| {
5359 assert!(workspace.right_dock().read(cx).is_open());
5360 assert!(panel.is_zoomed(cx));
5361 assert!(workspace.zoomed.is_some());
5362 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5363 });
5364
5365 // Unzoom and close the panel, zoom the active pane.
5366 panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
5367 workspace.update(cx, |workspace, cx| {
5368 workspace.toggle_dock(DockPosition::Right, cx)
5369 });
5370 pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
5371
5372 // Opening a dock unzooms the pane.
5373 workspace.update(cx, |workspace, cx| {
5374 workspace.toggle_dock(DockPosition::Right, cx)
5375 });
5376 workspace.update(cx, |workspace, cx| {
5377 let pane = pane.read(cx);
5378 assert!(!pane.is_zoomed());
5379 assert!(!pane.focus_handle(cx).is_focused(cx));
5380 assert!(workspace.right_dock().read(cx).is_open());
5381 assert!(workspace.zoomed.is_none());
5382 });
5383 }
5384
5385 struct TestModal(FocusHandle);
5386
5387 impl TestModal {
5388 fn new(cx: &mut ViewContext<Self>) -> Self {
5389 Self(cx.focus_handle())
5390 }
5391 }
5392
5393 impl EventEmitter<DismissEvent> for TestModal {}
5394
5395 impl FocusableView for TestModal {
5396 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
5397 self.0.clone()
5398 }
5399 }
5400
5401 impl ModalView for TestModal {}
5402
5403 impl Render for TestModal {
5404 fn render(&mut self, _cx: &mut ViewContext<TestModal>) -> impl IntoElement {
5405 div().track_focus(&self.0)
5406 }
5407 }
5408
5409 #[gpui::test]
5410 async fn test_panels(cx: &mut gpui::TestAppContext) {
5411 init_test(cx);
5412 let fs = FakeFs::new(cx.executor());
5413
5414 let project = Project::test(fs, [], cx).await;
5415 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5416
5417 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
5418 let panel_1 = cx.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
5419 workspace.add_panel(panel_1.clone(), cx);
5420 workspace
5421 .left_dock()
5422 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
5423 let panel_2 = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
5424 workspace.add_panel(panel_2.clone(), cx);
5425 workspace
5426 .right_dock()
5427 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5428
5429 let left_dock = workspace.left_dock();
5430 assert_eq!(
5431 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5432 panel_1.panel_id()
5433 );
5434 assert_eq!(
5435 left_dock.read(cx).active_panel_size(cx).unwrap(),
5436 panel_1.size(cx)
5437 );
5438
5439 left_dock.update(cx, |left_dock, cx| {
5440 left_dock.resize_active_panel(Some(px(1337.)), cx)
5441 });
5442 assert_eq!(
5443 workspace
5444 .right_dock()
5445 .read(cx)
5446 .visible_panel()
5447 .unwrap()
5448 .panel_id(),
5449 panel_2.panel_id(),
5450 );
5451
5452 (panel_1, panel_2)
5453 });
5454
5455 // Move panel_1 to the right
5456 panel_1.update(cx, |panel_1, cx| {
5457 panel_1.set_position(DockPosition::Right, cx)
5458 });
5459
5460 workspace.update(cx, |workspace, cx| {
5461 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
5462 // Since it was the only panel on the left, the left dock should now be closed.
5463 assert!(!workspace.left_dock().read(cx).is_open());
5464 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
5465 let right_dock = workspace.right_dock();
5466 assert_eq!(
5467 right_dock.read(cx).visible_panel().unwrap().panel_id(),
5468 panel_1.panel_id()
5469 );
5470 assert_eq!(
5471 right_dock.read(cx).active_panel_size(cx).unwrap(),
5472 px(1337.)
5473 );
5474
5475 // Now we move panel_2 to the left
5476 panel_2.set_position(DockPosition::Left, cx);
5477 });
5478
5479 workspace.update(cx, |workspace, cx| {
5480 // Since panel_2 was not visible on the right, we don't open the left dock.
5481 assert!(!workspace.left_dock().read(cx).is_open());
5482 // And the right dock is unaffected in it's displaying of panel_1
5483 assert!(workspace.right_dock().read(cx).is_open());
5484 assert_eq!(
5485 workspace
5486 .right_dock()
5487 .read(cx)
5488 .visible_panel()
5489 .unwrap()
5490 .panel_id(),
5491 panel_1.panel_id(),
5492 );
5493 });
5494
5495 // Move panel_1 back to the left
5496 panel_1.update(cx, |panel_1, cx| {
5497 panel_1.set_position(DockPosition::Left, cx)
5498 });
5499
5500 workspace.update(cx, |workspace, cx| {
5501 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
5502 let left_dock = workspace.left_dock();
5503 assert!(left_dock.read(cx).is_open());
5504 assert_eq!(
5505 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5506 panel_1.panel_id()
5507 );
5508 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), px(1337.));
5509 // And the right dock should be closed as it no longer has any panels.
5510 assert!(!workspace.right_dock().read(cx).is_open());
5511
5512 // Now we move panel_1 to the bottom
5513 panel_1.set_position(DockPosition::Bottom, cx);
5514 });
5515
5516 workspace.update(cx, |workspace, cx| {
5517 // Since panel_1 was visible on the left, we close the left dock.
5518 assert!(!workspace.left_dock().read(cx).is_open());
5519 // The bottom dock is sized based on the panel's default size,
5520 // since the panel orientation changed from vertical to horizontal.
5521 let bottom_dock = workspace.bottom_dock();
5522 assert_eq!(
5523 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
5524 panel_1.size(cx),
5525 );
5526 // Close bottom dock and move panel_1 back to the left.
5527 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
5528 panel_1.set_position(DockPosition::Left, cx);
5529 });
5530
5531 // Emit activated event on panel 1
5532 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
5533
5534 // Now the left dock is open and panel_1 is active and focused.
5535 workspace.update(cx, |workspace, cx| {
5536 let left_dock = workspace.left_dock();
5537 assert!(left_dock.read(cx).is_open());
5538 assert_eq!(
5539 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5540 panel_1.panel_id(),
5541 );
5542 assert!(panel_1.focus_handle(cx).is_focused(cx));
5543 });
5544
5545 // Emit closed event on panel 2, which is not active
5546 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
5547
5548 // Wo don't close the left dock, because panel_2 wasn't the active panel
5549 workspace.update(cx, |workspace, cx| {
5550 let left_dock = workspace.left_dock();
5551 assert!(left_dock.read(cx).is_open());
5552 assert_eq!(
5553 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5554 panel_1.panel_id(),
5555 );
5556 });
5557
5558 // Emitting a ZoomIn event shows the panel as zoomed.
5559 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
5560 workspace.update(cx, |workspace, _| {
5561 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5562 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
5563 });
5564
5565 // Move panel to another dock while it is zoomed
5566 panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
5567 workspace.update(cx, |workspace, _| {
5568 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5569
5570 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5571 });
5572
5573 // This is a helper for getting a:
5574 // - valid focus on an element,
5575 // - that isn't a part of the panes and panels system of the Workspace,
5576 // - and doesn't trigger the 'on_focus_lost' API.
5577 let focus_other_view = {
5578 let workspace = workspace.clone();
5579 move |cx: &mut VisualTestContext| {
5580 workspace.update(cx, |workspace, cx| {
5581 if let Some(_) = workspace.active_modal::<TestModal>(cx) {
5582 workspace.toggle_modal(cx, TestModal::new);
5583 workspace.toggle_modal(cx, TestModal::new);
5584 } else {
5585 workspace.toggle_modal(cx, TestModal::new);
5586 }
5587 })
5588 }
5589 };
5590
5591 // If focus is transferred to another view that's not a panel or another pane, we still show
5592 // the panel as zoomed.
5593 focus_other_view(cx);
5594 workspace.update(cx, |workspace, _| {
5595 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5596 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5597 });
5598
5599 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
5600 workspace.update(cx, |_, cx| cx.focus_self());
5601 workspace.update(cx, |workspace, _| {
5602 assert_eq!(workspace.zoomed, None);
5603 assert_eq!(workspace.zoomed_position, None);
5604 });
5605
5606 // If focus is transferred again to another view that's not a panel or a pane, we won't
5607 // show the panel as zoomed because it wasn't zoomed before.
5608 focus_other_view(cx);
5609 workspace.update(cx, |workspace, _| {
5610 assert_eq!(workspace.zoomed, None);
5611 assert_eq!(workspace.zoomed_position, None);
5612 });
5613
5614 // When the panel is activated, it is zoomed again.
5615 cx.dispatch_action(ToggleRightDock);
5616 workspace.update(cx, |workspace, _| {
5617 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5618 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5619 });
5620
5621 // Emitting a ZoomOut event unzooms the panel.
5622 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
5623 workspace.update(cx, |workspace, _| {
5624 assert_eq!(workspace.zoomed, None);
5625 assert_eq!(workspace.zoomed_position, None);
5626 });
5627
5628 // Emit closed event on panel 1, which is active
5629 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
5630
5631 // Now the left dock is closed, because panel_1 was the active panel
5632 workspace.update(cx, |workspace, cx| {
5633 let right_dock = workspace.right_dock();
5634 assert!(!right_dock.read(cx).is_open());
5635 });
5636 }
5637
5638 pub fn init_test(cx: &mut TestAppContext) {
5639 cx.update(|cx| {
5640 let settings_store = SettingsStore::test(cx);
5641 cx.set_global(settings_store);
5642 theme::init(theme::LoadThemes::JustBase, cx);
5643 language::init(cx);
5644 crate::init_settings(cx);
5645 Project::init_settings(cx);
5646 });
5647 }
5648}