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