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