1pub mod dock;
2pub mod item;
3mod modal_layer;
4pub mod notifications;
5pub mod pane;
6pub mod pane_group;
7mod persistence;
8pub mod searchable;
9pub mod shared_screen;
10mod status_bar;
11mod toolbar;
12mod workspace_settings;
13
14use anyhow::{anyhow, Context as _, Result};
15use call::{call_settings::CallSettings, ActiveCall};
16use client::{
17 proto::{self, ErrorCode, PeerId},
18 Client, ErrorExt, Status, TypedEnvelope, UserStore,
19};
20use collections::{hash_map, HashMap, HashSet};
21use derive_more::{Deref, DerefMut};
22use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
23use futures::{
24 channel::{mpsc, oneshot},
25 future::try_join_all,
26 Future, FutureExt, StreamExt,
27};
28use gpui::{
29 actions, canvas, div, impl_actions, point, px, size, Action, AnyElement, AnyModel, AnyView,
30 AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Context, Div,
31 DragMoveEvent, Element, ElementContext, Entity, EntityId, EventEmitter, FocusHandle,
32 FocusableView, Global, GlobalPixels, InteractiveElement, IntoElement, KeyContext, LayoutId,
33 ManagedView, Model, ModelContext, ParentElement, PathPromptOptions, Pixels, Point, PromptLevel,
34 Render, SharedString, Size, Styled, Subscription, Task, View, ViewContext, VisualContext,
35 WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions,
36};
37use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
38use itertools::Itertools;
39use language::{LanguageRegistry, Rope};
40use lazy_static::lazy_static;
41pub use modal_layer::*;
42use node_runtime::NodeRuntime;
43use notifications::{simple_message_notification::MessageNotification, NotificationHandle};
44pub use pane::*;
45pub use pane_group::*;
46use persistence::{model::SerializedWorkspace, SerializedWindowsBounds, DB};
47pub use persistence::{
48 model::{ItemId, WorkspaceLocation},
49 WorkspaceDb, DB as WORKSPACE_DB,
50};
51use postage::stream::Stream;
52use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
53use serde::Deserialize;
54use settings::Settings;
55use shared_screen::SharedScreen;
56use status_bar::StatusBar;
57pub use status_bar::StatusItemView;
58use std::{
59 any::TypeId,
60 borrow::Cow,
61 cmp, env,
62 path::{Path, PathBuf},
63 sync::Weak,
64 sync::{atomic::AtomicUsize, Arc},
65 time::Duration,
66};
67use theme::{ActiveTheme, SystemAppearance, ThemeSettings};
68pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
69pub use ui;
70use ui::Label;
71use util::ResultExt;
72use uuid::Uuid;
73pub use workspace_settings::{AutosaveSetting, WorkspaceSettings};
74
75use crate::persistence::{
76 model::{DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup},
77 SerializedAxis,
78};
79
80lazy_static! {
81 static ref ZED_WINDOW_SIZE: Option<Size<GlobalPixels>> = env::var("ZED_WINDOW_SIZE")
82 .ok()
83 .as_deref()
84 .and_then(parse_pixel_size_env_var);
85 static ref ZED_WINDOW_POSITION: Option<Point<GlobalPixels>> = env::var("ZED_WINDOW_POSITION")
86 .ok()
87 .as_deref()
88 .and_then(parse_pixel_position_env_var);
89}
90
91#[derive(Clone, PartialEq)]
92pub struct RemoveWorktreeFromProject(pub WorktreeId);
93
94actions!(
95 workspace,
96 [
97 Open,
98 NewFile,
99 NewWindow,
100 CloseWindow,
101 CloseInactiveTabsAndPanes,
102 AddFolderToProject,
103 Unfollow,
104 SaveAs,
105 ReloadActiveItem,
106 ActivatePreviousPane,
107 ActivateNextPane,
108 FollowNextCollaborator,
109 NewTerminal,
110 NewCenterTerminal,
111 ToggleTerminalFocus,
112 NewSearch,
113 Feedback,
114 Restart,
115 Welcome,
116 ToggleZoom,
117 ToggleLeftDock,
118 ToggleRightDock,
119 ToggleBottomDock,
120 CloseAllDocks,
121 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
3980actions!(collab, [OpenChannelNotes]);
3981
3982async fn join_channel_internal(
3983 channel_id: u64,
3984 app_state: &Arc<AppState>,
3985 requesting_window: Option<WindowHandle<Workspace>>,
3986 active_call: &Model<ActiveCall>,
3987 cx: &mut AsyncAppContext,
3988) -> Result<bool> {
3989 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
3990 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
3991 return (false, None);
3992 };
3993
3994 let already_in_channel = room.channel_id() == Some(channel_id);
3995 let should_prompt = room.is_sharing_project()
3996 && room.remote_participants().len() > 0
3997 && !already_in_channel;
3998 let open_room = if already_in_channel {
3999 active_call.room().cloned()
4000 } else {
4001 None
4002 };
4003 (should_prompt, open_room)
4004 })?;
4005
4006 if let Some(room) = open_room {
4007 let task = room.update(cx, |room, cx| {
4008 if let Some((project, host)) = room.most_active_project(cx) {
4009 return Some(join_remote_project(project, host, app_state.clone(), cx));
4010 }
4011
4012 None
4013 })?;
4014 if let Some(task) = task {
4015 task.await?;
4016 }
4017 return anyhow::Ok(true);
4018 }
4019
4020 if should_prompt {
4021 if let Some(workspace) = requesting_window {
4022 let answer = workspace
4023 .update(cx, |_, cx| {
4024 cx.prompt(
4025 PromptLevel::Warning,
4026 "Do you want to switch channels?",
4027 Some("Leaving this call will unshare your current project."),
4028 &["Yes, Join Channel", "Cancel"],
4029 )
4030 })?
4031 .await;
4032
4033 if answer == Ok(1) {
4034 return Ok(false);
4035 }
4036 } else {
4037 return Ok(false); // unreachable!() hopefully
4038 }
4039 }
4040
4041 let client = cx.update(|cx| active_call.read(cx).client())?;
4042
4043 let mut client_status = client.status();
4044
4045 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
4046 'outer: loop {
4047 let Some(status) = client_status.recv().await else {
4048 return Err(anyhow!("error connecting"));
4049 };
4050
4051 match status {
4052 Status::Connecting
4053 | Status::Authenticating
4054 | Status::Reconnecting
4055 | Status::Reauthenticating => continue,
4056 Status::Connected { .. } => break 'outer,
4057 Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
4058 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
4059 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
4060 return Err(ErrorCode::Disconnected.into())
4061 }
4062 }
4063 }
4064
4065 let room = active_call
4066 .update(cx, |active_call, cx| {
4067 active_call.join_channel(channel_id, cx)
4068 })?
4069 .await?;
4070
4071 let Some(room) = room else {
4072 return anyhow::Ok(true);
4073 };
4074
4075 room.update(cx, |room, _| room.room_update_completed())?
4076 .await;
4077
4078 let task = room.update(cx, |room, cx| {
4079 if let Some((project, host)) = room.most_active_project(cx) {
4080 return Some(join_remote_project(project, host, app_state.clone(), cx));
4081 }
4082
4083 // if you are the first to join a channel, share your project
4084 if room.remote_participants().len() == 0 && !room.local_participant_is_guest() {
4085 if let Some(workspace) = requesting_window {
4086 let project = workspace.update(cx, |workspace, cx| {
4087 if !CallSettings::get_global(cx).share_on_join {
4088 return None;
4089 }
4090 let project = workspace.project.read(cx);
4091 if project.is_local()
4092 && project.visible_worktrees(cx).any(|tree| {
4093 tree.read(cx)
4094 .root_entry()
4095 .map_or(false, |entry| entry.is_dir())
4096 })
4097 {
4098 Some(workspace.project.clone())
4099 } else {
4100 None
4101 }
4102 });
4103 if let Ok(Some(project)) = project {
4104 return Some(cx.spawn(|room, mut cx| async move {
4105 room.update(&mut cx, |room, cx| room.share_project(project, cx))?
4106 .await?;
4107 Ok(())
4108 }));
4109 }
4110 }
4111 }
4112
4113 None
4114 })?;
4115 if let Some(task) = task {
4116 task.await?;
4117 return anyhow::Ok(true);
4118 }
4119 anyhow::Ok(false)
4120}
4121
4122pub fn join_channel(
4123 channel_id: u64,
4124 app_state: Arc<AppState>,
4125 requesting_window: Option<WindowHandle<Workspace>>,
4126 cx: &mut AppContext,
4127) -> Task<Result<()>> {
4128 let active_call = ActiveCall::global(cx);
4129 cx.spawn(|mut cx| async move {
4130 let result = join_channel_internal(
4131 channel_id,
4132 &app_state,
4133 requesting_window,
4134 &active_call,
4135 &mut cx,
4136 )
4137 .await;
4138
4139 // join channel succeeded, and opened a window
4140 if matches!(result, Ok(true)) {
4141 return anyhow::Ok(());
4142 }
4143
4144 // find an existing workspace to focus and show call controls
4145 let mut active_window =
4146 requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
4147 if active_window.is_none() {
4148 // no open workspaces, make one to show the error in (blergh)
4149 let (window_handle, _) = cx
4150 .update(|cx| {
4151 Workspace::new_local(vec![], app_state.clone(), requesting_window, cx)
4152 })?
4153 .await?;
4154
4155 if result.is_ok() {
4156 cx.update(|cx| {
4157 cx.dispatch_action(&OpenChannelNotes);
4158 }).log_err();
4159 }
4160
4161 active_window = Some(window_handle);
4162 }
4163
4164 if let Err(err) = result {
4165 log::error!("failed to join channel: {}", err);
4166 if let Some(active_window) = active_window {
4167 active_window
4168 .update(&mut cx, |_, cx| {
4169 let detail: SharedString = match err.error_code() {
4170 ErrorCode::SignedOut => {
4171 "Please sign in to continue.".into()
4172 },
4173 ErrorCode::UpgradeRequired => {
4174 "Your are running an unsupported version of Zed. Please update to continue.".into()
4175 },
4176 ErrorCode::NoSuchChannel => {
4177 "No matching channel was found. Please check the link and try again.".into()
4178 },
4179 ErrorCode::Forbidden => {
4180 "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
4181 },
4182 ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
4183 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(),
4184 _ => format!("{}\n\nPlease try again.", err).into(),
4185 };
4186 cx.prompt(
4187 PromptLevel::Critical,
4188 "Failed to join channel",
4189 Some(&detail),
4190 &["Ok"],
4191 )
4192 })?
4193 .await
4194 .ok();
4195 }
4196 }
4197
4198 // return ok, we showed the error to the user.
4199 return anyhow::Ok(());
4200 })
4201}
4202
4203pub async fn get_any_active_workspace(
4204 app_state: Arc<AppState>,
4205 mut cx: AsyncAppContext,
4206) -> anyhow::Result<WindowHandle<Workspace>> {
4207 // find an existing workspace to focus and show call controls
4208 let active_window = activate_any_workspace_window(&mut cx);
4209 if active_window.is_none() {
4210 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, cx))?
4211 .await?;
4212 }
4213 activate_any_workspace_window(&mut cx).context("could not open zed")
4214}
4215
4216fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandle<Workspace>> {
4217 cx.update(|cx| {
4218 for window in cx.windows() {
4219 if let Some(workspace_window) = window.downcast::<Workspace>() {
4220 workspace_window
4221 .update(cx, |_, cx| cx.activate_window())
4222 .ok();
4223 return Some(workspace_window);
4224 }
4225 }
4226 None
4227 })
4228 .ok()
4229 .flatten()
4230}
4231
4232#[allow(clippy::type_complexity)]
4233pub fn open_paths(
4234 abs_paths: &[PathBuf],
4235 app_state: &Arc<AppState>,
4236 requesting_window: Option<WindowHandle<Workspace>>,
4237 cx: &mut AppContext,
4238) -> Task<
4239 anyhow::Result<(
4240 WindowHandle<Workspace>,
4241 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
4242 )>,
4243> {
4244 let app_state = app_state.clone();
4245 let abs_paths = abs_paths.to_vec();
4246 // Open paths in existing workspace if possible
4247 let existing = activate_workspace_for_project(cx, {
4248 let abs_paths = abs_paths.clone();
4249 move |project, cx| project.contains_paths(&abs_paths, cx)
4250 });
4251 cx.spawn(move |mut cx| async move {
4252 if let Some(existing) = existing {
4253 Ok((
4254 existing.clone(),
4255 existing
4256 .update(&mut cx, |workspace, cx| {
4257 workspace.open_paths(abs_paths, OpenVisible::All, None, cx)
4258 })?
4259 .await,
4260 ))
4261 } else {
4262 cx.update(move |cx| {
4263 Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx)
4264 })?
4265 .await
4266 }
4267 })
4268}
4269
4270pub fn open_new(
4271 app_state: &Arc<AppState>,
4272 cx: &mut AppContext,
4273 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static + Send,
4274) -> Task<()> {
4275 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
4276 cx.spawn(|mut cx| async move {
4277 if let Some((workspace, opened_paths)) = task.await.log_err() {
4278 workspace
4279 .update(&mut cx, |workspace, cx| {
4280 if opened_paths.is_empty() {
4281 init(workspace, cx)
4282 }
4283 })
4284 .log_err();
4285 }
4286 })
4287}
4288
4289pub fn create_and_open_local_file(
4290 path: &'static Path,
4291 cx: &mut ViewContext<Workspace>,
4292 default_content: impl 'static + Send + FnOnce() -> Rope,
4293) -> Task<Result<Box<dyn ItemHandle>>> {
4294 cx.spawn(|workspace, mut cx| async move {
4295 let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
4296 if !fs.is_file(path).await {
4297 fs.create_file(path, Default::default()).await?;
4298 fs.save(path, &default_content(), Default::default())
4299 .await?;
4300 }
4301
4302 let mut items = workspace
4303 .update(&mut cx, |workspace, cx| {
4304 workspace.with_local_workspace(cx, |workspace, cx| {
4305 workspace.open_paths(vec![path.to_path_buf()], OpenVisible::None, None, cx)
4306 })
4307 })?
4308 .await?
4309 .await;
4310
4311 let item = items.pop().flatten();
4312 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
4313 })
4314}
4315
4316pub fn join_remote_project(
4317 project_id: u64,
4318 follow_user_id: u64,
4319 app_state: Arc<AppState>,
4320 cx: &mut AppContext,
4321) -> Task<Result<()>> {
4322 let windows = cx.windows();
4323 cx.spawn(|mut cx| async move {
4324 let existing_workspace = windows.into_iter().find_map(|window| {
4325 window.downcast::<Workspace>().and_then(|window| {
4326 window
4327 .update(&mut cx, |workspace, cx| {
4328 if workspace.project().read(cx).remote_id() == Some(project_id) {
4329 Some(window)
4330 } else {
4331 None
4332 }
4333 })
4334 .unwrap_or(None)
4335 })
4336 });
4337
4338 let workspace = if let Some(existing_workspace) = existing_workspace {
4339 existing_workspace
4340 } else {
4341 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
4342 let room = active_call
4343 .read_with(&cx, |call, _| call.room().cloned())?
4344 .ok_or_else(|| anyhow!("not in a call"))?;
4345 let project = room
4346 .update(&mut cx, |room, cx| {
4347 room.join_project(
4348 project_id,
4349 app_state.languages.clone(),
4350 app_state.fs.clone(),
4351 cx,
4352 )
4353 })?
4354 .await?;
4355
4356 let window_bounds_override = window_bounds_env_override(&cx);
4357 cx.update(|cx| {
4358 let options = (app_state.build_window_options)(window_bounds_override, None, cx);
4359 cx.open_window(options, |cx| {
4360 cx.new_view(|cx| Workspace::new(0, project, app_state.clone(), cx))
4361 })
4362 })?
4363 };
4364
4365 workspace.update(&mut cx, |workspace, cx| {
4366 cx.activate(true);
4367 cx.activate_window();
4368
4369 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
4370 let follow_peer_id = room
4371 .read(cx)
4372 .remote_participants()
4373 .iter()
4374 .find(|(_, participant)| participant.user.id == follow_user_id)
4375 .map(|(_, p)| p.peer_id)
4376 .or_else(|| {
4377 // If we couldn't follow the given user, follow the host instead.
4378 let collaborator = workspace
4379 .project()
4380 .read(cx)
4381 .collaborators()
4382 .values()
4383 .find(|collaborator| collaborator.replica_id == 0)?;
4384 Some(collaborator.peer_id)
4385 });
4386
4387 if let Some(follow_peer_id) = follow_peer_id {
4388 workspace.follow(follow_peer_id, cx);
4389 }
4390 }
4391 })?;
4392
4393 anyhow::Ok(())
4394 })
4395}
4396
4397pub fn restart(_: &Restart, cx: &mut AppContext) {
4398 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
4399 let mut workspace_windows = cx
4400 .windows()
4401 .into_iter()
4402 .filter_map(|window| window.downcast::<Workspace>())
4403 .collect::<Vec<_>>();
4404
4405 // If multiple windows have unsaved changes, and need a save prompt,
4406 // prompt in the active window before switching to a different window.
4407 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
4408
4409 let mut prompt = None;
4410 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
4411 prompt = window
4412 .update(cx, |_, cx| {
4413 cx.prompt(
4414 PromptLevel::Info,
4415 "Are you sure you want to restart?",
4416 None,
4417 &["Restart", "Cancel"],
4418 )
4419 })
4420 .ok();
4421 }
4422
4423 cx.spawn(|mut cx| async move {
4424 if let Some(prompt) = prompt {
4425 let answer = prompt.await?;
4426 if answer != 0 {
4427 return Ok(());
4428 }
4429 }
4430
4431 // If the user cancels any save prompt, then keep the app open.
4432 for window in workspace_windows {
4433 if let Ok(should_close) = window.update(&mut cx, |workspace, cx| {
4434 workspace.prepare_to_close(true, cx)
4435 }) {
4436 if !should_close.await? {
4437 return Ok(());
4438 }
4439 }
4440 }
4441
4442 cx.update(|cx| cx.restart())
4443 })
4444 .detach_and_log_err(cx);
4445}
4446
4447fn parse_pixel_position_env_var(value: &str) -> Option<Point<GlobalPixels>> {
4448 let mut parts = value.split(',');
4449 let x: usize = parts.next()?.parse().ok()?;
4450 let y: usize = parts.next()?.parse().ok()?;
4451 Some(point((x as f64).into(), (y as f64).into()))
4452}
4453
4454fn parse_pixel_size_env_var(value: &str) -> Option<Size<GlobalPixels>> {
4455 let mut parts = value.split(',');
4456 let width: usize = parts.next()?.parse().ok()?;
4457 let height: usize = parts.next()?.parse().ok()?;
4458 Some(size((width as f64).into(), (height as f64).into()))
4459}
4460
4461pub fn titlebar_height(cx: &mut WindowContext) -> Pixels {
4462 (1.75 * cx.rem_size()).max(px(32.))
4463}
4464
4465struct DisconnectedOverlay;
4466
4467impl Element for DisconnectedOverlay {
4468 type State = AnyElement;
4469
4470 fn request_layout(
4471 &mut self,
4472 _: Option<Self::State>,
4473 cx: &mut ElementContext,
4474 ) -> (LayoutId, Self::State) {
4475 let mut background = cx.theme().colors().elevated_surface_background;
4476 background.fade_out(0.2);
4477 let mut overlay = div()
4478 .bg(background)
4479 .absolute()
4480 .left_0()
4481 .top(titlebar_height(cx))
4482 .size_full()
4483 .flex()
4484 .items_center()
4485 .justify_center()
4486 .capture_any_mouse_down(|_, cx| cx.stop_propagation())
4487 .capture_any_mouse_up(|_, cx| cx.stop_propagation())
4488 .child(Label::new(
4489 "Your connection to the remote project has been lost.",
4490 ))
4491 .into_any();
4492 (overlay.request_layout(cx), overlay)
4493 }
4494
4495 fn paint(
4496 &mut self,
4497 bounds: Bounds<Pixels>,
4498 overlay: &mut Self::State,
4499 cx: &mut ElementContext,
4500 ) {
4501 cx.with_z_index(u16::MAX, |cx| {
4502 cx.add_opaque_layer(bounds);
4503 overlay.paint(cx);
4504 })
4505 }
4506}
4507
4508impl IntoElement for DisconnectedOverlay {
4509 type Element = Self;
4510
4511 fn element_id(&self) -> Option<ui::prelude::ElementId> {
4512 None
4513 }
4514
4515 fn into_element(self) -> Self::Element {
4516 self
4517 }
4518}
4519
4520#[cfg(test)]
4521mod tests {
4522 use std::{cell::RefCell, rc::Rc};
4523
4524 use super::*;
4525 use crate::{
4526 dock::{test::TestPanel, PanelEvent},
4527 item::{
4528 test::{TestItem, TestProjectItem},
4529 ItemEvent,
4530 },
4531 };
4532 use fs::FakeFs;
4533 use gpui::{px, DismissEvent, TestAppContext, VisualTestContext};
4534 use project::{Project, ProjectEntryId};
4535 use serde_json::json;
4536 use settings::SettingsStore;
4537
4538 #[gpui::test]
4539 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
4540 init_test(cx);
4541
4542 let fs = FakeFs::new(cx.executor());
4543 let project = Project::test(fs, [], cx).await;
4544 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4545
4546 // Adding an item with no ambiguity renders the tab without detail.
4547 let item1 = cx.new_view(|cx| {
4548 let mut item = TestItem::new(cx);
4549 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
4550 item
4551 });
4552 workspace.update(cx, |workspace, cx| {
4553 workspace.add_item(Box::new(item1.clone()), cx);
4554 });
4555 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
4556
4557 // Adding an item that creates ambiguity increases the level of detail on
4558 // both tabs.
4559 let item2 = cx.new_view(|cx| {
4560 let mut item = TestItem::new(cx);
4561 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4562 item
4563 });
4564 workspace.update(cx, |workspace, cx| {
4565 workspace.add_item(Box::new(item2.clone()), cx);
4566 });
4567 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4568 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4569
4570 // Adding an item that creates ambiguity increases the level of detail only
4571 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
4572 // we stop at the highest detail available.
4573 let item3 = cx.new_view(|cx| {
4574 let mut item = TestItem::new(cx);
4575 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4576 item
4577 });
4578 workspace.update(cx, |workspace, cx| {
4579 workspace.add_item(Box::new(item3.clone()), cx);
4580 });
4581 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4582 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4583 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4584 }
4585
4586 #[gpui::test]
4587 async fn test_tracking_active_path(cx: &mut TestAppContext) {
4588 init_test(cx);
4589
4590 let fs = FakeFs::new(cx.executor());
4591 fs.insert_tree(
4592 "/root1",
4593 json!({
4594 "one.txt": "",
4595 "two.txt": "",
4596 }),
4597 )
4598 .await;
4599 fs.insert_tree(
4600 "/root2",
4601 json!({
4602 "three.txt": "",
4603 }),
4604 )
4605 .await;
4606
4607 let project = Project::test(fs, ["root1".as_ref()], cx).await;
4608 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4609 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4610 let worktree_id = project.update(cx, |project, cx| {
4611 project.worktrees().next().unwrap().read(cx).id()
4612 });
4613
4614 let item1 = cx.new_view(|cx| {
4615 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
4616 });
4617 let item2 = cx.new_view(|cx| {
4618 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
4619 });
4620
4621 // Add an item to an empty pane
4622 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
4623 project.update(cx, |project, cx| {
4624 assert_eq!(
4625 project.active_entry(),
4626 project
4627 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4628 .map(|e| e.id)
4629 );
4630 });
4631 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
4632
4633 // Add a second item to a non-empty pane
4634 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
4635 assert_eq!(cx.window_title().as_deref(), Some("two.txt — root1"));
4636 project.update(cx, |project, cx| {
4637 assert_eq!(
4638 project.active_entry(),
4639 project
4640 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
4641 .map(|e| e.id)
4642 );
4643 });
4644
4645 // Close the active item
4646 pane.update(cx, |pane, cx| {
4647 pane.close_active_item(&Default::default(), cx).unwrap()
4648 })
4649 .await
4650 .unwrap();
4651 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
4652 project.update(cx, |project, cx| {
4653 assert_eq!(
4654 project.active_entry(),
4655 project
4656 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4657 .map(|e| e.id)
4658 );
4659 });
4660
4661 // Add a project folder
4662 project
4663 .update(cx, |project, cx| {
4664 project.find_or_create_local_worktree("/root2", true, cx)
4665 })
4666 .await
4667 .unwrap();
4668 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1, root2"));
4669
4670 // Remove a project folder
4671 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
4672 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root2"));
4673 }
4674
4675 #[gpui::test]
4676 async fn test_close_window(cx: &mut TestAppContext) {
4677 init_test(cx);
4678
4679 let fs = FakeFs::new(cx.executor());
4680 fs.insert_tree("/root", json!({ "one": "" })).await;
4681
4682 let project = Project::test(fs, ["root".as_ref()], cx).await;
4683 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4684
4685 // When there are no dirty items, there's nothing to do.
4686 let item1 = cx.new_view(|cx| TestItem::new(cx));
4687 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
4688 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4689 assert!(task.await.unwrap());
4690
4691 // When there are dirty untitled items, prompt to save each one. If the user
4692 // cancels any prompt, then abort.
4693 let item2 = cx.new_view(|cx| TestItem::new(cx).with_dirty(true));
4694 let item3 = cx.new_view(|cx| {
4695 TestItem::new(cx)
4696 .with_dirty(true)
4697 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4698 });
4699 workspace.update(cx, |w, cx| {
4700 w.add_item(Box::new(item2.clone()), cx);
4701 w.add_item(Box::new(item3.clone()), cx);
4702 });
4703 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4704 cx.executor().run_until_parked();
4705 cx.simulate_prompt_answer(2); // cancel save all
4706 cx.executor().run_until_parked();
4707 cx.simulate_prompt_answer(2); // cancel save all
4708 cx.executor().run_until_parked();
4709 assert!(!cx.has_pending_prompt());
4710 assert!(!task.await.unwrap());
4711 }
4712
4713 #[gpui::test]
4714 async fn test_close_pane_items(cx: &mut TestAppContext) {
4715 init_test(cx);
4716
4717 let fs = FakeFs::new(cx.executor());
4718
4719 let project = Project::test(fs, None, cx).await;
4720 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4721
4722 let item1 = cx.new_view(|cx| {
4723 TestItem::new(cx)
4724 .with_dirty(true)
4725 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4726 });
4727 let item2 = cx.new_view(|cx| {
4728 TestItem::new(cx)
4729 .with_dirty(true)
4730 .with_conflict(true)
4731 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
4732 });
4733 let item3 = cx.new_view(|cx| {
4734 TestItem::new(cx)
4735 .with_dirty(true)
4736 .with_conflict(true)
4737 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
4738 });
4739 let item4 = cx.new_view(|cx| {
4740 TestItem::new(cx)
4741 .with_dirty(true)
4742 .with_project_items(&[TestProjectItem::new_untitled(cx)])
4743 });
4744 let pane = workspace.update(cx, |workspace, cx| {
4745 workspace.add_item(Box::new(item1.clone()), cx);
4746 workspace.add_item(Box::new(item2.clone()), cx);
4747 workspace.add_item(Box::new(item3.clone()), cx);
4748 workspace.add_item(Box::new(item4.clone()), cx);
4749 workspace.active_pane().clone()
4750 });
4751
4752 let close_items = pane.update(cx, |pane, cx| {
4753 pane.activate_item(1, true, true, cx);
4754 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
4755 let item1_id = item1.item_id();
4756 let item3_id = item3.item_id();
4757 let item4_id = item4.item_id();
4758 pane.close_items(cx, SaveIntent::Close, move |id| {
4759 [item1_id, item3_id, item4_id].contains(&id)
4760 })
4761 });
4762 cx.executor().run_until_parked();
4763
4764 assert!(cx.has_pending_prompt());
4765 // Ignore "Save all" prompt
4766 cx.simulate_prompt_answer(2);
4767 cx.executor().run_until_parked();
4768 // There's a prompt to save item 1.
4769 pane.update(cx, |pane, _| {
4770 assert_eq!(pane.items_len(), 4);
4771 assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
4772 });
4773 // Confirm saving item 1.
4774 cx.simulate_prompt_answer(0);
4775 cx.executor().run_until_parked();
4776
4777 // Item 1 is saved. There's a prompt to save item 3.
4778 pane.update(cx, |pane, cx| {
4779 assert_eq!(item1.read(cx).save_count, 1);
4780 assert_eq!(item1.read(cx).save_as_count, 0);
4781 assert_eq!(item1.read(cx).reload_count, 0);
4782 assert_eq!(pane.items_len(), 3);
4783 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
4784 });
4785 assert!(cx.has_pending_prompt());
4786
4787 // Cancel saving item 3.
4788 cx.simulate_prompt_answer(1);
4789 cx.executor().run_until_parked();
4790
4791 // Item 3 is reloaded. There's a prompt to save item 4.
4792 pane.update(cx, |pane, cx| {
4793 assert_eq!(item3.read(cx).save_count, 0);
4794 assert_eq!(item3.read(cx).save_as_count, 0);
4795 assert_eq!(item3.read(cx).reload_count, 1);
4796 assert_eq!(pane.items_len(), 2);
4797 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
4798 });
4799 assert!(cx.has_pending_prompt());
4800
4801 // Confirm saving item 4.
4802 cx.simulate_prompt_answer(0);
4803 cx.executor().run_until_parked();
4804
4805 // There's a prompt for a path for item 4.
4806 cx.simulate_new_path_selection(|_| Some(Default::default()));
4807 close_items.await.unwrap();
4808
4809 // The requested items are closed.
4810 pane.update(cx, |pane, cx| {
4811 assert_eq!(item4.read(cx).save_count, 0);
4812 assert_eq!(item4.read(cx).save_as_count, 1);
4813 assert_eq!(item4.read(cx).reload_count, 0);
4814 assert_eq!(pane.items_len(), 1);
4815 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
4816 });
4817 }
4818
4819 #[gpui::test]
4820 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
4821 init_test(cx);
4822
4823 let fs = FakeFs::new(cx.executor());
4824 let project = Project::test(fs, [], cx).await;
4825 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4826
4827 // Create several workspace items with single project entries, and two
4828 // workspace items with multiple project entries.
4829 let single_entry_items = (0..=4)
4830 .map(|project_entry_id| {
4831 cx.new_view(|cx| {
4832 TestItem::new(cx)
4833 .with_dirty(true)
4834 .with_project_items(&[TestProjectItem::new(
4835 project_entry_id,
4836 &format!("{project_entry_id}.txt"),
4837 cx,
4838 )])
4839 })
4840 })
4841 .collect::<Vec<_>>();
4842 let item_2_3 = cx.new_view(|cx| {
4843 TestItem::new(cx)
4844 .with_dirty(true)
4845 .with_singleton(false)
4846 .with_project_items(&[
4847 single_entry_items[2].read(cx).project_items[0].clone(),
4848 single_entry_items[3].read(cx).project_items[0].clone(),
4849 ])
4850 });
4851 let item_3_4 = cx.new_view(|cx| {
4852 TestItem::new(cx)
4853 .with_dirty(true)
4854 .with_singleton(false)
4855 .with_project_items(&[
4856 single_entry_items[3].read(cx).project_items[0].clone(),
4857 single_entry_items[4].read(cx).project_items[0].clone(),
4858 ])
4859 });
4860
4861 // Create two panes that contain the following project entries:
4862 // left pane:
4863 // multi-entry items: (2, 3)
4864 // single-entry items: 0, 1, 2, 3, 4
4865 // right pane:
4866 // single-entry items: 1
4867 // multi-entry items: (3, 4)
4868 let left_pane = workspace.update(cx, |workspace, cx| {
4869 let left_pane = workspace.active_pane().clone();
4870 workspace.add_item(Box::new(item_2_3.clone()), cx);
4871 for item in single_entry_items {
4872 workspace.add_item(Box::new(item), cx);
4873 }
4874 left_pane.update(cx, |pane, cx| {
4875 pane.activate_item(2, true, true, cx);
4876 });
4877
4878 let right_pane = workspace
4879 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
4880 .unwrap();
4881
4882 right_pane.update(cx, |pane, cx| {
4883 pane.add_item(Box::new(item_3_4.clone()), true, true, None, cx);
4884 });
4885
4886 left_pane
4887 });
4888
4889 cx.focus_view(&left_pane);
4890
4891 // When closing all of the items in the left pane, we should be prompted twice:
4892 // once for project entry 0, and once for project entry 2. Project entries 1,
4893 // 3, and 4 are all still open in the other paten. After those two
4894 // prompts, the task should complete.
4895
4896 let close = left_pane.update(cx, |pane, cx| {
4897 pane.close_all_items(&CloseAllItems::default(), cx).unwrap()
4898 });
4899 cx.executor().run_until_parked();
4900
4901 // Discard "Save all" prompt
4902 cx.simulate_prompt_answer(2);
4903
4904 cx.executor().run_until_parked();
4905 left_pane.update(cx, |pane, cx| {
4906 assert_eq!(
4907 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4908 &[ProjectEntryId::from_proto(0)]
4909 );
4910 });
4911 cx.simulate_prompt_answer(0);
4912
4913 cx.executor().run_until_parked();
4914 left_pane.update(cx, |pane, cx| {
4915 assert_eq!(
4916 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4917 &[ProjectEntryId::from_proto(2)]
4918 );
4919 });
4920 cx.simulate_prompt_answer(0);
4921
4922 cx.executor().run_until_parked();
4923 close.await.unwrap();
4924 left_pane.update(cx, |pane, _| {
4925 assert_eq!(pane.items_len(), 0);
4926 });
4927 }
4928
4929 #[gpui::test]
4930 async fn test_autosave(cx: &mut gpui::TestAppContext) {
4931 init_test(cx);
4932
4933 let fs = FakeFs::new(cx.executor());
4934 let project = Project::test(fs, [], cx).await;
4935 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4936 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4937
4938 let item = cx.new_view(|cx| {
4939 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4940 });
4941 let item_id = item.entity_id();
4942 workspace.update(cx, |workspace, cx| {
4943 workspace.add_item(Box::new(item.clone()), cx);
4944 });
4945
4946 // Autosave on window change.
4947 item.update(cx, |item, cx| {
4948 cx.update_global(|settings: &mut SettingsStore, cx| {
4949 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4950 settings.autosave = Some(AutosaveSetting::OnWindowChange);
4951 })
4952 });
4953 item.is_dirty = true;
4954 });
4955
4956 // Deactivating the window saves the file.
4957 cx.deactivate_window();
4958 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
4959
4960 // Autosave on focus change.
4961 item.update(cx, |item, cx| {
4962 cx.focus_self();
4963 cx.update_global(|settings: &mut SettingsStore, cx| {
4964 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4965 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4966 })
4967 });
4968 item.is_dirty = true;
4969 });
4970
4971 // Blurring the item saves the file.
4972 item.update(cx, |_, cx| cx.blur());
4973 cx.executor().run_until_parked();
4974 item.update(cx, |item, _| assert_eq!(item.save_count, 2));
4975
4976 // Deactivating the window still saves the file.
4977 cx.update(|cx| cx.activate_window());
4978 item.update(cx, |item, cx| {
4979 cx.focus_self();
4980 item.is_dirty = true;
4981 });
4982 cx.deactivate_window();
4983
4984 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
4985
4986 // Autosave after delay.
4987 item.update(cx, |item, cx| {
4988 cx.update_global(|settings: &mut SettingsStore, cx| {
4989 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4990 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
4991 })
4992 });
4993 item.is_dirty = true;
4994 cx.emit(ItemEvent::Edit);
4995 });
4996
4997 // Delay hasn't fully expired, so the file is still dirty and unsaved.
4998 cx.executor().advance_clock(Duration::from_millis(250));
4999 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
5000
5001 // After delay expires, the file is saved.
5002 cx.executor().advance_clock(Duration::from_millis(250));
5003 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
5004
5005 // Autosave on focus change, ensuring closing the tab counts as such.
5006 item.update(cx, |item, cx| {
5007 cx.update_global(|settings: &mut SettingsStore, cx| {
5008 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5009 settings.autosave = Some(AutosaveSetting::OnFocusChange);
5010 })
5011 });
5012 item.is_dirty = true;
5013 });
5014
5015 pane.update(cx, |pane, cx| {
5016 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
5017 })
5018 .await
5019 .unwrap();
5020 assert!(!cx.has_pending_prompt());
5021 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
5022
5023 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
5024 workspace.update(cx, |workspace, cx| {
5025 workspace.add_item(Box::new(item.clone()), cx);
5026 });
5027 item.update(cx, |item, cx| {
5028 item.project_items[0].update(cx, |item, _| {
5029 item.entry_id = None;
5030 });
5031 item.is_dirty = true;
5032 cx.blur();
5033 });
5034 cx.run_until_parked();
5035 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
5036
5037 // Ensure autosave is prevented for deleted files also when closing the buffer.
5038 let _close_items = pane.update(cx, |pane, cx| {
5039 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
5040 });
5041 cx.run_until_parked();
5042 assert!(cx.has_pending_prompt());
5043 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
5044 }
5045
5046 #[gpui::test]
5047 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
5048 init_test(cx);
5049
5050 let fs = FakeFs::new(cx.executor());
5051
5052 let project = Project::test(fs, [], cx).await;
5053 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5054
5055 let item = cx.new_view(|cx| {
5056 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5057 });
5058 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5059 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
5060 let toolbar_notify_count = Rc::new(RefCell::new(0));
5061
5062 workspace.update(cx, |workspace, cx| {
5063 workspace.add_item(Box::new(item.clone()), cx);
5064 let toolbar_notification_count = toolbar_notify_count.clone();
5065 cx.observe(&toolbar, move |_, _, _| {
5066 *toolbar_notification_count.borrow_mut() += 1
5067 })
5068 .detach();
5069 });
5070
5071 pane.update(cx, |pane, _| {
5072 assert!(!pane.can_navigate_backward());
5073 assert!(!pane.can_navigate_forward());
5074 });
5075
5076 item.update(cx, |item, cx| {
5077 item.set_state("one".to_string(), cx);
5078 });
5079
5080 // Toolbar must be notified to re-render the navigation buttons
5081 assert_eq!(*toolbar_notify_count.borrow(), 1);
5082
5083 pane.update(cx, |pane, _| {
5084 assert!(pane.can_navigate_backward());
5085 assert!(!pane.can_navigate_forward());
5086 });
5087
5088 workspace
5089 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
5090 .await
5091 .unwrap();
5092
5093 assert_eq!(*toolbar_notify_count.borrow(), 2);
5094 pane.update(cx, |pane, _| {
5095 assert!(!pane.can_navigate_backward());
5096 assert!(pane.can_navigate_forward());
5097 });
5098 }
5099
5100 #[gpui::test]
5101 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
5102 init_test(cx);
5103 let fs = FakeFs::new(cx.executor());
5104
5105 let project = Project::test(fs, [], cx).await;
5106 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5107
5108 let panel = workspace.update(cx, |workspace, cx| {
5109 let panel = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
5110 workspace.add_panel(panel.clone(), cx);
5111
5112 workspace
5113 .right_dock()
5114 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5115
5116 panel
5117 });
5118
5119 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5120 pane.update(cx, |pane, cx| {
5121 let item = cx.new_view(|cx| TestItem::new(cx));
5122 pane.add_item(Box::new(item), true, true, None, cx);
5123 });
5124
5125 // Transfer focus from center to panel
5126 workspace.update(cx, |workspace, cx| {
5127 workspace.toggle_panel_focus::<TestPanel>(cx);
5128 });
5129
5130 workspace.update(cx, |workspace, cx| {
5131 assert!(workspace.right_dock().read(cx).is_open());
5132 assert!(!panel.is_zoomed(cx));
5133 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5134 });
5135
5136 // Transfer focus from panel to center
5137 workspace.update(cx, |workspace, cx| {
5138 workspace.toggle_panel_focus::<TestPanel>(cx);
5139 });
5140
5141 workspace.update(cx, |workspace, cx| {
5142 assert!(workspace.right_dock().read(cx).is_open());
5143 assert!(!panel.is_zoomed(cx));
5144 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5145 });
5146
5147 // Close the dock
5148 workspace.update(cx, |workspace, cx| {
5149 workspace.toggle_dock(DockPosition::Right, cx);
5150 });
5151
5152 workspace.update(cx, |workspace, cx| {
5153 assert!(!workspace.right_dock().read(cx).is_open());
5154 assert!(!panel.is_zoomed(cx));
5155 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5156 });
5157
5158 // Open the dock
5159 workspace.update(cx, |workspace, cx| {
5160 workspace.toggle_dock(DockPosition::Right, cx);
5161 });
5162
5163 workspace.update(cx, |workspace, cx| {
5164 assert!(workspace.right_dock().read(cx).is_open());
5165 assert!(!panel.is_zoomed(cx));
5166 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5167 });
5168
5169 // Focus and zoom panel
5170 panel.update(cx, |panel, cx| {
5171 cx.focus_self();
5172 panel.set_zoomed(true, cx)
5173 });
5174
5175 workspace.update(cx, |workspace, cx| {
5176 assert!(workspace.right_dock().read(cx).is_open());
5177 assert!(panel.is_zoomed(cx));
5178 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5179 });
5180
5181 // Transfer focus to the center closes the dock
5182 workspace.update(cx, |workspace, cx| {
5183 workspace.toggle_panel_focus::<TestPanel>(cx);
5184 });
5185
5186 workspace.update(cx, |workspace, cx| {
5187 assert!(!workspace.right_dock().read(cx).is_open());
5188 assert!(panel.is_zoomed(cx));
5189 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5190 });
5191
5192 // Transferring focus back to the panel keeps it zoomed
5193 workspace.update(cx, |workspace, cx| {
5194 workspace.toggle_panel_focus::<TestPanel>(cx);
5195 });
5196
5197 workspace.update(cx, |workspace, cx| {
5198 assert!(workspace.right_dock().read(cx).is_open());
5199 assert!(panel.is_zoomed(cx));
5200 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5201 });
5202
5203 // Close the dock while it is zoomed
5204 workspace.update(cx, |workspace, cx| {
5205 workspace.toggle_dock(DockPosition::Right, cx)
5206 });
5207
5208 workspace.update(cx, |workspace, cx| {
5209 assert!(!workspace.right_dock().read(cx).is_open());
5210 assert!(panel.is_zoomed(cx));
5211 assert!(workspace.zoomed.is_none());
5212 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5213 });
5214
5215 // Opening the dock, when it's zoomed, retains focus
5216 workspace.update(cx, |workspace, cx| {
5217 workspace.toggle_dock(DockPosition::Right, cx)
5218 });
5219
5220 workspace.update(cx, |workspace, cx| {
5221 assert!(workspace.right_dock().read(cx).is_open());
5222 assert!(panel.is_zoomed(cx));
5223 assert!(workspace.zoomed.is_some());
5224 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5225 });
5226
5227 // Unzoom and close the panel, zoom the active pane.
5228 panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
5229 workspace.update(cx, |workspace, cx| {
5230 workspace.toggle_dock(DockPosition::Right, cx)
5231 });
5232 pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
5233
5234 // Opening a dock unzooms the pane.
5235 workspace.update(cx, |workspace, cx| {
5236 workspace.toggle_dock(DockPosition::Right, cx)
5237 });
5238 workspace.update(cx, |workspace, cx| {
5239 let pane = pane.read(cx);
5240 assert!(!pane.is_zoomed());
5241 assert!(!pane.focus_handle(cx).is_focused(cx));
5242 assert!(workspace.right_dock().read(cx).is_open());
5243 assert!(workspace.zoomed.is_none());
5244 });
5245 }
5246
5247 struct TestModal(FocusHandle);
5248
5249 impl TestModal {
5250 fn new(cx: &mut ViewContext<Self>) -> Self {
5251 Self(cx.focus_handle())
5252 }
5253 }
5254
5255 impl EventEmitter<DismissEvent> for TestModal {}
5256
5257 impl FocusableView for TestModal {
5258 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
5259 self.0.clone()
5260 }
5261 }
5262
5263 impl ModalView for TestModal {}
5264
5265 impl Render for TestModal {
5266 fn render(&mut self, _cx: &mut ViewContext<TestModal>) -> impl IntoElement {
5267 div().track_focus(&self.0)
5268 }
5269 }
5270
5271 #[gpui::test]
5272 async fn test_panels(cx: &mut gpui::TestAppContext) {
5273 init_test(cx);
5274 let fs = FakeFs::new(cx.executor());
5275
5276 let project = Project::test(fs, [], cx).await;
5277 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5278
5279 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
5280 let panel_1 = cx.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
5281 workspace.add_panel(panel_1.clone(), cx);
5282 workspace
5283 .left_dock()
5284 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
5285 let panel_2 = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
5286 workspace.add_panel(panel_2.clone(), cx);
5287 workspace
5288 .right_dock()
5289 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5290
5291 let left_dock = workspace.left_dock();
5292 assert_eq!(
5293 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5294 panel_1.panel_id()
5295 );
5296 assert_eq!(
5297 left_dock.read(cx).active_panel_size(cx).unwrap(),
5298 panel_1.size(cx)
5299 );
5300
5301 left_dock.update(cx, |left_dock, cx| {
5302 left_dock.resize_active_panel(Some(px(1337.)), cx)
5303 });
5304 assert_eq!(
5305 workspace
5306 .right_dock()
5307 .read(cx)
5308 .visible_panel()
5309 .unwrap()
5310 .panel_id(),
5311 panel_2.panel_id(),
5312 );
5313
5314 (panel_1, panel_2)
5315 });
5316
5317 // Move panel_1 to the right
5318 panel_1.update(cx, |panel_1, cx| {
5319 panel_1.set_position(DockPosition::Right, cx)
5320 });
5321
5322 workspace.update(cx, |workspace, cx| {
5323 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
5324 // Since it was the only panel on the left, the left dock should now be closed.
5325 assert!(!workspace.left_dock().read(cx).is_open());
5326 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
5327 let right_dock = workspace.right_dock();
5328 assert_eq!(
5329 right_dock.read(cx).visible_panel().unwrap().panel_id(),
5330 panel_1.panel_id()
5331 );
5332 assert_eq!(
5333 right_dock.read(cx).active_panel_size(cx).unwrap(),
5334 px(1337.)
5335 );
5336
5337 // Now we move panel_2 to the left
5338 panel_2.set_position(DockPosition::Left, cx);
5339 });
5340
5341 workspace.update(cx, |workspace, cx| {
5342 // Since panel_2 was not visible on the right, we don't open the left dock.
5343 assert!(!workspace.left_dock().read(cx).is_open());
5344 // And the right dock is unaffected in it's displaying of panel_1
5345 assert!(workspace.right_dock().read(cx).is_open());
5346 assert_eq!(
5347 workspace
5348 .right_dock()
5349 .read(cx)
5350 .visible_panel()
5351 .unwrap()
5352 .panel_id(),
5353 panel_1.panel_id(),
5354 );
5355 });
5356
5357 // Move panel_1 back to the left
5358 panel_1.update(cx, |panel_1, cx| {
5359 panel_1.set_position(DockPosition::Left, cx)
5360 });
5361
5362 workspace.update(cx, |workspace, cx| {
5363 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
5364 let left_dock = workspace.left_dock();
5365 assert!(left_dock.read(cx).is_open());
5366 assert_eq!(
5367 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5368 panel_1.panel_id()
5369 );
5370 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), px(1337.));
5371 // And the right dock should be closed as it no longer has any panels.
5372 assert!(!workspace.right_dock().read(cx).is_open());
5373
5374 // Now we move panel_1 to the bottom
5375 panel_1.set_position(DockPosition::Bottom, cx);
5376 });
5377
5378 workspace.update(cx, |workspace, cx| {
5379 // Since panel_1 was visible on the left, we close the left dock.
5380 assert!(!workspace.left_dock().read(cx).is_open());
5381 // The bottom dock is sized based on the panel's default size,
5382 // since the panel orientation changed from vertical to horizontal.
5383 let bottom_dock = workspace.bottom_dock();
5384 assert_eq!(
5385 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
5386 panel_1.size(cx),
5387 );
5388 // Close bottom dock and move panel_1 back to the left.
5389 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
5390 panel_1.set_position(DockPosition::Left, cx);
5391 });
5392
5393 // Emit activated event on panel 1
5394 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
5395
5396 // Now the left dock is open and panel_1 is active and focused.
5397 workspace.update(cx, |workspace, cx| {
5398 let left_dock = workspace.left_dock();
5399 assert!(left_dock.read(cx).is_open());
5400 assert_eq!(
5401 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5402 panel_1.panel_id(),
5403 );
5404 assert!(panel_1.focus_handle(cx).is_focused(cx));
5405 });
5406
5407 // Emit closed event on panel 2, which is not active
5408 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
5409
5410 // Wo don't close the left dock, because panel_2 wasn't the active panel
5411 workspace.update(cx, |workspace, cx| {
5412 let left_dock = workspace.left_dock();
5413 assert!(left_dock.read(cx).is_open());
5414 assert_eq!(
5415 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5416 panel_1.panel_id(),
5417 );
5418 });
5419
5420 // Emitting a ZoomIn event shows the panel as zoomed.
5421 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
5422 workspace.update(cx, |workspace, _| {
5423 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5424 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
5425 });
5426
5427 // Move panel to another dock while it is zoomed
5428 panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
5429 workspace.update(cx, |workspace, _| {
5430 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5431
5432 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5433 });
5434
5435 // This is a helper for getting a:
5436 // - valid focus on an element,
5437 // - that isn't a part of the panes and panels system of the Workspace,
5438 // - and doesn't trigger the 'on_focus_lost' API.
5439 let focus_other_view = {
5440 let workspace = workspace.clone();
5441 move |cx: &mut VisualTestContext| {
5442 workspace.update(cx, |workspace, cx| {
5443 if let Some(_) = workspace.active_modal::<TestModal>(cx) {
5444 workspace.toggle_modal(cx, TestModal::new);
5445 workspace.toggle_modal(cx, TestModal::new);
5446 } else {
5447 workspace.toggle_modal(cx, TestModal::new);
5448 }
5449 })
5450 }
5451 };
5452
5453 // If focus is transferred to another view that's not a panel or another pane, we still show
5454 // the panel as zoomed.
5455 focus_other_view(cx);
5456 workspace.update(cx, |workspace, _| {
5457 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5458 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5459 });
5460
5461 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
5462 workspace.update(cx, |_, cx| cx.focus_self());
5463 workspace.update(cx, |workspace, _| {
5464 assert_eq!(workspace.zoomed, None);
5465 assert_eq!(workspace.zoomed_position, None);
5466 });
5467
5468 // If focus is transferred again to another view that's not a panel or a pane, we won't
5469 // show the panel as zoomed because it wasn't zoomed before.
5470 focus_other_view(cx);
5471 workspace.update(cx, |workspace, _| {
5472 assert_eq!(workspace.zoomed, None);
5473 assert_eq!(workspace.zoomed_position, None);
5474 });
5475
5476 // When the panel is activated, it is zoomed again.
5477 cx.dispatch_action(ToggleRightDock);
5478 workspace.update(cx, |workspace, _| {
5479 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5480 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5481 });
5482
5483 // Emitting a ZoomOut event unzooms the panel.
5484 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
5485 workspace.update(cx, |workspace, _| {
5486 assert_eq!(workspace.zoomed, None);
5487 assert_eq!(workspace.zoomed_position, None);
5488 });
5489
5490 // Emit closed event on panel 1, which is active
5491 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
5492
5493 // Now the left dock is closed, because panel_1 was the active panel
5494 workspace.update(cx, |workspace, cx| {
5495 let right_dock = workspace.right_dock();
5496 assert!(!right_dock.read(cx).is_open());
5497 });
5498 }
5499
5500 pub fn init_test(cx: &mut TestAppContext) {
5501 cx.update(|cx| {
5502 let settings_store = SettingsStore::test(cx);
5503 cx.set_global(settings_store);
5504 theme::init(theme::LoadThemes::JustBase, cx);
5505 language::init(cx);
5506 crate::init_settings(cx);
5507 Project::init_settings(cx);
5508 });
5509 }
5510}