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