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