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