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