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