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