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