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