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