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