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