1pub mod dock;
2pub mod item;
3mod modal_layer;
4pub mod notifications;
5pub mod pane;
6pub mod pane_group;
7mod persistence;
8pub mod searchable;
9pub mod shared_screen;
10mod status_bar;
11mod toolbar;
12mod workspace_settings;
13
14use anyhow::{anyhow, Context as _, Result};
15use call::{call_settings::CallSettings, ActiveCall};
16use client::{
17 proto::{self, ErrorCode, PeerId},
18 Client, ErrorExt, Status, TypedEnvelope, UserStore,
19};
20use collections::{hash_map, HashMap, HashSet};
21use derive_more::{Deref, DerefMut};
22use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
23use futures::{
24 channel::{mpsc, oneshot},
25 future::try_join_all,
26 Future, FutureExt, StreamExt,
27};
28use gpui::{
29 actions, canvas, div, impl_actions, point, px, size, Action, AnyElement, AnyModel, AnyView,
30 AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Context, Div,
31 DragMoveEvent, Element, ElementContext, Entity, EntityId, EventEmitter, FocusHandle,
32 FocusableView, Global, GlobalPixels, InteractiveElement, IntoElement, KeyContext, 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
3977actions!(collab, [OpenChannelNotes]);
3978
3979async fn join_channel_internal(
3980 channel_id: u64,
3981 app_state: &Arc<AppState>,
3982 requesting_window: Option<WindowHandle<Workspace>>,
3983 active_call: &Model<ActiveCall>,
3984 cx: &mut AsyncAppContext,
3985) -> Result<bool> {
3986 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
3987 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
3988 return (false, None);
3989 };
3990
3991 let already_in_channel = room.channel_id() == Some(channel_id);
3992 let should_prompt = room.is_sharing_project()
3993 && room.remote_participants().len() > 0
3994 && !already_in_channel;
3995 let open_room = if already_in_channel {
3996 active_call.room().cloned()
3997 } else {
3998 None
3999 };
4000 (should_prompt, open_room)
4001 })?;
4002
4003 if let Some(room) = open_room {
4004 let task = room.update(cx, |room, cx| {
4005 if let Some((project, host)) = room.most_active_project(cx) {
4006 return Some(join_remote_project(project, host, app_state.clone(), cx));
4007 }
4008
4009 None
4010 })?;
4011 if let Some(task) = task {
4012 task.await?;
4013 }
4014 return anyhow::Ok(true);
4015 }
4016
4017 if should_prompt {
4018 if let Some(workspace) = requesting_window {
4019 let answer = workspace
4020 .update(cx, |_, cx| {
4021 cx.prompt(
4022 PromptLevel::Warning,
4023 "Do you want to switch channels?",
4024 Some("Leaving this call will unshare your current project."),
4025 &["Yes, Join Channel", "Cancel"],
4026 )
4027 })?
4028 .await;
4029
4030 if answer == Ok(1) {
4031 return Ok(false);
4032 }
4033 } else {
4034 return Ok(false); // unreachable!() hopefully
4035 }
4036 }
4037
4038 let client = cx.update(|cx| active_call.read(cx).client())?;
4039
4040 let mut client_status = client.status();
4041
4042 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
4043 'outer: loop {
4044 let Some(status) = client_status.recv().await else {
4045 return Err(anyhow!("error connecting"));
4046 };
4047
4048 match status {
4049 Status::Connecting
4050 | Status::Authenticating
4051 | Status::Reconnecting
4052 | Status::Reauthenticating => continue,
4053 Status::Connected { .. } => break 'outer,
4054 Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
4055 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
4056 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
4057 return Err(ErrorCode::Disconnected.into())
4058 }
4059 }
4060 }
4061
4062 let room = active_call
4063 .update(cx, |active_call, cx| {
4064 active_call.join_channel(channel_id, cx)
4065 })?
4066 .await?;
4067
4068 let Some(room) = room else {
4069 return anyhow::Ok(true);
4070 };
4071
4072 room.update(cx, |room, _| room.room_update_completed())?
4073 .await;
4074
4075 let task = room.update(cx, |room, cx| {
4076 if let Some((project, host)) = room.most_active_project(cx) {
4077 return Some(join_remote_project(project, host, app_state.clone(), cx));
4078 }
4079
4080 // if you are the first to join a channel, share your project
4081 if room.remote_participants().len() == 0 && !room.local_participant_is_guest() {
4082 if let Some(workspace) = requesting_window {
4083 let project = workspace.update(cx, |workspace, cx| {
4084 if !CallSettings::get_global(cx).share_on_join {
4085 return None;
4086 }
4087 let project = workspace.project.read(cx);
4088 if project.is_local()
4089 && project.visible_worktrees(cx).any(|tree| {
4090 tree.read(cx)
4091 .root_entry()
4092 .map_or(false, |entry| entry.is_dir())
4093 })
4094 {
4095 Some(workspace.project.clone())
4096 } else {
4097 None
4098 }
4099 });
4100 if let Ok(Some(project)) = project {
4101 return Some(cx.spawn(|room, mut cx| async move {
4102 room.update(&mut cx, |room, cx| room.share_project(project, cx))?
4103 .await?;
4104 Ok(())
4105 }));
4106 }
4107 }
4108 }
4109
4110 None
4111 })?;
4112 if let Some(task) = task {
4113 task.await?;
4114 return anyhow::Ok(true);
4115 }
4116 anyhow::Ok(false)
4117}
4118
4119pub fn join_channel(
4120 channel_id: u64,
4121 app_state: Arc<AppState>,
4122 requesting_window: Option<WindowHandle<Workspace>>,
4123 cx: &mut AppContext,
4124) -> Task<Result<()>> {
4125 let active_call = ActiveCall::global(cx);
4126 cx.spawn(|mut cx| async move {
4127 let result = join_channel_internal(
4128 channel_id,
4129 &app_state,
4130 requesting_window,
4131 &active_call,
4132 &mut cx,
4133 )
4134 .await;
4135
4136 // join channel succeeded, and opened a window
4137 if matches!(result, Ok(true)) {
4138 return anyhow::Ok(());
4139 }
4140
4141 // find an existing workspace to focus and show call controls
4142 let mut active_window =
4143 requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
4144 if active_window.is_none() {
4145 // no open workspaces, make one to show the error in (blergh)
4146 let (window_handle, _) = cx
4147 .update(|cx| {
4148 Workspace::new_local(vec![], app_state.clone(), requesting_window, cx)
4149 })?
4150 .await?;
4151
4152 if result.is_ok() {
4153 cx.update(|cx| {
4154 cx.dispatch_action(&OpenChannelNotes);
4155 }).log_err();
4156 }
4157
4158 active_window = Some(window_handle);
4159 }
4160
4161 if let Err(err) = result {
4162 log::error!("failed to join channel: {}", err);
4163 if let Some(active_window) = active_window {
4164 active_window
4165 .update(&mut cx, |_, cx| {
4166 let detail: SharedString = match err.error_code() {
4167 ErrorCode::SignedOut => {
4168 "Please sign in to continue.".into()
4169 },
4170 ErrorCode::UpgradeRequired => {
4171 "Your are running an unsupported version of Zed. Please update to continue.".into()
4172 },
4173 ErrorCode::NoSuchChannel => {
4174 "No matching channel was found. Please check the link and try again.".into()
4175 },
4176 ErrorCode::Forbidden => {
4177 "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
4178 },
4179 ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
4180 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(),
4181 _ => format!("{}\n\nPlease try again.", err).into(),
4182 };
4183 cx.prompt(
4184 PromptLevel::Critical,
4185 "Failed to join channel",
4186 Some(&detail),
4187 &["Ok"],
4188 )
4189 })?
4190 .await
4191 .ok();
4192 }
4193 }
4194
4195 // return ok, we showed the error to the user.
4196 return anyhow::Ok(());
4197 })
4198}
4199
4200pub async fn get_any_active_workspace(
4201 app_state: Arc<AppState>,
4202 mut cx: AsyncAppContext,
4203) -> anyhow::Result<WindowHandle<Workspace>> {
4204 // find an existing workspace to focus and show call controls
4205 let active_window = activate_any_workspace_window(&mut cx);
4206 if active_window.is_none() {
4207 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, cx))?
4208 .await?;
4209 }
4210 activate_any_workspace_window(&mut cx).context("could not open zed")
4211}
4212
4213fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandle<Workspace>> {
4214 cx.update(|cx| {
4215 for window in cx.windows() {
4216 if let Some(workspace_window) = window.downcast::<Workspace>() {
4217 workspace_window
4218 .update(cx, |_, cx| cx.activate_window())
4219 .ok();
4220 return Some(workspace_window);
4221 }
4222 }
4223 None
4224 })
4225 .ok()
4226 .flatten()
4227}
4228
4229#[allow(clippy::type_complexity)]
4230pub fn open_paths(
4231 abs_paths: &[PathBuf],
4232 app_state: &Arc<AppState>,
4233 requesting_window: Option<WindowHandle<Workspace>>,
4234 cx: &mut AppContext,
4235) -> Task<
4236 anyhow::Result<(
4237 WindowHandle<Workspace>,
4238 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
4239 )>,
4240> {
4241 let app_state = app_state.clone();
4242 let abs_paths = abs_paths.to_vec();
4243 // Open paths in existing workspace if possible
4244 let existing = activate_workspace_for_project(cx, {
4245 let abs_paths = abs_paths.clone();
4246 move |project, cx| project.contains_paths(&abs_paths, cx)
4247 });
4248 cx.spawn(move |mut cx| async move {
4249 if let Some(existing) = existing {
4250 Ok((
4251 existing.clone(),
4252 existing
4253 .update(&mut cx, |workspace, cx| {
4254 workspace.open_paths(abs_paths, OpenVisible::All, None, cx)
4255 })?
4256 .await,
4257 ))
4258 } else {
4259 cx.update(move |cx| {
4260 Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx)
4261 })?
4262 .await
4263 }
4264 })
4265}
4266
4267pub fn open_new(
4268 app_state: &Arc<AppState>,
4269 cx: &mut AppContext,
4270 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static + Send,
4271) -> Task<()> {
4272 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
4273 cx.spawn(|mut cx| async move {
4274 if let Some((workspace, opened_paths)) = task.await.log_err() {
4275 workspace
4276 .update(&mut cx, |workspace, cx| {
4277 if opened_paths.is_empty() {
4278 init(workspace, cx)
4279 }
4280 })
4281 .log_err();
4282 }
4283 })
4284}
4285
4286pub fn create_and_open_local_file(
4287 path: &'static Path,
4288 cx: &mut ViewContext<Workspace>,
4289 default_content: impl 'static + Send + FnOnce() -> Rope,
4290) -> Task<Result<Box<dyn ItemHandle>>> {
4291 cx.spawn(|workspace, mut cx| async move {
4292 let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
4293 if !fs.is_file(path).await {
4294 fs.create_file(path, Default::default()).await?;
4295 fs.save(path, &default_content(), Default::default())
4296 .await?;
4297 }
4298
4299 let mut items = workspace
4300 .update(&mut cx, |workspace, cx| {
4301 workspace.with_local_workspace(cx, |workspace, cx| {
4302 workspace.open_paths(vec![path.to_path_buf()], OpenVisible::None, None, cx)
4303 })
4304 })?
4305 .await?
4306 .await;
4307
4308 let item = items.pop().flatten();
4309 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
4310 })
4311}
4312
4313pub fn join_remote_project(
4314 project_id: u64,
4315 follow_user_id: u64,
4316 app_state: Arc<AppState>,
4317 cx: &mut AppContext,
4318) -> Task<Result<()>> {
4319 let windows = cx.windows();
4320 cx.spawn(|mut cx| async move {
4321 let existing_workspace = windows.into_iter().find_map(|window| {
4322 window.downcast::<Workspace>().and_then(|window| {
4323 window
4324 .update(&mut cx, |workspace, cx| {
4325 if workspace.project().read(cx).remote_id() == Some(project_id) {
4326 Some(window)
4327 } else {
4328 None
4329 }
4330 })
4331 .unwrap_or(None)
4332 })
4333 });
4334
4335 let workspace = if let Some(existing_workspace) = existing_workspace {
4336 existing_workspace
4337 } else {
4338 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
4339 let room = active_call
4340 .read_with(&cx, |call, _| call.room().cloned())?
4341 .ok_or_else(|| anyhow!("not in a call"))?;
4342 let project = room
4343 .update(&mut cx, |room, cx| {
4344 room.join_project(
4345 project_id,
4346 app_state.languages.clone(),
4347 app_state.fs.clone(),
4348 cx,
4349 )
4350 })?
4351 .await?;
4352
4353 let window_bounds_override = window_bounds_env_override(&cx);
4354 cx.update(|cx| {
4355 let options = (app_state.build_window_options)(window_bounds_override, None, cx);
4356 cx.open_window(options, |cx| {
4357 cx.new_view(|cx| Workspace::new(0, project, app_state.clone(), cx))
4358 })
4359 })?
4360 };
4361
4362 workspace.update(&mut cx, |workspace, cx| {
4363 cx.activate(true);
4364 cx.activate_window();
4365
4366 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
4367 let follow_peer_id = room
4368 .read(cx)
4369 .remote_participants()
4370 .iter()
4371 .find(|(_, participant)| participant.user.id == follow_user_id)
4372 .map(|(_, p)| p.peer_id)
4373 .or_else(|| {
4374 // If we couldn't follow the given user, follow the host instead.
4375 let collaborator = workspace
4376 .project()
4377 .read(cx)
4378 .collaborators()
4379 .values()
4380 .find(|collaborator| collaborator.replica_id == 0)?;
4381 Some(collaborator.peer_id)
4382 });
4383
4384 if let Some(follow_peer_id) = follow_peer_id {
4385 workspace.follow(follow_peer_id, cx);
4386 }
4387 }
4388 })?;
4389
4390 anyhow::Ok(())
4391 })
4392}
4393
4394pub fn restart(_: &Restart, cx: &mut AppContext) {
4395 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
4396 let mut workspace_windows = cx
4397 .windows()
4398 .into_iter()
4399 .filter_map(|window| window.downcast::<Workspace>())
4400 .collect::<Vec<_>>();
4401
4402 // If multiple windows have unsaved changes, and need a save prompt,
4403 // prompt in the active window before switching to a different window.
4404 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
4405
4406 let mut prompt = None;
4407 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
4408 prompt = window
4409 .update(cx, |_, cx| {
4410 cx.prompt(
4411 PromptLevel::Info,
4412 "Are you sure you want to restart?",
4413 None,
4414 &["Restart", "Cancel"],
4415 )
4416 })
4417 .ok();
4418 }
4419
4420 cx.spawn(|mut cx| async move {
4421 if let Some(prompt) = prompt {
4422 let answer = prompt.await?;
4423 if answer != 0 {
4424 return Ok(());
4425 }
4426 }
4427
4428 // If the user cancels any save prompt, then keep the app open.
4429 for window in workspace_windows {
4430 if let Ok(should_close) = window.update(&mut cx, |workspace, cx| {
4431 workspace.prepare_to_close(true, cx)
4432 }) {
4433 if !should_close.await? {
4434 return Ok(());
4435 }
4436 }
4437 }
4438
4439 cx.update(|cx| cx.restart())
4440 })
4441 .detach_and_log_err(cx);
4442}
4443
4444fn parse_pixel_position_env_var(value: &str) -> Option<Point<GlobalPixels>> {
4445 let mut parts = value.split(',');
4446 let x: usize = parts.next()?.parse().ok()?;
4447 let y: usize = parts.next()?.parse().ok()?;
4448 Some(point((x as f64).into(), (y as f64).into()))
4449}
4450
4451fn parse_pixel_size_env_var(value: &str) -> Option<Size<GlobalPixels>> {
4452 let mut parts = value.split(',');
4453 let width: usize = parts.next()?.parse().ok()?;
4454 let height: usize = parts.next()?.parse().ok()?;
4455 Some(size((width as f64).into(), (height as f64).into()))
4456}
4457
4458pub fn titlebar_height(cx: &mut WindowContext) -> Pixels {
4459 (1.75 * cx.rem_size()).max(px(32.))
4460}
4461
4462struct DisconnectedOverlay;
4463
4464impl Element for DisconnectedOverlay {
4465 type State = AnyElement;
4466
4467 fn request_layout(
4468 &mut self,
4469 _: Option<Self::State>,
4470 cx: &mut ElementContext,
4471 ) -> (LayoutId, Self::State) {
4472 let mut background = cx.theme().colors().elevated_surface_background;
4473 background.fade_out(0.2);
4474 let mut overlay = div()
4475 .bg(background)
4476 .absolute()
4477 .left_0()
4478 .top(titlebar_height(cx))
4479 .size_full()
4480 .flex()
4481 .items_center()
4482 .justify_center()
4483 .capture_any_mouse_down(|_, cx| cx.stop_propagation())
4484 .capture_any_mouse_up(|_, cx| cx.stop_propagation())
4485 .child(Label::new(
4486 "Your connection to the remote project has been lost.",
4487 ))
4488 .into_any();
4489 (overlay.request_layout(cx), overlay)
4490 }
4491
4492 fn paint(
4493 &mut self,
4494 bounds: Bounds<Pixels>,
4495 overlay: &mut Self::State,
4496 cx: &mut ElementContext,
4497 ) {
4498 cx.with_z_index(u16::MAX, |cx| {
4499 cx.add_opaque_layer(bounds);
4500 overlay.paint(cx);
4501 })
4502 }
4503}
4504
4505impl IntoElement for DisconnectedOverlay {
4506 type Element = Self;
4507
4508 fn element_id(&self) -> Option<ui::prelude::ElementId> {
4509 None
4510 }
4511
4512 fn into_element(self) -> Self::Element {
4513 self
4514 }
4515}
4516
4517#[cfg(test)]
4518mod tests {
4519 use std::{cell::RefCell, rc::Rc};
4520
4521 use super::*;
4522 use crate::{
4523 dock::{test::TestPanel, PanelEvent},
4524 item::{
4525 test::{TestItem, TestProjectItem},
4526 ItemEvent,
4527 },
4528 };
4529 use fs::FakeFs;
4530 use gpui::{px, DismissEvent, TestAppContext, VisualTestContext};
4531 use project::{Project, ProjectEntryId};
4532 use serde_json::json;
4533 use settings::SettingsStore;
4534
4535 #[gpui::test]
4536 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
4537 init_test(cx);
4538
4539 let fs = FakeFs::new(cx.executor());
4540 let project = Project::test(fs, [], cx).await;
4541 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4542
4543 // Adding an item with no ambiguity renders the tab without detail.
4544 let item1 = cx.new_view(|cx| {
4545 let mut item = TestItem::new(cx);
4546 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
4547 item
4548 });
4549 workspace.update(cx, |workspace, cx| {
4550 workspace.add_item(Box::new(item1.clone()), cx);
4551 });
4552 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
4553
4554 // Adding an item that creates ambiguity increases the level of detail on
4555 // both tabs.
4556 let item2 = cx.new_view(|cx| {
4557 let mut item = TestItem::new(cx);
4558 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4559 item
4560 });
4561 workspace.update(cx, |workspace, cx| {
4562 workspace.add_item(Box::new(item2.clone()), cx);
4563 });
4564 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4565 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4566
4567 // Adding an item that creates ambiguity increases the level of detail only
4568 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
4569 // we stop at the highest detail available.
4570 let item3 = cx.new_view(|cx| {
4571 let mut item = TestItem::new(cx);
4572 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4573 item
4574 });
4575 workspace.update(cx, |workspace, cx| {
4576 workspace.add_item(Box::new(item3.clone()), cx);
4577 });
4578 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4579 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4580 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4581 }
4582
4583 #[gpui::test]
4584 async fn test_tracking_active_path(cx: &mut TestAppContext) {
4585 init_test(cx);
4586
4587 let fs = FakeFs::new(cx.executor());
4588 fs.insert_tree(
4589 "/root1",
4590 json!({
4591 "one.txt": "",
4592 "two.txt": "",
4593 }),
4594 )
4595 .await;
4596 fs.insert_tree(
4597 "/root2",
4598 json!({
4599 "three.txt": "",
4600 }),
4601 )
4602 .await;
4603
4604 let project = Project::test(fs, ["root1".as_ref()], cx).await;
4605 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4606 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4607 let worktree_id = project.update(cx, |project, cx| {
4608 project.worktrees().next().unwrap().read(cx).id()
4609 });
4610
4611 let item1 = cx.new_view(|cx| {
4612 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
4613 });
4614 let item2 = cx.new_view(|cx| {
4615 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
4616 });
4617
4618 // Add an item to an empty pane
4619 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
4620 project.update(cx, |project, cx| {
4621 assert_eq!(
4622 project.active_entry(),
4623 project
4624 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4625 .map(|e| e.id)
4626 );
4627 });
4628 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
4629
4630 // Add a second item to a non-empty pane
4631 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
4632 assert_eq!(cx.window_title().as_deref(), Some("two.txt — root1"));
4633 project.update(cx, |project, cx| {
4634 assert_eq!(
4635 project.active_entry(),
4636 project
4637 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
4638 .map(|e| e.id)
4639 );
4640 });
4641
4642 // Close the active item
4643 pane.update(cx, |pane, cx| {
4644 pane.close_active_item(&Default::default(), cx).unwrap()
4645 })
4646 .await
4647 .unwrap();
4648 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
4649 project.update(cx, |project, cx| {
4650 assert_eq!(
4651 project.active_entry(),
4652 project
4653 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4654 .map(|e| e.id)
4655 );
4656 });
4657
4658 // Add a project folder
4659 project
4660 .update(cx, |project, cx| {
4661 project.find_or_create_local_worktree("/root2", true, cx)
4662 })
4663 .await
4664 .unwrap();
4665 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1, root2"));
4666
4667 // Remove a project folder
4668 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
4669 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root2"));
4670 }
4671
4672 #[gpui::test]
4673 async fn test_close_window(cx: &mut TestAppContext) {
4674 init_test(cx);
4675
4676 let fs = FakeFs::new(cx.executor());
4677 fs.insert_tree("/root", json!({ "one": "" })).await;
4678
4679 let project = Project::test(fs, ["root".as_ref()], cx).await;
4680 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4681
4682 // When there are no dirty items, there's nothing to do.
4683 let item1 = cx.new_view(|cx| TestItem::new(cx));
4684 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
4685 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4686 assert!(task.await.unwrap());
4687
4688 // When there are dirty untitled items, prompt to save each one. If the user
4689 // cancels any prompt, then abort.
4690 let item2 = cx.new_view(|cx| TestItem::new(cx).with_dirty(true));
4691 let item3 = cx.new_view(|cx| {
4692 TestItem::new(cx)
4693 .with_dirty(true)
4694 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4695 });
4696 workspace.update(cx, |w, cx| {
4697 w.add_item(Box::new(item2.clone()), cx);
4698 w.add_item(Box::new(item3.clone()), cx);
4699 });
4700 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4701 cx.executor().run_until_parked();
4702 cx.simulate_prompt_answer(2); // cancel save all
4703 cx.executor().run_until_parked();
4704 cx.simulate_prompt_answer(2); // cancel save all
4705 cx.executor().run_until_parked();
4706 assert!(!cx.has_pending_prompt());
4707 assert!(!task.await.unwrap());
4708 }
4709
4710 #[gpui::test]
4711 async fn test_close_pane_items(cx: &mut TestAppContext) {
4712 init_test(cx);
4713
4714 let fs = FakeFs::new(cx.executor());
4715
4716 let project = Project::test(fs, None, cx).await;
4717 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4718
4719 let item1 = cx.new_view(|cx| {
4720 TestItem::new(cx)
4721 .with_dirty(true)
4722 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4723 });
4724 let item2 = cx.new_view(|cx| {
4725 TestItem::new(cx)
4726 .with_dirty(true)
4727 .with_conflict(true)
4728 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
4729 });
4730 let item3 = cx.new_view(|cx| {
4731 TestItem::new(cx)
4732 .with_dirty(true)
4733 .with_conflict(true)
4734 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
4735 });
4736 let item4 = cx.new_view(|cx| {
4737 TestItem::new(cx)
4738 .with_dirty(true)
4739 .with_project_items(&[TestProjectItem::new_untitled(cx)])
4740 });
4741 let pane = workspace.update(cx, |workspace, cx| {
4742 workspace.add_item(Box::new(item1.clone()), cx);
4743 workspace.add_item(Box::new(item2.clone()), cx);
4744 workspace.add_item(Box::new(item3.clone()), cx);
4745 workspace.add_item(Box::new(item4.clone()), cx);
4746 workspace.active_pane().clone()
4747 });
4748
4749 let close_items = pane.update(cx, |pane, cx| {
4750 pane.activate_item(1, true, true, cx);
4751 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
4752 let item1_id = item1.item_id();
4753 let item3_id = item3.item_id();
4754 let item4_id = item4.item_id();
4755 pane.close_items(cx, SaveIntent::Close, move |id| {
4756 [item1_id, item3_id, item4_id].contains(&id)
4757 })
4758 });
4759 cx.executor().run_until_parked();
4760
4761 assert!(cx.has_pending_prompt());
4762 // Ignore "Save all" prompt
4763 cx.simulate_prompt_answer(2);
4764 cx.executor().run_until_parked();
4765 // There's a prompt to save item 1.
4766 pane.update(cx, |pane, _| {
4767 assert_eq!(pane.items_len(), 4);
4768 assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
4769 });
4770 // Confirm saving item 1.
4771 cx.simulate_prompt_answer(0);
4772 cx.executor().run_until_parked();
4773
4774 // Item 1 is saved. There's a prompt to save item 3.
4775 pane.update(cx, |pane, cx| {
4776 assert_eq!(item1.read(cx).save_count, 1);
4777 assert_eq!(item1.read(cx).save_as_count, 0);
4778 assert_eq!(item1.read(cx).reload_count, 0);
4779 assert_eq!(pane.items_len(), 3);
4780 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
4781 });
4782 assert!(cx.has_pending_prompt());
4783
4784 // Cancel saving item 3.
4785 cx.simulate_prompt_answer(1);
4786 cx.executor().run_until_parked();
4787
4788 // Item 3 is reloaded. There's a prompt to save item 4.
4789 pane.update(cx, |pane, cx| {
4790 assert_eq!(item3.read(cx).save_count, 0);
4791 assert_eq!(item3.read(cx).save_as_count, 0);
4792 assert_eq!(item3.read(cx).reload_count, 1);
4793 assert_eq!(pane.items_len(), 2);
4794 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
4795 });
4796 assert!(cx.has_pending_prompt());
4797
4798 // Confirm saving item 4.
4799 cx.simulate_prompt_answer(0);
4800 cx.executor().run_until_parked();
4801
4802 // There's a prompt for a path for item 4.
4803 cx.simulate_new_path_selection(|_| Some(Default::default()));
4804 close_items.await.unwrap();
4805
4806 // The requested items are closed.
4807 pane.update(cx, |pane, cx| {
4808 assert_eq!(item4.read(cx).save_count, 0);
4809 assert_eq!(item4.read(cx).save_as_count, 1);
4810 assert_eq!(item4.read(cx).reload_count, 0);
4811 assert_eq!(pane.items_len(), 1);
4812 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
4813 });
4814 }
4815
4816 #[gpui::test]
4817 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
4818 init_test(cx);
4819
4820 let fs = FakeFs::new(cx.executor());
4821 let project = Project::test(fs, [], cx).await;
4822 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4823
4824 // Create several workspace items with single project entries, and two
4825 // workspace items with multiple project entries.
4826 let single_entry_items = (0..=4)
4827 .map(|project_entry_id| {
4828 cx.new_view(|cx| {
4829 TestItem::new(cx)
4830 .with_dirty(true)
4831 .with_project_items(&[TestProjectItem::new(
4832 project_entry_id,
4833 &format!("{project_entry_id}.txt"),
4834 cx,
4835 )])
4836 })
4837 })
4838 .collect::<Vec<_>>();
4839 let item_2_3 = cx.new_view(|cx| {
4840 TestItem::new(cx)
4841 .with_dirty(true)
4842 .with_singleton(false)
4843 .with_project_items(&[
4844 single_entry_items[2].read(cx).project_items[0].clone(),
4845 single_entry_items[3].read(cx).project_items[0].clone(),
4846 ])
4847 });
4848 let item_3_4 = cx.new_view(|cx| {
4849 TestItem::new(cx)
4850 .with_dirty(true)
4851 .with_singleton(false)
4852 .with_project_items(&[
4853 single_entry_items[3].read(cx).project_items[0].clone(),
4854 single_entry_items[4].read(cx).project_items[0].clone(),
4855 ])
4856 });
4857
4858 // Create two panes that contain the following project entries:
4859 // left pane:
4860 // multi-entry items: (2, 3)
4861 // single-entry items: 0, 1, 2, 3, 4
4862 // right pane:
4863 // single-entry items: 1
4864 // multi-entry items: (3, 4)
4865 let left_pane = workspace.update(cx, |workspace, cx| {
4866 let left_pane = workspace.active_pane().clone();
4867 workspace.add_item(Box::new(item_2_3.clone()), cx);
4868 for item in single_entry_items {
4869 workspace.add_item(Box::new(item), cx);
4870 }
4871 left_pane.update(cx, |pane, cx| {
4872 pane.activate_item(2, true, true, cx);
4873 });
4874
4875 let right_pane = workspace
4876 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
4877 .unwrap();
4878
4879 right_pane.update(cx, |pane, cx| {
4880 pane.add_item(Box::new(item_3_4.clone()), true, true, None, cx);
4881 });
4882
4883 left_pane
4884 });
4885
4886 cx.focus_view(&left_pane);
4887
4888 // When closing all of the items in the left pane, we should be prompted twice:
4889 // once for project entry 0, and once for project entry 2. Project entries 1,
4890 // 3, and 4 are all still open in the other paten. After those two
4891 // prompts, the task should complete.
4892
4893 let close = left_pane.update(cx, |pane, cx| {
4894 pane.close_all_items(&CloseAllItems::default(), cx).unwrap()
4895 });
4896 cx.executor().run_until_parked();
4897
4898 // Discard "Save all" prompt
4899 cx.simulate_prompt_answer(2);
4900
4901 cx.executor().run_until_parked();
4902 left_pane.update(cx, |pane, cx| {
4903 assert_eq!(
4904 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4905 &[ProjectEntryId::from_proto(0)]
4906 );
4907 });
4908 cx.simulate_prompt_answer(0);
4909
4910 cx.executor().run_until_parked();
4911 left_pane.update(cx, |pane, cx| {
4912 assert_eq!(
4913 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4914 &[ProjectEntryId::from_proto(2)]
4915 );
4916 });
4917 cx.simulate_prompt_answer(0);
4918
4919 cx.executor().run_until_parked();
4920 close.await.unwrap();
4921 left_pane.update(cx, |pane, _| {
4922 assert_eq!(pane.items_len(), 0);
4923 });
4924 }
4925
4926 #[gpui::test]
4927 async fn test_autosave(cx: &mut gpui::TestAppContext) {
4928 init_test(cx);
4929
4930 let fs = FakeFs::new(cx.executor());
4931 let project = Project::test(fs, [], cx).await;
4932 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4933 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4934
4935 let item = cx.new_view(|cx| {
4936 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4937 });
4938 let item_id = item.entity_id();
4939 workspace.update(cx, |workspace, cx| {
4940 workspace.add_item(Box::new(item.clone()), cx);
4941 });
4942
4943 // Autosave on window change.
4944 item.update(cx, |item, cx| {
4945 cx.update_global(|settings: &mut SettingsStore, cx| {
4946 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4947 settings.autosave = Some(AutosaveSetting::OnWindowChange);
4948 })
4949 });
4950 item.is_dirty = true;
4951 });
4952
4953 // Deactivating the window saves the file.
4954 cx.deactivate_window();
4955 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
4956
4957 // Autosave on focus change.
4958 item.update(cx, |item, cx| {
4959 cx.focus_self();
4960 cx.update_global(|settings: &mut SettingsStore, cx| {
4961 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4962 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4963 })
4964 });
4965 item.is_dirty = true;
4966 });
4967
4968 // Blurring the item saves the file.
4969 item.update(cx, |_, cx| cx.blur());
4970 cx.executor().run_until_parked();
4971 item.update(cx, |item, _| assert_eq!(item.save_count, 2));
4972
4973 // Deactivating the window still saves the file.
4974 cx.update(|cx| cx.activate_window());
4975 item.update(cx, |item, cx| {
4976 cx.focus_self();
4977 item.is_dirty = true;
4978 });
4979 cx.deactivate_window();
4980
4981 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
4982
4983 // Autosave after delay.
4984 item.update(cx, |item, cx| {
4985 cx.update_global(|settings: &mut SettingsStore, cx| {
4986 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4987 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
4988 })
4989 });
4990 item.is_dirty = true;
4991 cx.emit(ItemEvent::Edit);
4992 });
4993
4994 // Delay hasn't fully expired, so the file is still dirty and unsaved.
4995 cx.executor().advance_clock(Duration::from_millis(250));
4996 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
4997
4998 // After delay expires, the file is saved.
4999 cx.executor().advance_clock(Duration::from_millis(250));
5000 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
5001
5002 // Autosave on focus change, ensuring closing the tab counts as such.
5003 item.update(cx, |item, cx| {
5004 cx.update_global(|settings: &mut SettingsStore, cx| {
5005 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5006 settings.autosave = Some(AutosaveSetting::OnFocusChange);
5007 })
5008 });
5009 item.is_dirty = true;
5010 });
5011
5012 pane.update(cx, |pane, cx| {
5013 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
5014 })
5015 .await
5016 .unwrap();
5017 assert!(!cx.has_pending_prompt());
5018 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
5019
5020 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
5021 workspace.update(cx, |workspace, cx| {
5022 workspace.add_item(Box::new(item.clone()), cx);
5023 });
5024 item.update(cx, |item, cx| {
5025 item.project_items[0].update(cx, |item, _| {
5026 item.entry_id = None;
5027 });
5028 item.is_dirty = true;
5029 cx.blur();
5030 });
5031 cx.run_until_parked();
5032 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
5033
5034 // Ensure autosave is prevented for deleted files also when closing the buffer.
5035 let _close_items = pane.update(cx, |pane, cx| {
5036 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
5037 });
5038 cx.run_until_parked();
5039 assert!(cx.has_pending_prompt());
5040 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
5041 }
5042
5043 #[gpui::test]
5044 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
5045 init_test(cx);
5046
5047 let fs = FakeFs::new(cx.executor());
5048
5049 let project = Project::test(fs, [], cx).await;
5050 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5051
5052 let item = cx.new_view(|cx| {
5053 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5054 });
5055 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5056 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
5057 let toolbar_notify_count = Rc::new(RefCell::new(0));
5058
5059 workspace.update(cx, |workspace, cx| {
5060 workspace.add_item(Box::new(item.clone()), cx);
5061 let toolbar_notification_count = toolbar_notify_count.clone();
5062 cx.observe(&toolbar, move |_, _, _| {
5063 *toolbar_notification_count.borrow_mut() += 1
5064 })
5065 .detach();
5066 });
5067
5068 pane.update(cx, |pane, _| {
5069 assert!(!pane.can_navigate_backward());
5070 assert!(!pane.can_navigate_forward());
5071 });
5072
5073 item.update(cx, |item, cx| {
5074 item.set_state("one".to_string(), cx);
5075 });
5076
5077 // Toolbar must be notified to re-render the navigation buttons
5078 assert_eq!(*toolbar_notify_count.borrow(), 1);
5079
5080 pane.update(cx, |pane, _| {
5081 assert!(pane.can_navigate_backward());
5082 assert!(!pane.can_navigate_forward());
5083 });
5084
5085 workspace
5086 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
5087 .await
5088 .unwrap();
5089
5090 assert_eq!(*toolbar_notify_count.borrow(), 2);
5091 pane.update(cx, |pane, _| {
5092 assert!(!pane.can_navigate_backward());
5093 assert!(pane.can_navigate_forward());
5094 });
5095 }
5096
5097 #[gpui::test]
5098 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
5099 init_test(cx);
5100 let fs = FakeFs::new(cx.executor());
5101
5102 let project = Project::test(fs, [], cx).await;
5103 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5104
5105 let panel = workspace.update(cx, |workspace, cx| {
5106 let panel = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
5107 workspace.add_panel(panel.clone(), cx);
5108
5109 workspace
5110 .right_dock()
5111 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5112
5113 panel
5114 });
5115
5116 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5117 pane.update(cx, |pane, cx| {
5118 let item = cx.new_view(|cx| TestItem::new(cx));
5119 pane.add_item(Box::new(item), true, true, None, cx);
5120 });
5121
5122 // Transfer focus from center to panel
5123 workspace.update(cx, |workspace, cx| {
5124 workspace.toggle_panel_focus::<TestPanel>(cx);
5125 });
5126
5127 workspace.update(cx, |workspace, cx| {
5128 assert!(workspace.right_dock().read(cx).is_open());
5129 assert!(!panel.is_zoomed(cx));
5130 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5131 });
5132
5133 // Transfer focus from panel to center
5134 workspace.update(cx, |workspace, cx| {
5135 workspace.toggle_panel_focus::<TestPanel>(cx);
5136 });
5137
5138 workspace.update(cx, |workspace, cx| {
5139 assert!(workspace.right_dock().read(cx).is_open());
5140 assert!(!panel.is_zoomed(cx));
5141 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5142 });
5143
5144 // Close the dock
5145 workspace.update(cx, |workspace, cx| {
5146 workspace.toggle_dock(DockPosition::Right, cx);
5147 });
5148
5149 workspace.update(cx, |workspace, cx| {
5150 assert!(!workspace.right_dock().read(cx).is_open());
5151 assert!(!panel.is_zoomed(cx));
5152 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5153 });
5154
5155 // Open the dock
5156 workspace.update(cx, |workspace, cx| {
5157 workspace.toggle_dock(DockPosition::Right, cx);
5158 });
5159
5160 workspace.update(cx, |workspace, cx| {
5161 assert!(workspace.right_dock().read(cx).is_open());
5162 assert!(!panel.is_zoomed(cx));
5163 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5164 });
5165
5166 // Focus and zoom panel
5167 panel.update(cx, |panel, cx| {
5168 cx.focus_self();
5169 panel.set_zoomed(true, cx)
5170 });
5171
5172 workspace.update(cx, |workspace, cx| {
5173 assert!(workspace.right_dock().read(cx).is_open());
5174 assert!(panel.is_zoomed(cx));
5175 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5176 });
5177
5178 // Transfer focus to the center closes the dock
5179 workspace.update(cx, |workspace, cx| {
5180 workspace.toggle_panel_focus::<TestPanel>(cx);
5181 });
5182
5183 workspace.update(cx, |workspace, cx| {
5184 assert!(!workspace.right_dock().read(cx).is_open());
5185 assert!(panel.is_zoomed(cx));
5186 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5187 });
5188
5189 // Transferring focus back to the panel keeps it zoomed
5190 workspace.update(cx, |workspace, cx| {
5191 workspace.toggle_panel_focus::<TestPanel>(cx);
5192 });
5193
5194 workspace.update(cx, |workspace, cx| {
5195 assert!(workspace.right_dock().read(cx).is_open());
5196 assert!(panel.is_zoomed(cx));
5197 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5198 });
5199
5200 // Close the dock while it is zoomed
5201 workspace.update(cx, |workspace, cx| {
5202 workspace.toggle_dock(DockPosition::Right, cx)
5203 });
5204
5205 workspace.update(cx, |workspace, cx| {
5206 assert!(!workspace.right_dock().read(cx).is_open());
5207 assert!(panel.is_zoomed(cx));
5208 assert!(workspace.zoomed.is_none());
5209 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5210 });
5211
5212 // Opening the dock, when it's zoomed, retains focus
5213 workspace.update(cx, |workspace, cx| {
5214 workspace.toggle_dock(DockPosition::Right, cx)
5215 });
5216
5217 workspace.update(cx, |workspace, cx| {
5218 assert!(workspace.right_dock().read(cx).is_open());
5219 assert!(panel.is_zoomed(cx));
5220 assert!(workspace.zoomed.is_some());
5221 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5222 });
5223
5224 // Unzoom and close the panel, zoom the active pane.
5225 panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
5226 workspace.update(cx, |workspace, cx| {
5227 workspace.toggle_dock(DockPosition::Right, cx)
5228 });
5229 pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
5230
5231 // Opening a dock unzooms the pane.
5232 workspace.update(cx, |workspace, cx| {
5233 workspace.toggle_dock(DockPosition::Right, cx)
5234 });
5235 workspace.update(cx, |workspace, cx| {
5236 let pane = pane.read(cx);
5237 assert!(!pane.is_zoomed());
5238 assert!(!pane.focus_handle(cx).is_focused(cx));
5239 assert!(workspace.right_dock().read(cx).is_open());
5240 assert!(workspace.zoomed.is_none());
5241 });
5242 }
5243
5244 struct TestModal(FocusHandle);
5245
5246 impl TestModal {
5247 fn new(cx: &mut ViewContext<Self>) -> Self {
5248 Self(cx.focus_handle())
5249 }
5250 }
5251
5252 impl EventEmitter<DismissEvent> for TestModal {}
5253
5254 impl FocusableView for TestModal {
5255 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
5256 self.0.clone()
5257 }
5258 }
5259
5260 impl ModalView for TestModal {}
5261
5262 impl Render for TestModal {
5263 fn render(&mut self, _cx: &mut ViewContext<TestModal>) -> impl IntoElement {
5264 div().track_focus(&self.0)
5265 }
5266 }
5267
5268 #[gpui::test]
5269 async fn test_panels(cx: &mut gpui::TestAppContext) {
5270 init_test(cx);
5271 let fs = FakeFs::new(cx.executor());
5272
5273 let project = Project::test(fs, [], cx).await;
5274 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5275
5276 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
5277 let panel_1 = cx.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
5278 workspace.add_panel(panel_1.clone(), cx);
5279 workspace
5280 .left_dock()
5281 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
5282 let panel_2 = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
5283 workspace.add_panel(panel_2.clone(), cx);
5284 workspace
5285 .right_dock()
5286 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5287
5288 let left_dock = workspace.left_dock();
5289 assert_eq!(
5290 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5291 panel_1.panel_id()
5292 );
5293 assert_eq!(
5294 left_dock.read(cx).active_panel_size(cx).unwrap(),
5295 panel_1.size(cx)
5296 );
5297
5298 left_dock.update(cx, |left_dock, cx| {
5299 left_dock.resize_active_panel(Some(px(1337.)), cx)
5300 });
5301 assert_eq!(
5302 workspace
5303 .right_dock()
5304 .read(cx)
5305 .visible_panel()
5306 .unwrap()
5307 .panel_id(),
5308 panel_2.panel_id(),
5309 );
5310
5311 (panel_1, panel_2)
5312 });
5313
5314 // Move panel_1 to the right
5315 panel_1.update(cx, |panel_1, cx| {
5316 panel_1.set_position(DockPosition::Right, cx)
5317 });
5318
5319 workspace.update(cx, |workspace, cx| {
5320 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
5321 // Since it was the only panel on the left, the left dock should now be closed.
5322 assert!(!workspace.left_dock().read(cx).is_open());
5323 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
5324 let right_dock = workspace.right_dock();
5325 assert_eq!(
5326 right_dock.read(cx).visible_panel().unwrap().panel_id(),
5327 panel_1.panel_id()
5328 );
5329 assert_eq!(
5330 right_dock.read(cx).active_panel_size(cx).unwrap(),
5331 px(1337.)
5332 );
5333
5334 // Now we move panel_2 to the left
5335 panel_2.set_position(DockPosition::Left, cx);
5336 });
5337
5338 workspace.update(cx, |workspace, cx| {
5339 // Since panel_2 was not visible on the right, we don't open the left dock.
5340 assert!(!workspace.left_dock().read(cx).is_open());
5341 // And the right dock is unaffected in it's displaying of panel_1
5342 assert!(workspace.right_dock().read(cx).is_open());
5343 assert_eq!(
5344 workspace
5345 .right_dock()
5346 .read(cx)
5347 .visible_panel()
5348 .unwrap()
5349 .panel_id(),
5350 panel_1.panel_id(),
5351 );
5352 });
5353
5354 // Move panel_1 back to the left
5355 panel_1.update(cx, |panel_1, cx| {
5356 panel_1.set_position(DockPosition::Left, cx)
5357 });
5358
5359 workspace.update(cx, |workspace, cx| {
5360 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
5361 let left_dock = workspace.left_dock();
5362 assert!(left_dock.read(cx).is_open());
5363 assert_eq!(
5364 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5365 panel_1.panel_id()
5366 );
5367 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), px(1337.));
5368 // And the right dock should be closed as it no longer has any panels.
5369 assert!(!workspace.right_dock().read(cx).is_open());
5370
5371 // Now we move panel_1 to the bottom
5372 panel_1.set_position(DockPosition::Bottom, cx);
5373 });
5374
5375 workspace.update(cx, |workspace, cx| {
5376 // Since panel_1 was visible on the left, we close the left dock.
5377 assert!(!workspace.left_dock().read(cx).is_open());
5378 // The bottom dock is sized based on the panel's default size,
5379 // since the panel orientation changed from vertical to horizontal.
5380 let bottom_dock = workspace.bottom_dock();
5381 assert_eq!(
5382 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
5383 panel_1.size(cx),
5384 );
5385 // Close bottom dock and move panel_1 back to the left.
5386 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
5387 panel_1.set_position(DockPosition::Left, cx);
5388 });
5389
5390 // Emit activated event on panel 1
5391 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
5392
5393 // Now the left dock is open and panel_1 is active and focused.
5394 workspace.update(cx, |workspace, cx| {
5395 let left_dock = workspace.left_dock();
5396 assert!(left_dock.read(cx).is_open());
5397 assert_eq!(
5398 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5399 panel_1.panel_id(),
5400 );
5401 assert!(panel_1.focus_handle(cx).is_focused(cx));
5402 });
5403
5404 // Emit closed event on panel 2, which is not active
5405 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
5406
5407 // Wo don't close the left dock, because panel_2 wasn't the active panel
5408 workspace.update(cx, |workspace, cx| {
5409 let left_dock = workspace.left_dock();
5410 assert!(left_dock.read(cx).is_open());
5411 assert_eq!(
5412 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5413 panel_1.panel_id(),
5414 );
5415 });
5416
5417 // Emitting a ZoomIn event shows the panel as zoomed.
5418 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
5419 workspace.update(cx, |workspace, _| {
5420 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5421 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
5422 });
5423
5424 // Move panel to another dock while it is zoomed
5425 panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
5426 workspace.update(cx, |workspace, _| {
5427 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5428
5429 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5430 });
5431
5432 // This is a helper for getting a:
5433 // - valid focus on an element,
5434 // - that isn't a part of the panes and panels system of the Workspace,
5435 // - and doesn't trigger the 'on_focus_lost' API.
5436 let focus_other_view = {
5437 let workspace = workspace.clone();
5438 move |cx: &mut VisualTestContext| {
5439 workspace.update(cx, |workspace, cx| {
5440 if let Some(_) = workspace.active_modal::<TestModal>(cx) {
5441 workspace.toggle_modal(cx, TestModal::new);
5442 workspace.toggle_modal(cx, TestModal::new);
5443 } else {
5444 workspace.toggle_modal(cx, TestModal::new);
5445 }
5446 })
5447 }
5448 };
5449
5450 // If focus is transferred to another view that's not a panel or another pane, we still show
5451 // the panel as zoomed.
5452 focus_other_view(cx);
5453 workspace.update(cx, |workspace, _| {
5454 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5455 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5456 });
5457
5458 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
5459 workspace.update(cx, |_, cx| cx.focus_self());
5460 workspace.update(cx, |workspace, _| {
5461 assert_eq!(workspace.zoomed, None);
5462 assert_eq!(workspace.zoomed_position, None);
5463 });
5464
5465 // If focus is transferred again to another view that's not a panel or a pane, we won't
5466 // show the panel as zoomed because it wasn't zoomed before.
5467 focus_other_view(cx);
5468 workspace.update(cx, |workspace, _| {
5469 assert_eq!(workspace.zoomed, None);
5470 assert_eq!(workspace.zoomed_position, None);
5471 });
5472
5473 // When the panel is activated, it is zoomed again.
5474 cx.dispatch_action(ToggleRightDock);
5475 workspace.update(cx, |workspace, _| {
5476 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5477 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5478 });
5479
5480 // Emitting a ZoomOut event unzooms the panel.
5481 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
5482 workspace.update(cx, |workspace, _| {
5483 assert_eq!(workspace.zoomed, None);
5484 assert_eq!(workspace.zoomed_position, None);
5485 });
5486
5487 // Emit closed event on panel 1, which is active
5488 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
5489
5490 // Now the left dock is closed, because panel_1 was the active panel
5491 workspace.update(cx, |workspace, cx| {
5492 let right_dock = workspace.right_dock();
5493 assert!(!right_dock.read(cx).is_open());
5494 });
5495 }
5496
5497 pub fn init_test(cx: &mut TestAppContext) {
5498 cx.update(|cx| {
5499 let settings_store = SettingsStore::test(cx);
5500 cx.set_global(settings_store);
5501 theme::init(theme::LoadThemes::JustBase, cx);
5502 language::init(cx);
5503 crate::init_settings(cx);
5504 Project::init_settings(cx);
5505 });
5506 }
5507}