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