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