1pub mod dock;
2pub mod item;
3mod modal_layer;
4pub mod notifications;
5pub mod pane;
6pub mod pane_group;
7mod persistence;
8pub mod searchable;
9pub mod shared_screen;
10mod status_bar;
11mod toolbar;
12mod workspace_settings;
13
14use anyhow::{anyhow, Context as _, Result};
15use call::{call_settings::CallSettings, ActiveCall};
16use client::{
17 proto::{self, ErrorCode, PeerId},
18 Client, ErrorExt, Status, TypedEnvelope, UserStore,
19};
20use collections::{hash_map, HashMap, HashSet};
21use derive_more::{Deref, DerefMut};
22use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
23use futures::{
24 channel::{mpsc, oneshot},
25 future::try_join_all,
26 Future, FutureExt, StreamExt,
27};
28use gpui::{
29 actions, canvas, div, impl_actions, point, px, size, Action, AnyElement, AnyModel, AnyView,
30 AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Context, Div,
31 DragMoveEvent, Element, ElementContext, Entity, EntityId, EventEmitter, FocusHandle,
32 FocusableView, Global, GlobalPixels, InteractiveElement, IntoElement, KeyContext, Keystroke,
33 LayoutId, ManagedView, Model, ModelContext, ParentElement, PathPromptOptions, Pixels, Point,
34 PromptLevel, Render, SharedString, Size, Styled, Subscription, Task, View, ViewContext,
35 VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions,
36};
37use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
38use itertools::Itertools;
39use language::{LanguageRegistry, Rope};
40use lazy_static::lazy_static;
41pub use modal_layer::*;
42use node_runtime::NodeRuntime;
43use notifications::{simple_message_notification::MessageNotification, NotificationHandle};
44pub use pane::*;
45pub use pane_group::*;
46use persistence::{model::SerializedWorkspace, SerializedWindowsBounds, DB};
47pub use persistence::{
48 model::{ItemId, WorkspaceLocation},
49 WorkspaceDb, DB as WORKSPACE_DB,
50};
51use postage::stream::Stream;
52use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
53use serde::Deserialize;
54use settings::Settings;
55use shared_screen::SharedScreen;
56use status_bar::StatusBar;
57pub use status_bar::StatusItemView;
58use std::{
59 any::TypeId,
60 borrow::Cow,
61 cell::RefCell,
62 cmp, env,
63 path::{Path, PathBuf},
64 rc::Rc,
65 sync::{atomic::AtomicUsize, Arc, Weak},
66 time::Duration,
67};
68use task::SpawnInTerminal;
69use theme::{ActiveTheme, SystemAppearance, ThemeSettings};
70pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
71pub use ui;
72use ui::Label;
73use util::ResultExt;
74use uuid::Uuid;
75pub use workspace_settings::{AutosaveSetting, WorkspaceSettings};
76
77use crate::persistence::{
78 model::{DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup},
79 SerializedAxis,
80};
81
82lazy_static! {
83 static ref ZED_WINDOW_SIZE: Option<Size<GlobalPixels>> = env::var("ZED_WINDOW_SIZE")
84 .ok()
85 .as_deref()
86 .and_then(parse_pixel_size_env_var);
87 static ref ZED_WINDOW_POSITION: Option<Point<GlobalPixels>> = env::var("ZED_WINDOW_POSITION")
88 .ok()
89 .as_deref()
90 .and_then(parse_pixel_position_env_var);
91}
92
93#[derive(Clone, PartialEq)]
94pub struct RemoveWorktreeFromProject(pub WorktreeId);
95
96actions!(
97 workspace,
98 [
99 Open,
100 NewFile,
101 NewWindow,
102 CloseWindow,
103 CloseInactiveTabsAndPanes,
104 AddFolderToProject,
105 Unfollow,
106 SaveAs,
107 ReloadActiveItem,
108 ActivatePreviousPane,
109 ActivateNextPane,
110 FollowNextCollaborator,
111 NewTerminal,
112 NewCenterTerminal,
113 NewSearch,
114 Feedback,
115 Restart,
116 Welcome,
117 ToggleZoom,
118 ToggleLeftDock,
119 ToggleRightDock,
120 ToggleBottomDock,
121 CloseAllDocks,
122 ToggleGraphicsProfiler,
123 ]
124);
125
126#[derive(Clone, PartialEq)]
127pub struct OpenPaths {
128 pub paths: Vec<PathBuf>,
129}
130
131#[derive(Clone, Deserialize, PartialEq)]
132pub struct ActivatePane(pub usize);
133
134#[derive(Clone, Deserialize, PartialEq)]
135pub struct ActivatePaneInDirection(pub SplitDirection);
136
137#[derive(Clone, Deserialize, PartialEq)]
138pub struct SwapPaneInDirection(pub SplitDirection);
139
140#[derive(Clone, Deserialize, PartialEq)]
141pub struct NewFileInDirection(pub SplitDirection);
142
143#[derive(Clone, PartialEq, Debug, Deserialize)]
144#[serde(rename_all = "camelCase")]
145pub struct SaveAll {
146 pub save_intent: Option<SaveIntent>,
147}
148
149#[derive(Clone, PartialEq, Debug, Deserialize)]
150#[serde(rename_all = "camelCase")]
151pub struct Save {
152 pub save_intent: Option<SaveIntent>,
153}
154
155#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
156#[serde(rename_all = "camelCase")]
157pub struct CloseAllItemsAndPanes {
158 pub save_intent: Option<SaveIntent>,
159}
160
161#[derive(Clone, Deserialize, PartialEq)]
162pub struct SendKeystrokes(pub String);
163
164impl_actions!(
165 workspace,
166 [
167 ActivatePane,
168 ActivatePaneInDirection,
169 CloseAllItemsAndPanes,
170 NewFileInDirection,
171 OpenTerminal,
172 Save,
173 SaveAll,
174 SwapPaneInDirection,
175 SendKeystrokes,
176 ]
177);
178
179#[derive(Deserialize)]
180pub struct Toast {
181 id: usize,
182 msg: Cow<'static, str>,
183 #[serde(skip)]
184 on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut WindowContext)>)>,
185}
186
187impl Toast {
188 pub fn new<I: Into<Cow<'static, str>>>(id: usize, msg: I) -> Self {
189 Toast {
190 id,
191 msg: msg.into(),
192 on_click: None,
193 }
194 }
195
196 pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
197 where
198 M: Into<Cow<'static, str>>,
199 F: Fn(&mut WindowContext) + 'static,
200 {
201 self.on_click = Some((message.into(), Arc::new(on_click)));
202 self
203 }
204}
205
206impl PartialEq for Toast {
207 fn eq(&self, other: &Self) -> bool {
208 self.id == other.id
209 && self.msg == other.msg
210 && self.on_click.is_some() == other.on_click.is_some()
211 }
212}
213
214impl Clone for Toast {
215 fn clone(&self) -> Self {
216 Toast {
217 id: self.id,
218 msg: self.msg.to_owned(),
219 on_click: self.on_click.clone(),
220 }
221 }
222}
223
224#[derive(Debug, Default, Clone, Deserialize, PartialEq)]
225pub struct OpenTerminal {
226 pub working_directory: PathBuf,
227}
228
229pub type WorkspaceId = i64;
230
231pub fn init_settings(cx: &mut AppContext) {
232 WorkspaceSettings::register(cx);
233 ItemSettings::register(cx);
234}
235
236pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
237 init_settings(cx);
238 notifications::init(cx);
239
240 cx.on_action(Workspace::close_global);
241 cx.on_action(restart);
242
243 cx.on_action({
244 let app_state = Arc::downgrade(&app_state);
245 move |_: &Open, cx: &mut AppContext| {
246 let paths = cx.prompt_for_paths(PathPromptOptions {
247 files: true,
248 directories: true,
249 multiple: true,
250 });
251
252 if let Some(app_state) = app_state.upgrade() {
253 cx.spawn(move |cx| async move {
254 if let Some(paths) = paths.await.log_err().flatten() {
255 cx.update(|cx| {
256 open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx)
257 })
258 .ok();
259 }
260 })
261 .detach();
262 }
263 }
264 });
265}
266
267#[derive(Clone, Default, Deref, DerefMut)]
268struct ProjectItemBuilders(
269 HashMap<TypeId, fn(Model<Project>, AnyModel, &mut ViewContext<Pane>) -> Box<dyn ItemHandle>>,
270);
271
272impl Global for ProjectItemBuilders {}
273
274pub fn register_project_item<I: ProjectItem>(cx: &mut AppContext) {
275 let builders = cx.default_global::<ProjectItemBuilders>();
276 builders.insert(TypeId::of::<I::Item>(), |project, model, cx| {
277 let item = model.downcast::<I::Item>().unwrap();
278 Box::new(cx.new_view(|cx| I::for_project_item(project, item, cx)))
279 });
280}
281
282type FollowableItemBuilder = fn(
283 View<Pane>,
284 View<Workspace>,
285 ViewId,
286 &mut Option<proto::view::Variant>,
287 &mut WindowContext,
288) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
289
290#[derive(Default, Deref, DerefMut)]
291struct FollowableItemBuilders(
292 HashMap<
293 TypeId,
294 (
295 FollowableItemBuilder,
296 fn(&AnyView) -> Box<dyn FollowableItemHandle>,
297 ),
298 >,
299);
300
301impl Global for FollowableItemBuilders {}
302
303pub fn register_followable_item<I: FollowableItem>(cx: &mut AppContext) {
304 let builders = cx.default_global::<FollowableItemBuilders>();
305 builders.insert(
306 TypeId::of::<I>(),
307 (
308 |pane, workspace, id, state, cx| {
309 I::from_state_proto(pane, workspace, id, state, cx).map(|task| {
310 cx.foreground_executor()
311 .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
312 })
313 },
314 |this| Box::new(this.clone().downcast::<I>().unwrap()),
315 ),
316 );
317}
318
319#[derive(Default, Deref, DerefMut)]
320struct ItemDeserializers(
321 HashMap<
322 Arc<str>,
323 fn(
324 Model<Project>,
325 WeakView<Workspace>,
326 WorkspaceId,
327 ItemId,
328 &mut ViewContext<Pane>,
329 ) -> Task<Result<Box<dyn ItemHandle>>>,
330 >,
331);
332
333impl Global for ItemDeserializers {}
334
335pub fn register_deserializable_item<I: Item>(cx: &mut AppContext) {
336 if let Some(serialized_item_kind) = I::serialized_item_kind() {
337 let deserializers = cx.default_global::<ItemDeserializers>();
338 deserializers.insert(
339 Arc::from(serialized_item_kind),
340 |project, workspace, workspace_id, item_id, cx| {
341 let task = I::deserialize(project, workspace, workspace_id, item_id, cx);
342 cx.foreground_executor()
343 .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
344 },
345 );
346 }
347}
348
349pub struct AppState {
350 pub languages: Arc<LanguageRegistry>,
351 pub client: Arc<Client>,
352 pub user_store: Model<UserStore>,
353 pub workspace_store: Model<WorkspaceStore>,
354 pub fs: Arc<dyn fs::Fs>,
355 pub build_window_options:
356 fn(Option<WindowBounds>, Option<Uuid>, &mut AppContext) -> WindowOptions,
357 pub node_runtime: Arc<dyn NodeRuntime>,
358}
359
360struct GlobalAppState(Weak<AppState>);
361
362impl Global for GlobalAppState {}
363
364pub struct WorkspaceStore {
365 workspaces: HashSet<WindowHandle<Workspace>>,
366 client: Arc<Client>,
367 _subscriptions: Vec<client::Subscription>,
368}
369
370#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
371struct Follower {
372 project_id: Option<u64>,
373 peer_id: PeerId,
374}
375
376impl AppState {
377 pub fn global(cx: &AppContext) -> Weak<Self> {
378 cx.global::<GlobalAppState>().0.clone()
379 }
380 pub fn try_global(cx: &AppContext) -> Option<Weak<Self>> {
381 cx.try_global::<GlobalAppState>()
382 .map(|state| state.0.clone())
383 }
384 pub fn set_global(state: Weak<AppState>, cx: &mut AppContext) {
385 cx.set_global(GlobalAppState(state));
386 }
387
388 #[cfg(any(test, feature = "test-support"))]
389 pub fn test(cx: &mut AppContext) -> Arc<Self> {
390 use node_runtime::FakeNodeRuntime;
391 use settings::SettingsStore;
392
393 if !cx.has_global::<SettingsStore>() {
394 let settings_store = SettingsStore::test(cx);
395 cx.set_global(settings_store);
396 }
397
398 let fs = fs::FakeFs::new(cx.background_executor().clone());
399 let languages = Arc::new(LanguageRegistry::test());
400 let 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 SpawnTask(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 {
3008 continue;
3009 };
3010 let id = ViewId::from_proto(id.clone())?;
3011 let mut variant = view.variant.clone();
3012 if variant.is_none() {
3013 Err(anyhow!("missing view variant"))?;
3014 }
3015 for build_item in &item_builders {
3016 let task = cx.update(|cx| {
3017 build_item(pane.clone(), this.clone(), id, &mut variant, cx)
3018 })?;
3019 if let Some(task) = task {
3020 item_tasks.push(task);
3021 leader_view_ids.push(id);
3022 break;
3023 } else if variant.is_none() {
3024 Err(anyhow!(
3025 "failed to construct view from leader (maybe from a different version of zed?)"
3026 ))?;
3027 }
3028 }
3029 }
3030
3031 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
3032 }
3033
3034 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
3035 let items = futures::future::try_join_all(item_tasks).await?;
3036 this.update(cx, |this, cx| {
3037 let state = this.follower_states.get_mut(&pane)?;
3038 for (id, item) in leader_view_ids.into_iter().zip(items) {
3039 item.set_leader_peer_id(Some(leader_id), cx);
3040 state.items_by_leader_view_id.insert(id, item);
3041 }
3042
3043 Some(())
3044 })?;
3045 }
3046 Ok(())
3047 }
3048
3049 pub fn update_active_view_for_followers(&mut self, cx: &mut WindowContext) {
3050 let mut is_project_item = true;
3051 let mut update = proto::UpdateActiveView::default();
3052 if cx.is_window_active() {
3053 if let Some(item) = self.active_item(cx) {
3054 if item.focus_handle(cx).contains_focused(cx) {
3055 let leader_id = self
3056 .pane_for(&*item)
3057 .and_then(|pane| self.leader_for_pane(&pane));
3058
3059 if let Some(item) = item.to_followable_item_handle(cx) {
3060 let id = item
3061 .remote_id(&self.app_state.client, cx)
3062 .map(|id| id.to_proto());
3063
3064 if let Some(id) = id.clone() {
3065 if let Some(variant) = item.to_state_proto(cx) {
3066 let view = Some(proto::View {
3067 id: Some(id.clone()),
3068 leader_id,
3069 variant: Some(variant),
3070 });
3071
3072 is_project_item = item.is_project_item(cx);
3073 update = proto::UpdateActiveView {
3074 view,
3075 // TODO: once v0.124.0 is retired we can stop sending these
3076 id: Some(id),
3077 leader_id,
3078 };
3079 }
3080 };
3081 }
3082 }
3083 }
3084 }
3085
3086 if &update.id != &self.last_active_view_id {
3087 self.last_active_view_id = update.id.clone();
3088 self.update_followers(
3089 is_project_item,
3090 proto::update_followers::Variant::UpdateActiveView(update),
3091 cx,
3092 );
3093 }
3094 }
3095
3096 fn update_followers(
3097 &self,
3098 project_only: bool,
3099 update: proto::update_followers::Variant,
3100 cx: &mut WindowContext,
3101 ) -> Option<()> {
3102 // If this update only applies to for followers in the current project,
3103 // then skip it unless this project is shared. If it applies to all
3104 // followers, regardless of project, then set `project_id` to none,
3105 // indicating that it goes to all followers.
3106 let project_id = if project_only {
3107 Some(self.project.read(cx).remote_id()?)
3108 } else {
3109 None
3110 };
3111 self.app_state().workspace_store.update(cx, |store, cx| {
3112 store.update_followers(project_id, update, cx)
3113 })
3114 }
3115
3116 pub fn leader_for_pane(&self, pane: &View<Pane>) -> Option<PeerId> {
3117 self.follower_states.get(pane).map(|state| state.leader_id)
3118 }
3119
3120 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
3121 cx.notify();
3122
3123 let call = self.active_call()?;
3124 let room = call.read(cx).room()?.read(cx);
3125 let participant = room.remote_participant_for_peer_id(leader_id)?;
3126 let mut items_to_activate = Vec::new();
3127
3128 let leader_in_this_app;
3129 let leader_in_this_project;
3130 match participant.location {
3131 call::ParticipantLocation::SharedProject { project_id } => {
3132 leader_in_this_app = true;
3133 leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
3134 }
3135 call::ParticipantLocation::UnsharedProject => {
3136 leader_in_this_app = true;
3137 leader_in_this_project = false;
3138 }
3139 call::ParticipantLocation::External => {
3140 leader_in_this_app = false;
3141 leader_in_this_project = false;
3142 }
3143 };
3144
3145 for (pane, state) in &self.follower_states {
3146 if state.leader_id != leader_id {
3147 continue;
3148 }
3149 if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
3150 if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
3151 if leader_in_this_project || !item.is_project_item(cx) {
3152 items_to_activate.push((pane.clone(), item.boxed_clone()));
3153 }
3154 }
3155 continue;
3156 }
3157
3158 if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
3159 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
3160 }
3161 }
3162
3163 for (pane, item) in items_to_activate {
3164 let pane_was_focused = pane.read(cx).has_focus(cx);
3165 if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
3166 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
3167 } else {
3168 pane.update(cx, |pane, cx| {
3169 pane.add_item(item.boxed_clone(), false, false, None, cx)
3170 });
3171 }
3172
3173 if pane_was_focused {
3174 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
3175 }
3176 }
3177
3178 None
3179 }
3180
3181 fn shared_screen_for_peer(
3182 &self,
3183 peer_id: PeerId,
3184 pane: &View<Pane>,
3185 cx: &mut WindowContext,
3186 ) -> Option<View<SharedScreen>> {
3187 let call = self.active_call()?;
3188 let room = call.read(cx).room()?.read(cx);
3189 let participant = room.remote_participant_for_peer_id(peer_id)?;
3190 let track = participant.video_tracks.values().next()?.clone();
3191 let user = participant.user.clone();
3192
3193 for item in pane.read(cx).items_of_type::<SharedScreen>() {
3194 if item.read(cx).peer_id == peer_id {
3195 return Some(item);
3196 }
3197 }
3198
3199 Some(cx.new_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
3200 }
3201
3202 pub fn on_window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
3203 if cx.is_window_active() {
3204 self.update_active_view_for_followers(cx);
3205 cx.background_executor()
3206 .spawn(persistence::DB.update_timestamp(self.database_id()))
3207 .detach();
3208 } else {
3209 for pane in &self.panes {
3210 pane.update(cx, |pane, cx| {
3211 if let Some(item) = pane.active_item() {
3212 item.workspace_deactivated(cx);
3213 }
3214 if matches!(
3215 WorkspaceSettings::get_global(cx).autosave,
3216 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
3217 ) {
3218 for item in pane.items() {
3219 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
3220 .detach_and_log_err(cx);
3221 }
3222 }
3223 });
3224 }
3225 }
3226 }
3227
3228 fn active_call(&self) -> Option<&Model<ActiveCall>> {
3229 self.active_call.as_ref().map(|(call, _)| call)
3230 }
3231
3232 fn on_active_call_event(
3233 &mut self,
3234 _: Model<ActiveCall>,
3235 event: &call::room::Event,
3236 cx: &mut ViewContext<Self>,
3237 ) {
3238 match event {
3239 call::room::Event::ParticipantLocationChanged { participant_id }
3240 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
3241 self.leader_updated(*participant_id, cx);
3242 }
3243 _ => {}
3244 }
3245 }
3246
3247 pub fn database_id(&self) -> WorkspaceId {
3248 self.database_id
3249 }
3250
3251 fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
3252 let project = self.project().read(cx);
3253
3254 if project.is_local() {
3255 Some(
3256 project
3257 .visible_worktrees(cx)
3258 .map(|worktree| worktree.read(cx).abs_path())
3259 .collect::<Vec<_>>()
3260 .into(),
3261 )
3262 } else {
3263 None
3264 }
3265 }
3266
3267 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
3268 match member {
3269 Member::Axis(PaneAxis { members, .. }) => {
3270 for child in members.iter() {
3271 self.remove_panes(child.clone(), cx)
3272 }
3273 }
3274 Member::Pane(pane) => {
3275 self.force_remove_pane(&pane, cx);
3276 }
3277 }
3278 }
3279
3280 fn force_remove_pane(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Workspace>) {
3281 self.panes.retain(|p| p != pane);
3282 self.panes
3283 .last()
3284 .unwrap()
3285 .update(cx, |pane, cx| pane.focus(cx));
3286 if self.last_active_center_pane == Some(pane.downgrade()) {
3287 self.last_active_center_pane = None;
3288 }
3289 cx.notify();
3290 }
3291
3292 fn schedule_serialize(&mut self, cx: &mut ViewContext<Self>) {
3293 self._schedule_serialize = Some(cx.spawn(|this, mut cx| async move {
3294 cx.background_executor()
3295 .timer(Duration::from_millis(100))
3296 .await;
3297 this.update(&mut cx, |this, cx| this.serialize_workspace(cx))
3298 .log_err();
3299 }));
3300 }
3301
3302 fn serialize_workspace(&self, cx: &mut WindowContext) {
3303 fn serialize_pane_handle(pane_handle: &View<Pane>, cx: &WindowContext) -> SerializedPane {
3304 let (items, active) = {
3305 let pane = pane_handle.read(cx);
3306 let active_item_id = pane.active_item().map(|item| item.item_id());
3307 (
3308 pane.items()
3309 .filter_map(|item_handle| {
3310 Some(SerializedItem {
3311 kind: Arc::from(item_handle.serialized_item_kind()?),
3312 item_id: item_handle.item_id().as_u64(),
3313 active: Some(item_handle.item_id()) == active_item_id,
3314 })
3315 })
3316 .collect::<Vec<_>>(),
3317 pane.has_focus(cx),
3318 )
3319 };
3320
3321 SerializedPane::new(items, active)
3322 }
3323
3324 fn build_serialized_pane_group(
3325 pane_group: &Member,
3326 cx: &WindowContext,
3327 ) -> SerializedPaneGroup {
3328 match pane_group {
3329 Member::Axis(PaneAxis {
3330 axis,
3331 members,
3332 flexes,
3333 bounding_boxes: _,
3334 }) => SerializedPaneGroup::Group {
3335 axis: SerializedAxis(*axis),
3336 children: members
3337 .iter()
3338 .map(|member| build_serialized_pane_group(member, cx))
3339 .collect::<Vec<_>>(),
3340 flexes: Some(flexes.lock().clone()),
3341 },
3342 Member::Pane(pane_handle) => {
3343 SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, cx))
3344 }
3345 }
3346 }
3347
3348 fn build_serialized_docks(this: &Workspace, cx: &mut WindowContext) -> DockStructure {
3349 let left_dock = this.left_dock.read(cx);
3350 let left_visible = left_dock.is_open();
3351 let left_active_panel = left_dock
3352 .visible_panel()
3353 .and_then(|panel| Some(panel.persistent_name().to_string()));
3354 let left_dock_zoom = left_dock
3355 .visible_panel()
3356 .map(|panel| panel.is_zoomed(cx))
3357 .unwrap_or(false);
3358
3359 let right_dock = this.right_dock.read(cx);
3360 let right_visible = right_dock.is_open();
3361 let right_active_panel = right_dock
3362 .visible_panel()
3363 .and_then(|panel| Some(panel.persistent_name().to_string()));
3364 let right_dock_zoom = right_dock
3365 .visible_panel()
3366 .map(|panel| panel.is_zoomed(cx))
3367 .unwrap_or(false);
3368
3369 let bottom_dock = this.bottom_dock.read(cx);
3370 let bottom_visible = bottom_dock.is_open();
3371 let bottom_active_panel = bottom_dock
3372 .visible_panel()
3373 .and_then(|panel| Some(panel.persistent_name().to_string()));
3374 let bottom_dock_zoom = bottom_dock
3375 .visible_panel()
3376 .map(|panel| panel.is_zoomed(cx))
3377 .unwrap_or(false);
3378
3379 DockStructure {
3380 left: DockData {
3381 visible: left_visible,
3382 active_panel: left_active_panel,
3383 zoom: left_dock_zoom,
3384 },
3385 right: DockData {
3386 visible: right_visible,
3387 active_panel: right_active_panel,
3388 zoom: right_dock_zoom,
3389 },
3390 bottom: DockData {
3391 visible: bottom_visible,
3392 active_panel: bottom_active_panel,
3393 zoom: bottom_dock_zoom,
3394 },
3395 }
3396 }
3397
3398 if let Some(location) = self.location(cx) {
3399 // Load bearing special case:
3400 // - with_local_workspace() relies on this to not have other stuff open
3401 // when you open your log
3402 if !location.paths().is_empty() {
3403 let center_group = build_serialized_pane_group(&self.center.root, cx);
3404 let docks = build_serialized_docks(self, cx);
3405
3406 let serialized_workspace = SerializedWorkspace {
3407 id: self.database_id,
3408 location,
3409 center_group,
3410 bounds: Default::default(),
3411 display: Default::default(),
3412 docks,
3413 };
3414
3415 cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace))
3416 .detach();
3417 }
3418 }
3419 }
3420
3421 pub(crate) fn load_workspace(
3422 serialized_workspace: SerializedWorkspace,
3423 paths_to_open: Vec<Option<ProjectPath>>,
3424 cx: &mut ViewContext<Workspace>,
3425 ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
3426 cx.spawn(|workspace, mut cx| async move {
3427 let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
3428
3429 let mut center_group = None;
3430 let mut center_items = None;
3431
3432 // Traverse the splits tree and add to things
3433 if let Some((group, active_pane, items)) = serialized_workspace
3434 .center_group
3435 .deserialize(
3436 &project,
3437 serialized_workspace.id,
3438 workspace.clone(),
3439 &mut cx,
3440 )
3441 .await
3442 {
3443 center_items = Some(items);
3444 center_group = Some((group, active_pane))
3445 }
3446
3447 let mut items_by_project_path = cx.update(|cx| {
3448 center_items
3449 .unwrap_or_default()
3450 .into_iter()
3451 .filter_map(|item| {
3452 let item = item?;
3453 let project_path = item.project_path(cx)?;
3454 Some((project_path, item))
3455 })
3456 .collect::<HashMap<_, _>>()
3457 })?;
3458
3459 let opened_items = paths_to_open
3460 .into_iter()
3461 .map(|path_to_open| {
3462 path_to_open
3463 .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
3464 })
3465 .collect::<Vec<_>>();
3466
3467 // Remove old panes from workspace panes list
3468 workspace.update(&mut cx, |workspace, cx| {
3469 if let Some((center_group, active_pane)) = center_group {
3470 workspace.remove_panes(workspace.center.root.clone(), cx);
3471
3472 // Swap workspace center group
3473 workspace.center = PaneGroup::with_root(center_group);
3474 workspace.last_active_center_pane = active_pane.as_ref().map(|p| p.downgrade());
3475 if let Some(active_pane) = active_pane {
3476 workspace.active_pane = active_pane;
3477 cx.focus_self();
3478 } else {
3479 workspace.active_pane = workspace.center.first_pane().clone();
3480 }
3481 }
3482
3483 let docks = serialized_workspace.docks;
3484
3485 let right = docks.right.clone();
3486 workspace
3487 .right_dock
3488 .update(cx, |dock, _| dock.serialized_dock = Some(right));
3489 let left = docks.left.clone();
3490 workspace
3491 .left_dock
3492 .update(cx, |dock, _| dock.serialized_dock = Some(left));
3493 let bottom = docks.bottom.clone();
3494 workspace
3495 .bottom_dock
3496 .update(cx, |dock, _| dock.serialized_dock = Some(bottom));
3497
3498 cx.notify();
3499 })?;
3500
3501 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
3502 workspace.update(&mut cx, |workspace, cx| workspace.serialize_workspace(cx))?;
3503
3504 Ok(opened_items)
3505 })
3506 }
3507
3508 fn actions(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
3509 self.add_workspace_actions_listeners(div, cx)
3510 .on_action(cx.listener(Self::close_inactive_items_and_panes))
3511 .on_action(cx.listener(Self::close_all_items_and_panes))
3512 .on_action(cx.listener(Self::save_all))
3513 .on_action(cx.listener(Self::send_keystrokes))
3514 .on_action(cx.listener(Self::add_folder_to_project))
3515 .on_action(cx.listener(Self::follow_next_collaborator))
3516 .on_action(cx.listener(|workspace, _: &Unfollow, cx| {
3517 let pane = workspace.active_pane().clone();
3518 workspace.unfollow(&pane, cx);
3519 }))
3520 .on_action(cx.listener(|workspace, action: &Save, cx| {
3521 workspace
3522 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
3523 .detach_and_log_err(cx);
3524 }))
3525 .on_action(cx.listener(|workspace, _: &SaveAs, cx| {
3526 workspace
3527 .save_active_item(SaveIntent::SaveAs, cx)
3528 .detach_and_log_err(cx);
3529 }))
3530 .on_action(cx.listener(|workspace, _: &ActivatePreviousPane, cx| {
3531 workspace.activate_previous_pane(cx)
3532 }))
3533 .on_action(
3534 cx.listener(|workspace, _: &ActivateNextPane, cx| workspace.activate_next_pane(cx)),
3535 )
3536 .on_action(
3537 cx.listener(|workspace, action: &ActivatePaneInDirection, cx| {
3538 workspace.activate_pane_in_direction(action.0, cx)
3539 }),
3540 )
3541 .on_action(cx.listener(|workspace, action: &SwapPaneInDirection, cx| {
3542 workspace.swap_pane_in_direction(action.0, cx)
3543 }))
3544 .on_action(cx.listener(|this, _: &ToggleLeftDock, cx| {
3545 this.toggle_dock(DockPosition::Left, cx);
3546 }))
3547 .on_action(
3548 cx.listener(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
3549 workspace.toggle_dock(DockPosition::Right, cx);
3550 }),
3551 )
3552 .on_action(
3553 cx.listener(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
3554 workspace.toggle_dock(DockPosition::Bottom, cx);
3555 }),
3556 )
3557 .on_action(
3558 cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
3559 workspace.close_all_docks(cx);
3560 }),
3561 )
3562 .on_action(cx.listener(Workspace::open))
3563 .on_action(cx.listener(Workspace::close_window))
3564 .on_action(cx.listener(Workspace::activate_pane_at_index))
3565 .on_action(
3566 cx.listener(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
3567 workspace.reopen_closed_item(cx).detach();
3568 }),
3569 )
3570 .on_action(|_: &ToggleGraphicsProfiler, cx| cx.toggle_graphics_profiler())
3571 }
3572
3573 #[cfg(any(test, feature = "test-support"))]
3574 pub fn test_new(project: Model<Project>, cx: &mut ViewContext<Self>) -> Self {
3575 use node_runtime::FakeNodeRuntime;
3576
3577 let client = project.read(cx).client();
3578 let user_store = project.read(cx).user_store();
3579
3580 let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
3581 cx.activate_window();
3582 let app_state = Arc::new(AppState {
3583 languages: project.read(cx).languages().clone(),
3584 workspace_store,
3585 client,
3586 user_store,
3587 fs: project.read(cx).fs().clone(),
3588 build_window_options: |_, _, _| Default::default(),
3589 node_runtime: FakeNodeRuntime::new(),
3590 });
3591 let workspace = Self::new(0, project, app_state, cx);
3592 workspace.active_pane.update(cx, |pane, cx| pane.focus(cx));
3593 workspace
3594 }
3595
3596 pub fn register_action<A: Action>(
3597 &mut self,
3598 callback: impl Fn(&mut Self, &A, &mut ViewContext<Self>) + 'static,
3599 ) -> &mut Self {
3600 let callback = Arc::new(callback);
3601
3602 self.workspace_actions.push(Box::new(move |div, cx| {
3603 let callback = callback.clone();
3604 div.on_action(
3605 cx.listener(move |workspace, event, cx| (callback.clone())(workspace, event, cx)),
3606 )
3607 }));
3608 self
3609 }
3610
3611 fn add_workspace_actions_listeners(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
3612 let mut div = div
3613 .on_action(cx.listener(Self::close_inactive_items_and_panes))
3614 .on_action(cx.listener(Self::close_all_items_and_panes))
3615 .on_action(cx.listener(Self::add_folder_to_project))
3616 .on_action(cx.listener(Self::save_all))
3617 .on_action(cx.listener(Self::open));
3618 for action in self.workspace_actions.iter() {
3619 div = (action)(div, cx)
3620 }
3621 div
3622 }
3623
3624 pub fn has_active_modal(&self, cx: &WindowContext<'_>) -> bool {
3625 self.modal_layer.read(cx).has_active_modal()
3626 }
3627
3628 pub fn active_modal<V: ManagedView + 'static>(&mut self, cx: &AppContext) -> Option<View<V>> {
3629 self.modal_layer.read(cx).active_modal()
3630 }
3631
3632 pub fn toggle_modal<V: ModalView, B>(&mut self, cx: &mut WindowContext, build: B)
3633 where
3634 B: FnOnce(&mut ViewContext<V>) -> V,
3635 {
3636 self.modal_layer
3637 .update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build))
3638 }
3639}
3640
3641fn window_bounds_env_override(cx: &AsyncAppContext) -> Option<WindowBounds> {
3642 let display_origin = cx
3643 .update(|cx| Some(cx.displays().first()?.bounds().origin))
3644 .ok()??;
3645 ZED_WINDOW_POSITION
3646 .zip(*ZED_WINDOW_SIZE)
3647 .map(|(position, size)| {
3648 WindowBounds::Fixed(Bounds {
3649 origin: display_origin + position,
3650 size,
3651 })
3652 })
3653}
3654
3655fn open_items(
3656 serialized_workspace: Option<SerializedWorkspace>,
3657 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
3658 app_state: Arc<AppState>,
3659 cx: &mut ViewContext<Workspace>,
3660) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> {
3661 let restored_items = serialized_workspace.map(|serialized_workspace| {
3662 Workspace::load_workspace(
3663 serialized_workspace,
3664 project_paths_to_open
3665 .iter()
3666 .map(|(_, project_path)| project_path)
3667 .cloned()
3668 .collect(),
3669 cx,
3670 )
3671 });
3672
3673 cx.spawn(|workspace, mut cx| async move {
3674 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
3675
3676 if let Some(restored_items) = restored_items {
3677 let restored_items = restored_items.await?;
3678
3679 let restored_project_paths = restored_items
3680 .iter()
3681 .filter_map(|item| {
3682 cx.update(|cx| item.as_ref()?.project_path(cx))
3683 .ok()
3684 .flatten()
3685 })
3686 .collect::<HashSet<_>>();
3687
3688 for restored_item in restored_items {
3689 opened_items.push(restored_item.map(Ok));
3690 }
3691
3692 project_paths_to_open
3693 .iter_mut()
3694 .for_each(|(_, project_path)| {
3695 if let Some(project_path_to_open) = project_path {
3696 if restored_project_paths.contains(project_path_to_open) {
3697 *project_path = None;
3698 }
3699 }
3700 });
3701 } else {
3702 for _ in 0..project_paths_to_open.len() {
3703 opened_items.push(None);
3704 }
3705 }
3706 assert!(opened_items.len() == project_paths_to_open.len());
3707
3708 let tasks =
3709 project_paths_to_open
3710 .into_iter()
3711 .enumerate()
3712 .map(|(i, (abs_path, project_path))| {
3713 let workspace = workspace.clone();
3714 cx.spawn(|mut cx| {
3715 let fs = app_state.fs.clone();
3716 async move {
3717 let file_project_path = project_path?;
3718 if fs.is_file(&abs_path).await {
3719 Some((
3720 i,
3721 workspace
3722 .update(&mut cx, |workspace, cx| {
3723 workspace.open_path(file_project_path, None, true, cx)
3724 })
3725 .log_err()?
3726 .await,
3727 ))
3728 } else {
3729 None
3730 }
3731 }
3732 })
3733 });
3734
3735 let tasks = tasks.collect::<Vec<_>>();
3736
3737 let tasks = futures::future::join_all(tasks.into_iter());
3738 for maybe_opened_path in tasks.await.into_iter() {
3739 if let Some((i, path_open_result)) = maybe_opened_path {
3740 opened_items[i] = Some(path_open_result);
3741 }
3742 }
3743
3744 Ok(opened_items)
3745 })
3746}
3747
3748enum ActivateInDirectionTarget {
3749 Pane(View<Pane>),
3750 Dock(View<Dock>),
3751}
3752
3753fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncAppContext) {
3754 const REPORT_ISSUE_URL: &str = "https://github.com/zed-industries/zed/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
3755
3756 workspace
3757 .update(cx, |workspace, cx| {
3758 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
3759 workspace.show_notification_once(0, cx, |cx| {
3760 cx.new_view(|_| {
3761 MessageNotification::new("Failed to load the database file.")
3762 .with_click_message("Click to let us know about this error")
3763 .on_click(|cx| cx.open_url(REPORT_ISSUE_URL))
3764 })
3765 });
3766 }
3767 })
3768 .log_err();
3769}
3770
3771impl FocusableView for Workspace {
3772 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
3773 self.active_pane.focus_handle(cx)
3774 }
3775}
3776
3777#[derive(Clone, Render)]
3778struct DraggedDock(DockPosition);
3779
3780impl Render for Workspace {
3781 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3782 let mut context = KeyContext::default();
3783 context.add("Workspace");
3784
3785 let (ui_font, ui_font_size) = {
3786 let theme_settings = ThemeSettings::get_global(cx);
3787 (
3788 theme_settings.ui_font.family.clone(),
3789 theme_settings.ui_font_size.clone(),
3790 )
3791 };
3792
3793 let theme = cx.theme().clone();
3794 let colors = theme.colors();
3795 cx.set_rem_size(ui_font_size);
3796
3797 self.actions(div(), cx)
3798 .key_context(context)
3799 .relative()
3800 .size_full()
3801 .flex()
3802 .flex_col()
3803 .font(ui_font)
3804 .gap_0()
3805 .justify_start()
3806 .items_start()
3807 .text_color(colors.text)
3808 .bg(colors.background)
3809 .border()
3810 .border_color(colors.border)
3811 .children(self.titlebar_item.clone())
3812 .child(
3813 div()
3814 .id("workspace")
3815 .relative()
3816 .flex_1()
3817 .w_full()
3818 .flex()
3819 .flex_col()
3820 .overflow_hidden()
3821 .border_t()
3822 .border_b()
3823 .border_color(colors.border)
3824 .child(
3825 canvas({
3826 let this = cx.view().clone();
3827 move |bounds, cx| {
3828 this.update(cx, |this, _cx| {
3829 this.bounds = *bounds;
3830 })
3831 }
3832 })
3833 .absolute()
3834 .size_full(),
3835 )
3836 .on_drag_move(
3837 cx.listener(|workspace, e: &DragMoveEvent<DraggedDock>, cx| {
3838 match e.drag(cx).0 {
3839 DockPosition::Left => {
3840 let size = workspace.bounds.left() + e.event.position.x;
3841 workspace.left_dock.update(cx, |left_dock, cx| {
3842 left_dock.resize_active_panel(Some(size), cx);
3843 });
3844 }
3845 DockPosition::Right => {
3846 let size = workspace.bounds.right() - e.event.position.x;
3847 workspace.right_dock.update(cx, |right_dock, cx| {
3848 right_dock.resize_active_panel(Some(size), cx);
3849 });
3850 }
3851 DockPosition::Bottom => {
3852 let size = workspace.bounds.bottom() - e.event.position.y;
3853 workspace.bottom_dock.update(cx, |bottom_dock, cx| {
3854 bottom_dock.resize_active_panel(Some(size), cx);
3855 });
3856 }
3857 }
3858 }),
3859 )
3860 .child(self.modal_layer.clone())
3861 .child(
3862 div()
3863 .flex()
3864 .flex_row()
3865 .h_full()
3866 // Left Dock
3867 .children(self.zoomed_position.ne(&Some(DockPosition::Left)).then(
3868 || {
3869 div()
3870 .flex()
3871 .flex_none()
3872 .overflow_hidden()
3873 .child(self.left_dock.clone())
3874 },
3875 ))
3876 // Panes
3877 .child(
3878 div()
3879 .flex()
3880 .flex_col()
3881 .flex_1()
3882 .overflow_hidden()
3883 .child(self.center.render(
3884 &self.project,
3885 &self.follower_states,
3886 self.active_call(),
3887 &self.active_pane,
3888 self.zoomed.as_ref(),
3889 &self.app_state,
3890 cx,
3891 ))
3892 .children(
3893 self.zoomed_position
3894 .ne(&Some(DockPosition::Bottom))
3895 .then(|| self.bottom_dock.clone()),
3896 ),
3897 )
3898 // Right Dock
3899 .children(self.zoomed_position.ne(&Some(DockPosition::Right)).then(
3900 || {
3901 div()
3902 .flex()
3903 .flex_none()
3904 .overflow_hidden()
3905 .child(self.right_dock.clone())
3906 },
3907 )),
3908 )
3909 .children(self.render_notifications(cx))
3910 .children(self.zoomed.as_ref().and_then(|view| {
3911 let zoomed_view = view.upgrade()?;
3912 let div = div()
3913 .z_index(1)
3914 .absolute()
3915 .overflow_hidden()
3916 .border_color(colors.border)
3917 .bg(colors.background)
3918 .child(zoomed_view)
3919 .inset_0()
3920 .shadow_lg();
3921
3922 Some(match self.zoomed_position {
3923 Some(DockPosition::Left) => div.right_2().border_r(),
3924 Some(DockPosition::Right) => div.left_2().border_l(),
3925 Some(DockPosition::Bottom) => div.top_2().border_t(),
3926 None => div.top_2().bottom_2().left_2().right_2().border(),
3927 })
3928 })),
3929 )
3930 .child(self.status_bar.clone())
3931 .children(if self.project.read(cx).is_disconnected() {
3932 Some(DisconnectedOverlay)
3933 } else {
3934 None
3935 })
3936 }
3937}
3938
3939impl WorkspaceStore {
3940 pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
3941 Self {
3942 workspaces: Default::default(),
3943 _subscriptions: vec![
3944 client.add_request_handler(cx.weak_model(), Self::handle_follow),
3945 client.add_message_handler(cx.weak_model(), Self::handle_update_followers),
3946 ],
3947 client,
3948 }
3949 }
3950
3951 pub fn update_followers(
3952 &self,
3953 project_id: Option<u64>,
3954 update: proto::update_followers::Variant,
3955 cx: &AppContext,
3956 ) -> Option<()> {
3957 let active_call = ActiveCall::try_global(cx)?;
3958 let room_id = active_call.read(cx).room()?.read(cx).id();
3959 self.client
3960 .send(proto::UpdateFollowers {
3961 room_id,
3962 project_id,
3963 variant: Some(update),
3964 })
3965 .log_err()
3966 }
3967
3968 pub async fn handle_follow(
3969 this: Model<Self>,
3970 envelope: TypedEnvelope<proto::Follow>,
3971 _: Arc<Client>,
3972 mut cx: AsyncAppContext,
3973 ) -> Result<proto::FollowResponse> {
3974 this.update(&mut cx, |this, cx| {
3975 let follower = Follower {
3976 project_id: envelope.payload.project_id,
3977 peer_id: envelope.original_sender_id()?,
3978 };
3979 let active_project = ActiveCall::global(cx).read(cx).location().cloned();
3980
3981 let mut response = proto::FollowResponse::default();
3982 this.workspaces.retain(|workspace| {
3983 workspace
3984 .update(cx, |workspace, cx| {
3985 let handler_response = workspace.handle_follow(follower.project_id, cx);
3986 if response.views.is_empty() {
3987 response.views = handler_response.views;
3988 } else {
3989 response.views.extend_from_slice(&handler_response.views);
3990 }
3991
3992 if let Some(active_view_id) = handler_response.active_view_id.clone() {
3993 if response.active_view_id.is_none()
3994 || Some(workspace.project.downgrade()) == active_project
3995 {
3996 response.active_view_id = Some(active_view_id);
3997 }
3998 }
3999
4000 if let Some(active_view) = handler_response.active_view.clone() {
4001 if workspace.project.read(cx).remote_id() == follower.project_id {
4002 response.active_view = Some(active_view)
4003 }
4004 }
4005 })
4006 .is_ok()
4007 });
4008
4009 Ok(response)
4010 })?
4011 }
4012
4013 async fn handle_update_followers(
4014 this: Model<Self>,
4015 envelope: TypedEnvelope<proto::UpdateFollowers>,
4016 _: Arc<Client>,
4017 mut cx: AsyncAppContext,
4018 ) -> Result<()> {
4019 let leader_id = envelope.original_sender_id()?;
4020 let update = envelope.payload;
4021
4022 this.update(&mut cx, |this, cx| {
4023 this.workspaces.retain(|workspace| {
4024 workspace
4025 .update(cx, |workspace, cx| {
4026 let project_id = workspace.project.read(cx).remote_id();
4027 if update.project_id != project_id && update.project_id.is_some() {
4028 return;
4029 }
4030 workspace.handle_update_followers(leader_id, update.clone(), cx);
4031 })
4032 .is_ok()
4033 });
4034 Ok(())
4035 })?
4036 }
4037}
4038
4039impl ViewId {
4040 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
4041 Ok(Self {
4042 creator: message
4043 .creator
4044 .ok_or_else(|| anyhow!("creator is missing"))?,
4045 id: message.id,
4046 })
4047 }
4048
4049 pub(crate) fn to_proto(&self) -> proto::ViewId {
4050 proto::ViewId {
4051 creator: Some(self.creator),
4052 id: self.id,
4053 }
4054 }
4055}
4056
4057pub trait WorkspaceHandle {
4058 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
4059}
4060
4061impl WorkspaceHandle for View<Workspace> {
4062 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
4063 self.read(cx)
4064 .worktrees(cx)
4065 .flat_map(|worktree| {
4066 let worktree_id = worktree.read(cx).id();
4067 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
4068 worktree_id,
4069 path: f.path.clone(),
4070 })
4071 })
4072 .collect::<Vec<_>>()
4073 }
4074}
4075
4076impl std::fmt::Debug for OpenPaths {
4077 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4078 f.debug_struct("OpenPaths")
4079 .field("paths", &self.paths)
4080 .finish()
4081 }
4082}
4083
4084pub fn activate_workspace_for_project(
4085 cx: &mut AppContext,
4086 predicate: impl Fn(&Project, &AppContext) -> bool + Send + 'static,
4087) -> Option<WindowHandle<Workspace>> {
4088 for window in cx.windows() {
4089 let Some(workspace) = window.downcast::<Workspace>() else {
4090 continue;
4091 };
4092
4093 let predicate = workspace
4094 .update(cx, |workspace, cx| {
4095 let project = workspace.project.read(cx);
4096 if predicate(project, cx) {
4097 cx.activate_window();
4098 true
4099 } else {
4100 false
4101 }
4102 })
4103 .log_err()
4104 .unwrap_or(false);
4105
4106 if predicate {
4107 return Some(workspace);
4108 }
4109 }
4110
4111 None
4112}
4113
4114pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
4115 DB.last_workspace().await.log_err().flatten()
4116}
4117
4118actions!(collab, [OpenChannelNotes]);
4119
4120async fn join_channel_internal(
4121 channel_id: u64,
4122 app_state: &Arc<AppState>,
4123 requesting_window: Option<WindowHandle<Workspace>>,
4124 active_call: &Model<ActiveCall>,
4125 cx: &mut AsyncAppContext,
4126) -> Result<bool> {
4127 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
4128 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
4129 return (false, None);
4130 };
4131
4132 let already_in_channel = room.channel_id() == Some(channel_id);
4133 let should_prompt = room.is_sharing_project()
4134 && room.remote_participants().len() > 0
4135 && !already_in_channel;
4136 let open_room = if already_in_channel {
4137 active_call.room().cloned()
4138 } else {
4139 None
4140 };
4141 (should_prompt, open_room)
4142 })?;
4143
4144 if let Some(room) = open_room {
4145 let task = room.update(cx, |room, cx| {
4146 if let Some((project, host)) = room.most_active_project(cx) {
4147 return Some(join_remote_project(project, host, app_state.clone(), cx));
4148 }
4149
4150 None
4151 })?;
4152 if let Some(task) = task {
4153 task.await?;
4154 }
4155 return anyhow::Ok(true);
4156 }
4157
4158 if should_prompt {
4159 if let Some(workspace) = requesting_window {
4160 let answer = workspace
4161 .update(cx, |_, cx| {
4162 cx.prompt(
4163 PromptLevel::Warning,
4164 "Do you want to switch channels?",
4165 Some("Leaving this call will unshare your current project."),
4166 &["Yes, Join Channel", "Cancel"],
4167 )
4168 })?
4169 .await;
4170
4171 if answer == Ok(1) {
4172 return Ok(false);
4173 }
4174 } else {
4175 return Ok(false); // unreachable!() hopefully
4176 }
4177 }
4178
4179 let client = cx.update(|cx| active_call.read(cx).client())?;
4180
4181 let mut client_status = client.status();
4182
4183 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
4184 'outer: loop {
4185 let Some(status) = client_status.recv().await else {
4186 return Err(anyhow!("error connecting"));
4187 };
4188
4189 match status {
4190 Status::Connecting
4191 | Status::Authenticating
4192 | Status::Reconnecting
4193 | Status::Reauthenticating => continue,
4194 Status::Connected { .. } => break 'outer,
4195 Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
4196 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
4197 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
4198 return Err(ErrorCode::Disconnected.into());
4199 }
4200 }
4201 }
4202
4203 let room = active_call
4204 .update(cx, |active_call, cx| {
4205 active_call.join_channel(channel_id, cx)
4206 })?
4207 .await?;
4208
4209 let Some(room) = room else {
4210 return anyhow::Ok(true);
4211 };
4212
4213 room.update(cx, |room, _| room.room_update_completed())?
4214 .await;
4215
4216 let task = room.update(cx, |room, cx| {
4217 if let Some((project, host)) = room.most_active_project(cx) {
4218 return Some(join_remote_project(project, host, app_state.clone(), cx));
4219 }
4220
4221 // if you are the first to join a channel, share your project
4222 if room.remote_participants().len() == 0 && !room.local_participant_is_guest() {
4223 if let Some(workspace) = requesting_window {
4224 let project = workspace.update(cx, |workspace, cx| {
4225 if !CallSettings::get_global(cx).share_on_join {
4226 return None;
4227 }
4228 let project = workspace.project.read(cx);
4229 if project.is_local()
4230 && project.visible_worktrees(cx).any(|tree| {
4231 tree.read(cx)
4232 .root_entry()
4233 .map_or(false, |entry| entry.is_dir())
4234 })
4235 {
4236 Some(workspace.project.clone())
4237 } else {
4238 None
4239 }
4240 });
4241 if let Ok(Some(project)) = project {
4242 return Some(cx.spawn(|room, mut cx| async move {
4243 room.update(&mut cx, |room, cx| room.share_project(project, cx))?
4244 .await?;
4245 Ok(())
4246 }));
4247 }
4248 }
4249 }
4250
4251 None
4252 })?;
4253 if let Some(task) = task {
4254 task.await?;
4255 return anyhow::Ok(true);
4256 }
4257 anyhow::Ok(false)
4258}
4259
4260pub fn join_channel(
4261 channel_id: u64,
4262 app_state: Arc<AppState>,
4263 requesting_window: Option<WindowHandle<Workspace>>,
4264 cx: &mut AppContext,
4265) -> Task<Result<()>> {
4266 let active_call = ActiveCall::global(cx);
4267 cx.spawn(|mut cx| async move {
4268 let result = join_channel_internal(
4269 channel_id,
4270 &app_state,
4271 requesting_window,
4272 &active_call,
4273 &mut cx,
4274 )
4275 .await;
4276
4277 // join channel succeeded, and opened a window
4278 if matches!(result, Ok(true)) {
4279 return anyhow::Ok(());
4280 }
4281
4282 // find an existing workspace to focus and show call controls
4283 let mut active_window =
4284 requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
4285 if active_window.is_none() {
4286 // no open workspaces, make one to show the error in (blergh)
4287 let (window_handle, _) = cx
4288 .update(|cx| {
4289 Workspace::new_local(vec![], app_state.clone(), requesting_window, cx)
4290 })?
4291 .await?;
4292
4293 if result.is_ok() {
4294 cx.update(|cx| {
4295 cx.dispatch_action(&OpenChannelNotes);
4296 }).log_err();
4297 }
4298
4299 active_window = Some(window_handle);
4300 }
4301
4302 if let Err(err) = result {
4303 log::error!("failed to join channel: {}", err);
4304 if let Some(active_window) = active_window {
4305 active_window
4306 .update(&mut cx, |_, cx| {
4307 let detail: SharedString = match err.error_code() {
4308 ErrorCode::SignedOut => {
4309 "Please sign in to continue.".into()
4310 }
4311 ErrorCode::UpgradeRequired => {
4312 "Your are running an unsupported version of Zed. Please update to continue.".into()
4313 }
4314 ErrorCode::NoSuchChannel => {
4315 "No matching channel was found. Please check the link and try again.".into()
4316 }
4317 ErrorCode::Forbidden => {
4318 "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
4319 }
4320 ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
4321 _ => format!("{}\n\nPlease try again.", err).into(),
4322 };
4323 cx.prompt(
4324 PromptLevel::Critical,
4325 "Failed to join channel",
4326 Some(&detail),
4327 &["Ok"],
4328 )
4329 })?
4330 .await
4331 .ok();
4332 }
4333 }
4334
4335 // return ok, we showed the error to the user.
4336 return anyhow::Ok(());
4337 })
4338}
4339
4340pub async fn get_any_active_workspace(
4341 app_state: Arc<AppState>,
4342 mut cx: AsyncAppContext,
4343) -> anyhow::Result<WindowHandle<Workspace>> {
4344 // find an existing workspace to focus and show call controls
4345 let active_window = activate_any_workspace_window(&mut cx);
4346 if active_window.is_none() {
4347 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, cx))?
4348 .await?;
4349 }
4350 activate_any_workspace_window(&mut cx).context("could not open zed")
4351}
4352
4353fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandle<Workspace>> {
4354 cx.update(|cx| {
4355 for window in cx.windows() {
4356 if let Some(workspace_window) = window.downcast::<Workspace>() {
4357 workspace_window
4358 .update(cx, |_, cx| cx.activate_window())
4359 .ok();
4360 return Some(workspace_window);
4361 }
4362 }
4363 None
4364 })
4365 .ok()
4366 .flatten()
4367}
4368
4369#[allow(clippy::type_complexity)]
4370pub fn open_paths(
4371 abs_paths: &[PathBuf],
4372 app_state: &Arc<AppState>,
4373 requesting_window: Option<WindowHandle<Workspace>>,
4374 cx: &mut AppContext,
4375) -> Task<
4376 anyhow::Result<(
4377 WindowHandle<Workspace>,
4378 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
4379 )>,
4380> {
4381 let app_state = app_state.clone();
4382 let abs_paths = abs_paths.to_vec();
4383 // Open paths in existing workspace if possible
4384 let existing = activate_workspace_for_project(cx, {
4385 let abs_paths = abs_paths.clone();
4386 move |project, cx| project.contains_paths(&abs_paths, cx)
4387 });
4388 cx.spawn(move |mut cx| async move {
4389 if let Some(existing) = existing {
4390 Ok((
4391 existing.clone(),
4392 existing
4393 .update(&mut cx, |workspace, cx| {
4394 workspace.open_paths(abs_paths, OpenVisible::All, None, cx)
4395 })?
4396 .await,
4397 ))
4398 } else {
4399 cx.update(move |cx| {
4400 Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx)
4401 })?
4402 .await
4403 }
4404 })
4405}
4406
4407pub fn open_new(
4408 app_state: &Arc<AppState>,
4409 cx: &mut AppContext,
4410 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static + Send,
4411) -> Task<()> {
4412 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
4413 cx.spawn(|mut cx| async move {
4414 if let Some((workspace, opened_paths)) = task.await.log_err() {
4415 workspace
4416 .update(&mut cx, |workspace, cx| {
4417 if opened_paths.is_empty() {
4418 init(workspace, cx)
4419 }
4420 })
4421 .log_err();
4422 }
4423 })
4424}
4425
4426pub fn create_and_open_local_file(
4427 path: &'static Path,
4428 cx: &mut ViewContext<Workspace>,
4429 default_content: impl 'static + Send + FnOnce() -> Rope,
4430) -> Task<Result<Box<dyn ItemHandle>>> {
4431 cx.spawn(|workspace, mut cx| async move {
4432 let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
4433 if !fs.is_file(path).await {
4434 fs.create_file(path, Default::default()).await?;
4435 fs.save(path, &default_content(), Default::default())
4436 .await?;
4437 }
4438
4439 let mut items = workspace
4440 .update(&mut cx, |workspace, cx| {
4441 workspace.with_local_workspace(cx, |workspace, cx| {
4442 workspace.open_paths(vec![path.to_path_buf()], OpenVisible::None, None, cx)
4443 })
4444 })?
4445 .await?
4446 .await;
4447
4448 let item = items.pop().flatten();
4449 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
4450 })
4451}
4452
4453pub fn join_remote_project(
4454 project_id: u64,
4455 follow_user_id: u64,
4456 app_state: Arc<AppState>,
4457 cx: &mut AppContext,
4458) -> Task<Result<()>> {
4459 let windows = cx.windows();
4460 cx.spawn(|mut cx| async move {
4461 let existing_workspace = windows.into_iter().find_map(|window| {
4462 window.downcast::<Workspace>().and_then(|window| {
4463 window
4464 .update(&mut cx, |workspace, cx| {
4465 if workspace.project().read(cx).remote_id() == Some(project_id) {
4466 Some(window)
4467 } else {
4468 None
4469 }
4470 })
4471 .unwrap_or(None)
4472 })
4473 });
4474
4475 let workspace = if let Some(existing_workspace) = existing_workspace {
4476 existing_workspace
4477 } else {
4478 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
4479 let room = active_call
4480 .read_with(&cx, |call, _| call.room().cloned())?
4481 .ok_or_else(|| anyhow!("not in a call"))?;
4482 let project = room
4483 .update(&mut cx, |room, cx| {
4484 room.join_project(
4485 project_id,
4486 app_state.languages.clone(),
4487 app_state.fs.clone(),
4488 cx,
4489 )
4490 })?
4491 .await?;
4492
4493 let window_bounds_override = window_bounds_env_override(&cx);
4494 cx.update(|cx| {
4495 let options = (app_state.build_window_options)(window_bounds_override, None, cx);
4496 cx.open_window(options, |cx| {
4497 cx.new_view(|cx| Workspace::new(0, project, app_state.clone(), cx))
4498 })
4499 })?
4500 };
4501
4502 workspace.update(&mut cx, |workspace, cx| {
4503 cx.activate(true);
4504 cx.activate_window();
4505
4506 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
4507 let follow_peer_id = room
4508 .read(cx)
4509 .remote_participants()
4510 .iter()
4511 .find(|(_, participant)| participant.user.id == follow_user_id)
4512 .map(|(_, p)| p.peer_id)
4513 .or_else(|| {
4514 // If we couldn't follow the given user, follow the host instead.
4515 let collaborator = workspace
4516 .project()
4517 .read(cx)
4518 .collaborators()
4519 .values()
4520 .find(|collaborator| collaborator.replica_id == 0)?;
4521 Some(collaborator.peer_id)
4522 });
4523
4524 if let Some(follow_peer_id) = follow_peer_id {
4525 workspace.follow(follow_peer_id, cx);
4526 }
4527 }
4528 })?;
4529
4530 anyhow::Ok(())
4531 })
4532}
4533
4534pub fn restart(_: &Restart, cx: &mut AppContext) {
4535 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
4536 let mut workspace_windows = cx
4537 .windows()
4538 .into_iter()
4539 .filter_map(|window| window.downcast::<Workspace>())
4540 .collect::<Vec<_>>();
4541
4542 // If multiple windows have unsaved changes, and need a save prompt,
4543 // prompt in the active window before switching to a different window.
4544 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
4545
4546 let mut prompt = None;
4547 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
4548 prompt = window
4549 .update(cx, |_, cx| {
4550 cx.prompt(
4551 PromptLevel::Info,
4552 "Are you sure you want to restart?",
4553 None,
4554 &["Restart", "Cancel"],
4555 )
4556 })
4557 .ok();
4558 }
4559
4560 cx.spawn(|mut cx| async move {
4561 if let Some(prompt) = prompt {
4562 let answer = prompt.await?;
4563 if answer != 0 {
4564 return Ok(());
4565 }
4566 }
4567
4568 // If the user cancels any save prompt, then keep the app open.
4569 for window in workspace_windows {
4570 if let Ok(should_close) = window.update(&mut cx, |workspace, cx| {
4571 workspace.prepare_to_close(true, cx)
4572 }) {
4573 if !should_close.await? {
4574 return Ok(());
4575 }
4576 }
4577 }
4578
4579 cx.update(|cx| cx.restart())
4580 })
4581 .detach_and_log_err(cx);
4582}
4583
4584fn parse_pixel_position_env_var(value: &str) -> Option<Point<GlobalPixels>> {
4585 let mut parts = value.split(',');
4586 let x: usize = parts.next()?.parse().ok()?;
4587 let y: usize = parts.next()?.parse().ok()?;
4588 Some(point((x as f64).into(), (y as f64).into()))
4589}
4590
4591fn parse_pixel_size_env_var(value: &str) -> Option<Size<GlobalPixels>> {
4592 let mut parts = value.split(',');
4593 let width: usize = parts.next()?.parse().ok()?;
4594 let height: usize = parts.next()?.parse().ok()?;
4595 Some(size((width as f64).into(), (height as f64).into()))
4596}
4597
4598pub fn titlebar_height(cx: &mut WindowContext) -> Pixels {
4599 (1.75 * cx.rem_size()).max(px(32.))
4600}
4601
4602struct DisconnectedOverlay;
4603
4604impl Element for DisconnectedOverlay {
4605 type State = AnyElement;
4606
4607 fn request_layout(
4608 &mut self,
4609 _: Option<Self::State>,
4610 cx: &mut ElementContext,
4611 ) -> (LayoutId, Self::State) {
4612 let mut background = cx.theme().colors().elevated_surface_background;
4613 background.fade_out(0.2);
4614 let mut overlay = div()
4615 .bg(background)
4616 .absolute()
4617 .left_0()
4618 .top(titlebar_height(cx))
4619 .size_full()
4620 .flex()
4621 .items_center()
4622 .justify_center()
4623 .capture_any_mouse_down(|_, cx| cx.stop_propagation())
4624 .capture_any_mouse_up(|_, cx| cx.stop_propagation())
4625 .child(Label::new(
4626 "Your connection to the remote project has been lost.",
4627 ))
4628 .into_any();
4629 (overlay.request_layout(cx), overlay)
4630 }
4631
4632 fn paint(
4633 &mut self,
4634 bounds: Bounds<Pixels>,
4635 overlay: &mut Self::State,
4636 cx: &mut ElementContext,
4637 ) {
4638 cx.with_z_index(u16::MAX, |cx| {
4639 cx.add_opaque_layer(bounds);
4640 overlay.paint(cx);
4641 })
4642 }
4643}
4644
4645impl IntoElement for DisconnectedOverlay {
4646 type Element = Self;
4647
4648 fn element_id(&self) -> Option<ui::prelude::ElementId> {
4649 None
4650 }
4651
4652 fn into_element(self) -> Self::Element {
4653 self
4654 }
4655}
4656
4657#[cfg(test)]
4658mod tests {
4659 use std::{cell::RefCell, rc::Rc};
4660
4661 use super::*;
4662 use crate::{
4663 dock::{test::TestPanel, PanelEvent},
4664 item::{
4665 test::{TestItem, TestProjectItem},
4666 ItemEvent,
4667 },
4668 };
4669 use fs::FakeFs;
4670 use gpui::{px, DismissEvent, TestAppContext, VisualTestContext};
4671 use project::{Project, ProjectEntryId};
4672 use serde_json::json;
4673 use settings::SettingsStore;
4674
4675 #[gpui::test]
4676 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
4677 init_test(cx);
4678
4679 let fs = FakeFs::new(cx.executor());
4680 let project = Project::test(fs, [], cx).await;
4681 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4682
4683 // Adding an item with no ambiguity renders the tab without detail.
4684 let item1 = cx.new_view(|cx| {
4685 let mut item = TestItem::new(cx);
4686 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
4687 item
4688 });
4689 workspace.update(cx, |workspace, cx| {
4690 workspace.add_item(Box::new(item1.clone()), cx);
4691 });
4692 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
4693
4694 // Adding an item that creates ambiguity increases the level of detail on
4695 // both tabs.
4696 let item2 = cx.new_view(|cx| {
4697 let mut item = TestItem::new(cx);
4698 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4699 item
4700 });
4701 workspace.update(cx, |workspace, cx| {
4702 workspace.add_item(Box::new(item2.clone()), cx);
4703 });
4704 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4705 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4706
4707 // Adding an item that creates ambiguity increases the level of detail only
4708 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
4709 // we stop at the highest detail available.
4710 let item3 = cx.new_view(|cx| {
4711 let mut item = TestItem::new(cx);
4712 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4713 item
4714 });
4715 workspace.update(cx, |workspace, cx| {
4716 workspace.add_item(Box::new(item3.clone()), cx);
4717 });
4718 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4719 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4720 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4721 }
4722
4723 #[gpui::test]
4724 async fn test_tracking_active_path(cx: &mut TestAppContext) {
4725 init_test(cx);
4726
4727 let fs = FakeFs::new(cx.executor());
4728 fs.insert_tree(
4729 "/root1",
4730 json!({
4731 "one.txt": "",
4732 "two.txt": "",
4733 }),
4734 )
4735 .await;
4736 fs.insert_tree(
4737 "/root2",
4738 json!({
4739 "three.txt": "",
4740 }),
4741 )
4742 .await;
4743
4744 let project = Project::test(fs, ["root1".as_ref()], cx).await;
4745 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4746 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4747 let worktree_id = project.update(cx, |project, cx| {
4748 project.worktrees().next().unwrap().read(cx).id()
4749 });
4750
4751 let item1 = cx.new_view(|cx| {
4752 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
4753 });
4754 let item2 = cx.new_view(|cx| {
4755 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
4756 });
4757
4758 // Add an item to an empty pane
4759 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
4760 project.update(cx, |project, cx| {
4761 assert_eq!(
4762 project.active_entry(),
4763 project
4764 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4765 .map(|e| e.id)
4766 );
4767 });
4768 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
4769
4770 // Add a second item to a non-empty pane
4771 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
4772 assert_eq!(cx.window_title().as_deref(), Some("two.txt — root1"));
4773 project.update(cx, |project, cx| {
4774 assert_eq!(
4775 project.active_entry(),
4776 project
4777 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
4778 .map(|e| e.id)
4779 );
4780 });
4781
4782 // Close the active item
4783 pane.update(cx, |pane, cx| {
4784 pane.close_active_item(&Default::default(), cx).unwrap()
4785 })
4786 .await
4787 .unwrap();
4788 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
4789 project.update(cx, |project, cx| {
4790 assert_eq!(
4791 project.active_entry(),
4792 project
4793 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4794 .map(|e| e.id)
4795 );
4796 });
4797
4798 // Add a project folder
4799 project
4800 .update(cx, |project, cx| {
4801 project.find_or_create_local_worktree("/root2", true, cx)
4802 })
4803 .await
4804 .unwrap();
4805 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1, root2"));
4806
4807 // Remove a project folder
4808 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
4809 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root2"));
4810 }
4811
4812 #[gpui::test]
4813 async fn test_close_window(cx: &mut TestAppContext) {
4814 init_test(cx);
4815
4816 let fs = FakeFs::new(cx.executor());
4817 fs.insert_tree("/root", json!({ "one": "" })).await;
4818
4819 let project = Project::test(fs, ["root".as_ref()], cx).await;
4820 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4821
4822 // When there are no dirty items, there's nothing to do.
4823 let item1 = cx.new_view(|cx| TestItem::new(cx));
4824 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
4825 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4826 assert!(task.await.unwrap());
4827
4828 // When there are dirty untitled items, prompt to save each one. If the user
4829 // cancels any prompt, then abort.
4830 let item2 = cx.new_view(|cx| TestItem::new(cx).with_dirty(true));
4831 let item3 = cx.new_view(|cx| {
4832 TestItem::new(cx)
4833 .with_dirty(true)
4834 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4835 });
4836 workspace.update(cx, |w, cx| {
4837 w.add_item(Box::new(item2.clone()), cx);
4838 w.add_item(Box::new(item3.clone()), cx);
4839 });
4840 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4841 cx.executor().run_until_parked();
4842 cx.simulate_prompt_answer(2); // cancel save all
4843 cx.executor().run_until_parked();
4844 cx.simulate_prompt_answer(2); // cancel save all
4845 cx.executor().run_until_parked();
4846 assert!(!cx.has_pending_prompt());
4847 assert!(!task.await.unwrap());
4848 }
4849
4850 #[gpui::test]
4851 async fn test_close_pane_items(cx: &mut TestAppContext) {
4852 init_test(cx);
4853
4854 let fs = FakeFs::new(cx.executor());
4855
4856 let project = Project::test(fs, None, cx).await;
4857 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4858
4859 let item1 = cx.new_view(|cx| {
4860 TestItem::new(cx)
4861 .with_dirty(true)
4862 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4863 });
4864 let item2 = cx.new_view(|cx| {
4865 TestItem::new(cx)
4866 .with_dirty(true)
4867 .with_conflict(true)
4868 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
4869 });
4870 let item3 = cx.new_view(|cx| {
4871 TestItem::new(cx)
4872 .with_dirty(true)
4873 .with_conflict(true)
4874 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
4875 });
4876 let item4 = cx.new_view(|cx| {
4877 TestItem::new(cx)
4878 .with_dirty(true)
4879 .with_project_items(&[TestProjectItem::new_untitled(cx)])
4880 });
4881 let pane = workspace.update(cx, |workspace, cx| {
4882 workspace.add_item(Box::new(item1.clone()), cx);
4883 workspace.add_item(Box::new(item2.clone()), cx);
4884 workspace.add_item(Box::new(item3.clone()), cx);
4885 workspace.add_item(Box::new(item4.clone()), cx);
4886 workspace.active_pane().clone()
4887 });
4888
4889 let close_items = pane.update(cx, |pane, cx| {
4890 pane.activate_item(1, true, true, cx);
4891 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
4892 let item1_id = item1.item_id();
4893 let item3_id = item3.item_id();
4894 let item4_id = item4.item_id();
4895 pane.close_items(cx, SaveIntent::Close, move |id| {
4896 [item1_id, item3_id, item4_id].contains(&id)
4897 })
4898 });
4899 cx.executor().run_until_parked();
4900
4901 assert!(cx.has_pending_prompt());
4902 // Ignore "Save all" prompt
4903 cx.simulate_prompt_answer(2);
4904 cx.executor().run_until_parked();
4905 // There's a prompt to save item 1.
4906 pane.update(cx, |pane, _| {
4907 assert_eq!(pane.items_len(), 4);
4908 assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
4909 });
4910 // Confirm saving item 1.
4911 cx.simulate_prompt_answer(0);
4912 cx.executor().run_until_parked();
4913
4914 // Item 1 is saved. There's a prompt to save item 3.
4915 pane.update(cx, |pane, cx| {
4916 assert_eq!(item1.read(cx).save_count, 1);
4917 assert_eq!(item1.read(cx).save_as_count, 0);
4918 assert_eq!(item1.read(cx).reload_count, 0);
4919 assert_eq!(pane.items_len(), 3);
4920 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
4921 });
4922 assert!(cx.has_pending_prompt());
4923
4924 // Cancel saving item 3.
4925 cx.simulate_prompt_answer(1);
4926 cx.executor().run_until_parked();
4927
4928 // Item 3 is reloaded. There's a prompt to save item 4.
4929 pane.update(cx, |pane, cx| {
4930 assert_eq!(item3.read(cx).save_count, 0);
4931 assert_eq!(item3.read(cx).save_as_count, 0);
4932 assert_eq!(item3.read(cx).reload_count, 1);
4933 assert_eq!(pane.items_len(), 2);
4934 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
4935 });
4936 assert!(cx.has_pending_prompt());
4937
4938 // Confirm saving item 4.
4939 cx.simulate_prompt_answer(0);
4940 cx.executor().run_until_parked();
4941
4942 // There's a prompt for a path for item 4.
4943 cx.simulate_new_path_selection(|_| Some(Default::default()));
4944 close_items.await.unwrap();
4945
4946 // The requested items are closed.
4947 pane.update(cx, |pane, cx| {
4948 assert_eq!(item4.read(cx).save_count, 0);
4949 assert_eq!(item4.read(cx).save_as_count, 1);
4950 assert_eq!(item4.read(cx).reload_count, 0);
4951 assert_eq!(pane.items_len(), 1);
4952 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
4953 });
4954 }
4955
4956 #[gpui::test]
4957 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
4958 init_test(cx);
4959
4960 let fs = FakeFs::new(cx.executor());
4961 let project = Project::test(fs, [], cx).await;
4962 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4963
4964 // Create several workspace items with single project entries, and two
4965 // workspace items with multiple project entries.
4966 let single_entry_items = (0..=4)
4967 .map(|project_entry_id| {
4968 cx.new_view(|cx| {
4969 TestItem::new(cx)
4970 .with_dirty(true)
4971 .with_project_items(&[TestProjectItem::new(
4972 project_entry_id,
4973 &format!("{project_entry_id}.txt"),
4974 cx,
4975 )])
4976 })
4977 })
4978 .collect::<Vec<_>>();
4979 let item_2_3 = cx.new_view(|cx| {
4980 TestItem::new(cx)
4981 .with_dirty(true)
4982 .with_singleton(false)
4983 .with_project_items(&[
4984 single_entry_items[2].read(cx).project_items[0].clone(),
4985 single_entry_items[3].read(cx).project_items[0].clone(),
4986 ])
4987 });
4988 let item_3_4 = cx.new_view(|cx| {
4989 TestItem::new(cx)
4990 .with_dirty(true)
4991 .with_singleton(false)
4992 .with_project_items(&[
4993 single_entry_items[3].read(cx).project_items[0].clone(),
4994 single_entry_items[4].read(cx).project_items[0].clone(),
4995 ])
4996 });
4997
4998 // Create two panes that contain the following project entries:
4999 // left pane:
5000 // multi-entry items: (2, 3)
5001 // single-entry items: 0, 1, 2, 3, 4
5002 // right pane:
5003 // single-entry items: 1
5004 // multi-entry items: (3, 4)
5005 let left_pane = workspace.update(cx, |workspace, cx| {
5006 let left_pane = workspace.active_pane().clone();
5007 workspace.add_item(Box::new(item_2_3.clone()), cx);
5008 for item in single_entry_items {
5009 workspace.add_item(Box::new(item), cx);
5010 }
5011 left_pane.update(cx, |pane, cx| {
5012 pane.activate_item(2, true, true, cx);
5013 });
5014
5015 let right_pane = workspace
5016 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
5017 .unwrap();
5018
5019 right_pane.update(cx, |pane, cx| {
5020 pane.add_item(Box::new(item_3_4.clone()), true, true, None, cx);
5021 });
5022
5023 left_pane
5024 });
5025
5026 cx.focus_view(&left_pane);
5027
5028 // When closing all of the items in the left pane, we should be prompted twice:
5029 // once for project entry 0, and once for project entry 2. Project entries 1,
5030 // 3, and 4 are all still open in the other paten. After those two
5031 // prompts, the task should complete.
5032
5033 let close = left_pane.update(cx, |pane, cx| {
5034 pane.close_all_items(&CloseAllItems::default(), cx).unwrap()
5035 });
5036 cx.executor().run_until_parked();
5037
5038 // Discard "Save all" prompt
5039 cx.simulate_prompt_answer(2);
5040
5041 cx.executor().run_until_parked();
5042 left_pane.update(cx, |pane, cx| {
5043 assert_eq!(
5044 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
5045 &[ProjectEntryId::from_proto(0)]
5046 );
5047 });
5048 cx.simulate_prompt_answer(0);
5049
5050 cx.executor().run_until_parked();
5051 left_pane.update(cx, |pane, cx| {
5052 assert_eq!(
5053 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
5054 &[ProjectEntryId::from_proto(2)]
5055 );
5056 });
5057 cx.simulate_prompt_answer(0);
5058
5059 cx.executor().run_until_parked();
5060 close.await.unwrap();
5061 left_pane.update(cx, |pane, _| {
5062 assert_eq!(pane.items_len(), 0);
5063 });
5064 }
5065
5066 #[gpui::test]
5067 async fn test_autosave(cx: &mut gpui::TestAppContext) {
5068 init_test(cx);
5069
5070 let fs = FakeFs::new(cx.executor());
5071 let project = Project::test(fs, [], cx).await;
5072 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5073 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5074
5075 let item = cx.new_view(|cx| {
5076 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5077 });
5078 let item_id = item.entity_id();
5079 workspace.update(cx, |workspace, cx| {
5080 workspace.add_item(Box::new(item.clone()), cx);
5081 });
5082
5083 // Autosave on window change.
5084 item.update(cx, |item, cx| {
5085 cx.update_global(|settings: &mut SettingsStore, cx| {
5086 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5087 settings.autosave = Some(AutosaveSetting::OnWindowChange);
5088 })
5089 });
5090 item.is_dirty = true;
5091 });
5092
5093 // Deactivating the window saves the file.
5094 cx.deactivate_window();
5095 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
5096
5097 // Autosave on focus change.
5098 item.update(cx, |item, cx| {
5099 cx.focus_self();
5100 cx.update_global(|settings: &mut SettingsStore, cx| {
5101 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5102 settings.autosave = Some(AutosaveSetting::OnFocusChange);
5103 })
5104 });
5105 item.is_dirty = true;
5106 });
5107
5108 // Blurring the item saves the file.
5109 item.update(cx, |_, cx| cx.blur());
5110 cx.executor().run_until_parked();
5111 item.update(cx, |item, _| assert_eq!(item.save_count, 2));
5112
5113 // Deactivating the window still saves the file.
5114 cx.update(|cx| cx.activate_window());
5115 item.update(cx, |item, cx| {
5116 cx.focus_self();
5117 item.is_dirty = true;
5118 });
5119 cx.deactivate_window();
5120
5121 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
5122
5123 // Autosave after delay.
5124 item.update(cx, |item, cx| {
5125 cx.update_global(|settings: &mut SettingsStore, cx| {
5126 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5127 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
5128 })
5129 });
5130 item.is_dirty = true;
5131 cx.emit(ItemEvent::Edit);
5132 });
5133
5134 // Delay hasn't fully expired, so the file is still dirty and unsaved.
5135 cx.executor().advance_clock(Duration::from_millis(250));
5136 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
5137
5138 // After delay expires, the file is saved.
5139 cx.executor().advance_clock(Duration::from_millis(250));
5140 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
5141
5142 // Autosave on focus change, ensuring closing the tab counts as such.
5143 item.update(cx, |item, cx| {
5144 cx.update_global(|settings: &mut SettingsStore, cx| {
5145 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5146 settings.autosave = Some(AutosaveSetting::OnFocusChange);
5147 })
5148 });
5149 item.is_dirty = true;
5150 });
5151
5152 pane.update(cx, |pane, cx| {
5153 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
5154 })
5155 .await
5156 .unwrap();
5157 assert!(!cx.has_pending_prompt());
5158 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
5159
5160 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
5161 workspace.update(cx, |workspace, cx| {
5162 workspace.add_item(Box::new(item.clone()), cx);
5163 });
5164 item.update(cx, |item, cx| {
5165 item.project_items[0].update(cx, |item, _| {
5166 item.entry_id = None;
5167 });
5168 item.is_dirty = true;
5169 cx.blur();
5170 });
5171 cx.run_until_parked();
5172 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
5173
5174 // Ensure autosave is prevented for deleted files also when closing the buffer.
5175 let _close_items = pane.update(cx, |pane, cx| {
5176 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
5177 });
5178 cx.run_until_parked();
5179 assert!(cx.has_pending_prompt());
5180 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
5181 }
5182
5183 #[gpui::test]
5184 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
5185 init_test(cx);
5186
5187 let fs = FakeFs::new(cx.executor());
5188
5189 let project = Project::test(fs, [], cx).await;
5190 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5191
5192 let item = cx.new_view(|cx| {
5193 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5194 });
5195 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5196 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
5197 let toolbar_notify_count = Rc::new(RefCell::new(0));
5198
5199 workspace.update(cx, |workspace, cx| {
5200 workspace.add_item(Box::new(item.clone()), cx);
5201 let toolbar_notification_count = toolbar_notify_count.clone();
5202 cx.observe(&toolbar, move |_, _, _| {
5203 *toolbar_notification_count.borrow_mut() += 1
5204 })
5205 .detach();
5206 });
5207
5208 pane.update(cx, |pane, _| {
5209 assert!(!pane.can_navigate_backward());
5210 assert!(!pane.can_navigate_forward());
5211 });
5212
5213 item.update(cx, |item, cx| {
5214 item.set_state("one".to_string(), cx);
5215 });
5216
5217 // Toolbar must be notified to re-render the navigation buttons
5218 assert_eq!(*toolbar_notify_count.borrow(), 1);
5219
5220 pane.update(cx, |pane, _| {
5221 assert!(pane.can_navigate_backward());
5222 assert!(!pane.can_navigate_forward());
5223 });
5224
5225 workspace
5226 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
5227 .await
5228 .unwrap();
5229
5230 assert_eq!(*toolbar_notify_count.borrow(), 2);
5231 pane.update(cx, |pane, _| {
5232 assert!(!pane.can_navigate_backward());
5233 assert!(pane.can_navigate_forward());
5234 });
5235 }
5236
5237 #[gpui::test]
5238 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
5239 init_test(cx);
5240 let fs = FakeFs::new(cx.executor());
5241
5242 let project = Project::test(fs, [], cx).await;
5243 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5244
5245 let panel = workspace.update(cx, |workspace, cx| {
5246 let panel = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
5247 workspace.add_panel(panel.clone(), cx);
5248
5249 workspace
5250 .right_dock()
5251 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5252
5253 panel
5254 });
5255
5256 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5257 pane.update(cx, |pane, cx| {
5258 let item = cx.new_view(|cx| TestItem::new(cx));
5259 pane.add_item(Box::new(item), true, true, None, cx);
5260 });
5261
5262 // Transfer focus from center to panel
5263 workspace.update(cx, |workspace, cx| {
5264 workspace.toggle_panel_focus::<TestPanel>(cx);
5265 });
5266
5267 workspace.update(cx, |workspace, cx| {
5268 assert!(workspace.right_dock().read(cx).is_open());
5269 assert!(!panel.is_zoomed(cx));
5270 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5271 });
5272
5273 // Transfer focus from panel to center
5274 workspace.update(cx, |workspace, cx| {
5275 workspace.toggle_panel_focus::<TestPanel>(cx);
5276 });
5277
5278 workspace.update(cx, |workspace, cx| {
5279 assert!(workspace.right_dock().read(cx).is_open());
5280 assert!(!panel.is_zoomed(cx));
5281 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5282 });
5283
5284 // Close the dock
5285 workspace.update(cx, |workspace, cx| {
5286 workspace.toggle_dock(DockPosition::Right, cx);
5287 });
5288
5289 workspace.update(cx, |workspace, cx| {
5290 assert!(!workspace.right_dock().read(cx).is_open());
5291 assert!(!panel.is_zoomed(cx));
5292 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5293 });
5294
5295 // Open the dock
5296 workspace.update(cx, |workspace, cx| {
5297 workspace.toggle_dock(DockPosition::Right, cx);
5298 });
5299
5300 workspace.update(cx, |workspace, cx| {
5301 assert!(workspace.right_dock().read(cx).is_open());
5302 assert!(!panel.is_zoomed(cx));
5303 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5304 });
5305
5306 // Focus and zoom panel
5307 panel.update(cx, |panel, cx| {
5308 cx.focus_self();
5309 panel.set_zoomed(true, cx)
5310 });
5311
5312 workspace.update(cx, |workspace, cx| {
5313 assert!(workspace.right_dock().read(cx).is_open());
5314 assert!(panel.is_zoomed(cx));
5315 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5316 });
5317
5318 // Transfer focus to the center closes the dock
5319 workspace.update(cx, |workspace, cx| {
5320 workspace.toggle_panel_focus::<TestPanel>(cx);
5321 });
5322
5323 workspace.update(cx, |workspace, cx| {
5324 assert!(!workspace.right_dock().read(cx).is_open());
5325 assert!(panel.is_zoomed(cx));
5326 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5327 });
5328
5329 // Transferring focus back to the panel keeps it zoomed
5330 workspace.update(cx, |workspace, cx| {
5331 workspace.toggle_panel_focus::<TestPanel>(cx);
5332 });
5333
5334 workspace.update(cx, |workspace, cx| {
5335 assert!(workspace.right_dock().read(cx).is_open());
5336 assert!(panel.is_zoomed(cx));
5337 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5338 });
5339
5340 // Close the dock while it is zoomed
5341 workspace.update(cx, |workspace, cx| {
5342 workspace.toggle_dock(DockPosition::Right, cx)
5343 });
5344
5345 workspace.update(cx, |workspace, cx| {
5346 assert!(!workspace.right_dock().read(cx).is_open());
5347 assert!(panel.is_zoomed(cx));
5348 assert!(workspace.zoomed.is_none());
5349 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5350 });
5351
5352 // Opening the dock, when it's zoomed, retains focus
5353 workspace.update(cx, |workspace, cx| {
5354 workspace.toggle_dock(DockPosition::Right, cx)
5355 });
5356
5357 workspace.update(cx, |workspace, cx| {
5358 assert!(workspace.right_dock().read(cx).is_open());
5359 assert!(panel.is_zoomed(cx));
5360 assert!(workspace.zoomed.is_some());
5361 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5362 });
5363
5364 // Unzoom and close the panel, zoom the active pane.
5365 panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
5366 workspace.update(cx, |workspace, cx| {
5367 workspace.toggle_dock(DockPosition::Right, cx)
5368 });
5369 pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
5370
5371 // Opening a dock unzooms the pane.
5372 workspace.update(cx, |workspace, cx| {
5373 workspace.toggle_dock(DockPosition::Right, cx)
5374 });
5375 workspace.update(cx, |workspace, cx| {
5376 let pane = pane.read(cx);
5377 assert!(!pane.is_zoomed());
5378 assert!(!pane.focus_handle(cx).is_focused(cx));
5379 assert!(workspace.right_dock().read(cx).is_open());
5380 assert!(workspace.zoomed.is_none());
5381 });
5382 }
5383
5384 struct TestModal(FocusHandle);
5385
5386 impl TestModal {
5387 fn new(cx: &mut ViewContext<Self>) -> Self {
5388 Self(cx.focus_handle())
5389 }
5390 }
5391
5392 impl EventEmitter<DismissEvent> for TestModal {}
5393
5394 impl FocusableView for TestModal {
5395 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
5396 self.0.clone()
5397 }
5398 }
5399
5400 impl ModalView for TestModal {}
5401
5402 impl Render for TestModal {
5403 fn render(&mut self, _cx: &mut ViewContext<TestModal>) -> impl IntoElement {
5404 div().track_focus(&self.0)
5405 }
5406 }
5407
5408 #[gpui::test]
5409 async fn test_panels(cx: &mut gpui::TestAppContext) {
5410 init_test(cx);
5411 let fs = FakeFs::new(cx.executor());
5412
5413 let project = Project::test(fs, [], cx).await;
5414 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5415
5416 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
5417 let panel_1 = cx.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
5418 workspace.add_panel(panel_1.clone(), cx);
5419 workspace
5420 .left_dock()
5421 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
5422 let panel_2 = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
5423 workspace.add_panel(panel_2.clone(), cx);
5424 workspace
5425 .right_dock()
5426 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5427
5428 let left_dock = workspace.left_dock();
5429 assert_eq!(
5430 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5431 panel_1.panel_id()
5432 );
5433 assert_eq!(
5434 left_dock.read(cx).active_panel_size(cx).unwrap(),
5435 panel_1.size(cx)
5436 );
5437
5438 left_dock.update(cx, |left_dock, cx| {
5439 left_dock.resize_active_panel(Some(px(1337.)), cx)
5440 });
5441 assert_eq!(
5442 workspace
5443 .right_dock()
5444 .read(cx)
5445 .visible_panel()
5446 .unwrap()
5447 .panel_id(),
5448 panel_2.panel_id(),
5449 );
5450
5451 (panel_1, panel_2)
5452 });
5453
5454 // Move panel_1 to the right
5455 panel_1.update(cx, |panel_1, cx| {
5456 panel_1.set_position(DockPosition::Right, cx)
5457 });
5458
5459 workspace.update(cx, |workspace, cx| {
5460 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
5461 // Since it was the only panel on the left, the left dock should now be closed.
5462 assert!(!workspace.left_dock().read(cx).is_open());
5463 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
5464 let right_dock = workspace.right_dock();
5465 assert_eq!(
5466 right_dock.read(cx).visible_panel().unwrap().panel_id(),
5467 panel_1.panel_id()
5468 );
5469 assert_eq!(
5470 right_dock.read(cx).active_panel_size(cx).unwrap(),
5471 px(1337.)
5472 );
5473
5474 // Now we move panel_2 to the left
5475 panel_2.set_position(DockPosition::Left, cx);
5476 });
5477
5478 workspace.update(cx, |workspace, cx| {
5479 // Since panel_2 was not visible on the right, we don't open the left dock.
5480 assert!(!workspace.left_dock().read(cx).is_open());
5481 // And the right dock is unaffected in it's displaying of panel_1
5482 assert!(workspace.right_dock().read(cx).is_open());
5483 assert_eq!(
5484 workspace
5485 .right_dock()
5486 .read(cx)
5487 .visible_panel()
5488 .unwrap()
5489 .panel_id(),
5490 panel_1.panel_id(),
5491 );
5492 });
5493
5494 // Move panel_1 back to the left
5495 panel_1.update(cx, |panel_1, cx| {
5496 panel_1.set_position(DockPosition::Left, cx)
5497 });
5498
5499 workspace.update(cx, |workspace, cx| {
5500 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
5501 let left_dock = workspace.left_dock();
5502 assert!(left_dock.read(cx).is_open());
5503 assert_eq!(
5504 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5505 panel_1.panel_id()
5506 );
5507 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), px(1337.));
5508 // And the right dock should be closed as it no longer has any panels.
5509 assert!(!workspace.right_dock().read(cx).is_open());
5510
5511 // Now we move panel_1 to the bottom
5512 panel_1.set_position(DockPosition::Bottom, cx);
5513 });
5514
5515 workspace.update(cx, |workspace, cx| {
5516 // Since panel_1 was visible on the left, we close the left dock.
5517 assert!(!workspace.left_dock().read(cx).is_open());
5518 // The bottom dock is sized based on the panel's default size,
5519 // since the panel orientation changed from vertical to horizontal.
5520 let bottom_dock = workspace.bottom_dock();
5521 assert_eq!(
5522 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
5523 panel_1.size(cx),
5524 );
5525 // Close bottom dock and move panel_1 back to the left.
5526 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
5527 panel_1.set_position(DockPosition::Left, cx);
5528 });
5529
5530 // Emit activated event on panel 1
5531 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
5532
5533 // Now the left dock is open and panel_1 is active and focused.
5534 workspace.update(cx, |workspace, cx| {
5535 let left_dock = workspace.left_dock();
5536 assert!(left_dock.read(cx).is_open());
5537 assert_eq!(
5538 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5539 panel_1.panel_id(),
5540 );
5541 assert!(panel_1.focus_handle(cx).is_focused(cx));
5542 });
5543
5544 // Emit closed event on panel 2, which is not active
5545 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
5546
5547 // Wo don't close the left dock, because panel_2 wasn't the active panel
5548 workspace.update(cx, |workspace, cx| {
5549 let left_dock = workspace.left_dock();
5550 assert!(left_dock.read(cx).is_open());
5551 assert_eq!(
5552 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5553 panel_1.panel_id(),
5554 );
5555 });
5556
5557 // Emitting a ZoomIn event shows the panel as zoomed.
5558 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
5559 workspace.update(cx, |workspace, _| {
5560 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5561 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
5562 });
5563
5564 // Move panel to another dock while it is zoomed
5565 panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
5566 workspace.update(cx, |workspace, _| {
5567 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5568
5569 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5570 });
5571
5572 // This is a helper for getting a:
5573 // - valid focus on an element,
5574 // - that isn't a part of the panes and panels system of the Workspace,
5575 // - and doesn't trigger the 'on_focus_lost' API.
5576 let focus_other_view = {
5577 let workspace = workspace.clone();
5578 move |cx: &mut VisualTestContext| {
5579 workspace.update(cx, |workspace, cx| {
5580 if let Some(_) = workspace.active_modal::<TestModal>(cx) {
5581 workspace.toggle_modal(cx, TestModal::new);
5582 workspace.toggle_modal(cx, TestModal::new);
5583 } else {
5584 workspace.toggle_modal(cx, TestModal::new);
5585 }
5586 })
5587 }
5588 };
5589
5590 // If focus is transferred to another view that's not a panel or another pane, we still show
5591 // the panel as zoomed.
5592 focus_other_view(cx);
5593 workspace.update(cx, |workspace, _| {
5594 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5595 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5596 });
5597
5598 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
5599 workspace.update(cx, |_, cx| cx.focus_self());
5600 workspace.update(cx, |workspace, _| {
5601 assert_eq!(workspace.zoomed, None);
5602 assert_eq!(workspace.zoomed_position, None);
5603 });
5604
5605 // If focus is transferred again to another view that's not a panel or a pane, we won't
5606 // show the panel as zoomed because it wasn't zoomed before.
5607 focus_other_view(cx);
5608 workspace.update(cx, |workspace, _| {
5609 assert_eq!(workspace.zoomed, None);
5610 assert_eq!(workspace.zoomed_position, None);
5611 });
5612
5613 // When the panel is activated, it is zoomed again.
5614 cx.dispatch_action(ToggleRightDock);
5615 workspace.update(cx, |workspace, _| {
5616 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5617 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5618 });
5619
5620 // Emitting a ZoomOut event unzooms the panel.
5621 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
5622 workspace.update(cx, |workspace, _| {
5623 assert_eq!(workspace.zoomed, None);
5624 assert_eq!(workspace.zoomed_position, None);
5625 });
5626
5627 // Emit closed event on panel 1, which is active
5628 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
5629
5630 // Now the left dock is closed, because panel_1 was the active panel
5631 workspace.update(cx, |workspace, cx| {
5632 let right_dock = workspace.right_dock();
5633 assert!(!right_dock.read(cx).is_open());
5634 });
5635 }
5636
5637 pub fn init_test(cx: &mut TestAppContext) {
5638 cx.update(|cx| {
5639 let settings_store = SettingsStore::test(cx);
5640 cx.set_global(settings_store);
5641 theme::init(theme::LoadThemes::JustBase, cx);
5642 language::init(cx);
5643 crate::init_settings(cx);
5644 Project::init_settings(cx);
5645 });
5646 }
5647}