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