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