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