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