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