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