1pub mod dock;
2/// NOTE: Focus only 'takes' after an update has flushed_effects.
3///
4/// This may cause issues when you're trying to write tests that use workspace focus to add items at
5/// specific locations.
6pub mod item;
7pub mod notifications;
8pub mod pane;
9pub mod pane_group;
10mod persistence;
11pub mod searchable;
12pub mod shared_screen;
13mod status_bar;
14mod toolbar;
15mod workspace_settings;
16
17use anyhow::{anyhow, Context, Result};
18use assets::Assets;
19use call::ActiveCall;
20use client::{
21 proto::{self, PeerId},
22 Client, TypedEnvelope, UserStore,
23};
24use collections::{hash_map, HashMap, HashSet};
25use drag_and_drop::DragAndDrop;
26use futures::{
27 channel::{mpsc, oneshot},
28 future::try_join_all,
29 FutureExt, StreamExt,
30};
31use gpui::{
32 actions,
33 elements::*,
34 geometry::{
35 rect::RectF,
36 vector::{vec2f, Vector2F},
37 },
38 impl_actions,
39 platform::{
40 CursorStyle, MouseButton, PathPromptOptions, Platform, PromptLevel, WindowBounds,
41 WindowOptions,
42 },
43 AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
44 SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
45 WindowContext,
46};
47use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
48use itertools::Itertools;
49use language::{LanguageRegistry, Rope};
50use std::{
51 any::TypeId,
52 borrow::Cow,
53 cmp, env,
54 future::Future,
55 path::{Path, PathBuf},
56 str,
57 sync::{atomic::AtomicUsize, Arc},
58 time::Duration,
59};
60
61use crate::{
62 notifications::simple_message_notification::MessageNotification,
63 persistence::model::{
64 DockData, DockStructure, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
65 },
66};
67use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle, TogglePanel};
68use lazy_static::lazy_static;
69use notifications::{NotificationHandle, NotifyResultExt};
70pub use pane::*;
71pub use pane_group::*;
72use persistence::{model::SerializedItem, DB};
73pub use persistence::{
74 model::{ItemId, WorkspaceLocation},
75 WorkspaceDb, DB as WORKSPACE_DB,
76};
77use postage::prelude::Stream;
78use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
79use serde::Deserialize;
80use shared_screen::SharedScreen;
81use status_bar::StatusBar;
82pub use status_bar::StatusItemView;
83use theme::Theme;
84pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
85use util::{async_iife, paths, ResultExt};
86pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings};
87
88lazy_static! {
89 static ref ZED_WINDOW_SIZE: Option<Vector2F> = env::var("ZED_WINDOW_SIZE")
90 .ok()
91 .as_deref()
92 .and_then(parse_pixel_position_env_var);
93 static ref ZED_WINDOW_POSITION: Option<Vector2F> = env::var("ZED_WINDOW_POSITION")
94 .ok()
95 .as_deref()
96 .and_then(parse_pixel_position_env_var);
97}
98
99pub trait Modal: View {
100 fn dismiss_on_event(event: &Self::Event) -> bool;
101}
102
103#[derive(Clone, PartialEq)]
104pub struct RemoveWorktreeFromProject(pub WorktreeId);
105
106actions!(
107 workspace,
108 [
109 Open,
110 NewFile,
111 NewWindow,
112 CloseWindow,
113 AddFolderToProject,
114 Unfollow,
115 Save,
116 SaveAs,
117 SaveAll,
118 ActivatePreviousPane,
119 ActivateNextPane,
120 FollowNextCollaborator,
121 ToggleLeftDock,
122 NewTerminal,
123 ToggleTerminalFocus,
124 NewSearch,
125 Feedback,
126 Restart,
127 Welcome,
128 ToggleZoom,
129 ]
130);
131
132actions!(zed, [OpenSettings]);
133
134#[derive(Clone, PartialEq)]
135pub struct OpenPaths {
136 pub paths: Vec<PathBuf>,
137}
138
139#[derive(Clone, Deserialize, PartialEq)]
140pub struct ActivatePane(pub usize);
141
142pub struct Toast {
143 id: usize,
144 msg: Cow<'static, str>,
145 on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut WindowContext)>)>,
146}
147
148impl Toast {
149 pub fn new<I: Into<Cow<'static, str>>>(id: usize, msg: I) -> Self {
150 Toast {
151 id,
152 msg: msg.into(),
153 on_click: None,
154 }
155 }
156
157 pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
158 where
159 M: Into<Cow<'static, str>>,
160 F: Fn(&mut WindowContext) + 'static,
161 {
162 self.on_click = Some((message.into(), Arc::new(on_click)));
163 self
164 }
165}
166
167impl PartialEq for Toast {
168 fn eq(&self, other: &Self) -> bool {
169 self.id == other.id
170 && self.msg == other.msg
171 && self.on_click.is_some() == other.on_click.is_some()
172 }
173}
174
175impl Clone for Toast {
176 fn clone(&self) -> Self {
177 Toast {
178 id: self.id,
179 msg: self.msg.to_owned(),
180 on_click: self.on_click.clone(),
181 }
182 }
183}
184
185pub type WorkspaceId = i64;
186
187impl_actions!(workspace, [ActivatePane]);
188
189pub fn init_settings(cx: &mut AppContext) {
190 settings::register::<WorkspaceSettings>(cx);
191}
192
193pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
194 init_settings(cx);
195 pane::init(cx);
196 notifications::init(cx);
197
198 cx.add_global_action({
199 let app_state = Arc::downgrade(&app_state);
200 move |_: &Open, cx: &mut AppContext| {
201 let mut paths = cx.prompt_for_paths(PathPromptOptions {
202 files: true,
203 directories: true,
204 multiple: true,
205 });
206
207 if let Some(app_state) = app_state.upgrade() {
208 cx.spawn(move |mut cx| async move {
209 if let Some(paths) = paths.recv().await.flatten() {
210 cx.update(|cx| {
211 open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx)
212 });
213 }
214 })
215 .detach();
216 }
217 }
218 });
219 cx.add_async_action(Workspace::open);
220
221 cx.add_async_action(Workspace::follow_next_collaborator);
222 cx.add_async_action(Workspace::close);
223 cx.add_global_action(Workspace::close_global);
224 cx.add_global_action(restart);
225 cx.add_async_action(Workspace::save_all);
226 cx.add_action(Workspace::add_folder_to_project);
227 cx.add_action(
228 |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
229 let pane = workspace.active_pane().clone();
230 workspace.unfollow(&pane, cx);
231 },
232 );
233 cx.add_action(
234 |workspace: &mut Workspace, _: &Save, cx: &mut ViewContext<Workspace>| {
235 workspace.save_active_item(false, cx).detach_and_log_err(cx);
236 },
237 );
238 cx.add_action(
239 |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext<Workspace>| {
240 workspace.save_active_item(true, cx).detach_and_log_err(cx);
241 },
242 );
243 cx.add_action(Workspace::toggle_panel);
244 cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| {
245 workspace.activate_previous_pane(cx)
246 });
247 cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| {
248 workspace.activate_next_pane(cx)
249 });
250 cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftDock, cx| {
251 workspace.toggle_dock(DockPosition::Left, cx);
252 });
253 cx.add_action(Workspace::activate_pane_at_index);
254
255 cx.add_action(|_: &mut Workspace, _: &install_cli::Install, cx| {
256 cx.spawn(|workspace, mut cx| async move {
257 let err = install_cli::install_cli(&cx)
258 .await
259 .context("Failed to create CLI symlink");
260
261 workspace.update(&mut cx, |workspace, cx| {
262 if matches!(err, Err(_)) {
263 err.notify_err(workspace, cx);
264 } else {
265 workspace.show_notification(1, cx, |cx| {
266 cx.add_view(|_| {
267 MessageNotification::new("Successfully installed the `zed` binary")
268 })
269 });
270 }
271 })
272 })
273 .detach();
274 });
275
276 cx.add_action(
277 move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
278 create_and_open_local_file(&paths::SETTINGS, cx, || {
279 settings::initial_user_settings_content(&Assets)
280 .as_ref()
281 .into()
282 })
283 .detach_and_log_err(cx);
284 },
285 );
286
287 let client = &app_state.client;
288 client.add_view_request_handler(Workspace::handle_follow);
289 client.add_view_message_handler(Workspace::handle_unfollow);
290 client.add_view_message_handler(Workspace::handle_update_followers);
291}
292
293type ProjectItemBuilders = HashMap<
294 TypeId,
295 fn(ModelHandle<Project>, AnyModelHandle, &mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
296>;
297pub fn register_project_item<I: ProjectItem>(cx: &mut AppContext) {
298 cx.update_default_global(|builders: &mut ProjectItemBuilders, _| {
299 builders.insert(TypeId::of::<I::Item>(), |project, model, cx| {
300 let item = model.downcast::<I::Item>().unwrap();
301 Box::new(cx.add_view(|cx| I::for_project_item(project, item, cx)))
302 });
303 });
304}
305
306type FollowableItemBuilder = fn(
307 ViewHandle<Pane>,
308 ModelHandle<Project>,
309 ViewId,
310 &mut Option<proto::view::Variant>,
311 &mut AppContext,
312) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
313type FollowableItemBuilders = HashMap<
314 TypeId,
315 (
316 FollowableItemBuilder,
317 fn(&AnyViewHandle) -> Box<dyn FollowableItemHandle>,
318 ),
319>;
320pub fn register_followable_item<I: FollowableItem>(cx: &mut AppContext) {
321 cx.update_default_global(|builders: &mut FollowableItemBuilders, _| {
322 builders.insert(
323 TypeId::of::<I>(),
324 (
325 |pane, project, id, state, cx| {
326 I::from_state_proto(pane, project, id, state, cx).map(|task| {
327 cx.foreground()
328 .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
329 })
330 },
331 |this| Box::new(this.clone().downcast::<I>().unwrap()),
332 ),
333 );
334 });
335}
336
337type ItemDeserializers = HashMap<
338 Arc<str>,
339 fn(
340 ModelHandle<Project>,
341 WeakViewHandle<Workspace>,
342 WorkspaceId,
343 ItemId,
344 &mut ViewContext<Pane>,
345 ) -> Task<Result<Box<dyn ItemHandle>>>,
346>;
347pub fn register_deserializable_item<I: Item>(cx: &mut AppContext) {
348 cx.update_default_global(|deserializers: &mut ItemDeserializers, _cx| {
349 if let Some(serialized_item_kind) = I::serialized_item_kind() {
350 deserializers.insert(
351 Arc::from(serialized_item_kind),
352 |project, workspace, workspace_id, item_id, cx| {
353 let task = I::deserialize(project, workspace, workspace_id, item_id, cx);
354 cx.foreground()
355 .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
356 },
357 );
358 }
359 });
360}
361
362pub struct AppState {
363 pub languages: Arc<LanguageRegistry>,
364 pub client: Arc<client::Client>,
365 pub user_store: ModelHandle<client::UserStore>,
366 pub fs: Arc<dyn fs::Fs>,
367 pub build_window_options:
368 fn(Option<WindowBounds>, Option<uuid::Uuid>, &dyn Platform) -> WindowOptions<'static>,
369 pub initialize_workspace:
370 fn(WeakViewHandle<Workspace>, bool, Arc<AppState>, AsyncAppContext) -> Task<Result<()>>,
371 pub background_actions: BackgroundActions,
372}
373
374impl AppState {
375 #[cfg(any(test, feature = "test-support"))]
376 pub fn test(cx: &mut AppContext) -> Arc<Self> {
377 use settings::SettingsStore;
378
379 if !cx.has_global::<SettingsStore>() {
380 cx.set_global(SettingsStore::test(cx));
381 }
382
383 let fs = fs::FakeFs::new(cx.background().clone());
384 let languages = Arc::new(LanguageRegistry::test());
385 let http_client = util::http::FakeHttpClient::with_404_response();
386 let client = Client::new(http_client.clone(), cx);
387 let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
388
389 theme::init((), cx);
390 client::init(&client, cx);
391 crate::init_settings(cx);
392
393 Arc::new(Self {
394 client,
395 fs,
396 languages,
397 user_store,
398 initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
399 build_window_options: |_, _, _| Default::default(),
400 background_actions: || &[],
401 })
402 }
403}
404
405struct DelayedDebouncedEditAction {
406 task: Option<Task<()>>,
407 cancel_channel: Option<oneshot::Sender<()>>,
408}
409
410impl DelayedDebouncedEditAction {
411 fn new() -> DelayedDebouncedEditAction {
412 DelayedDebouncedEditAction {
413 task: None,
414 cancel_channel: None,
415 }
416 }
417
418 fn fire_new<F>(&mut self, delay: Duration, cx: &mut ViewContext<Workspace>, f: F)
419 where
420 F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> Task<Result<()>>,
421 {
422 if let Some(channel) = self.cancel_channel.take() {
423 _ = channel.send(());
424 }
425
426 let (sender, mut receiver) = oneshot::channel::<()>();
427 self.cancel_channel = Some(sender);
428
429 let previous_task = self.task.take();
430 self.task = Some(cx.spawn(|workspace, mut cx| async move {
431 let mut timer = cx.background().timer(delay).fuse();
432 if let Some(previous_task) = previous_task {
433 previous_task.await;
434 }
435
436 futures::select_biased! {
437 _ = receiver => return,
438 _ = timer => {}
439 }
440
441 if let Some(result) = workspace
442 .update(&mut cx, |workspace, cx| (f)(workspace, cx))
443 .log_err()
444 {
445 result.await.log_err();
446 }
447 }));
448 }
449}
450
451pub enum Event {
452 PaneAdded(ViewHandle<Pane>),
453 ContactRequestedJoin(u64),
454}
455
456pub struct Workspace {
457 weak_self: WeakViewHandle<Self>,
458 remote_entity_subscription: Option<client::Subscription>,
459 modal: Option<AnyViewHandle>,
460 center: PaneGroup,
461 left_dock: ViewHandle<Dock>,
462 bottom_dock: ViewHandle<Dock>,
463 right_dock: ViewHandle<Dock>,
464 panes: Vec<ViewHandle<Pane>>,
465 panes_by_item: HashMap<usize, WeakViewHandle<Pane>>,
466 active_pane: ViewHandle<Pane>,
467 last_active_center_pane: Option<WeakViewHandle<Pane>>,
468 status_bar: ViewHandle<StatusBar>,
469 titlebar_item: Option<AnyViewHandle>,
470 notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
471 project: ModelHandle<Project>,
472 leader_state: LeaderState,
473 follower_states_by_leader: FollowerStatesByLeader,
474 last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
475 window_edited: bool,
476 active_call: Option<(ModelHandle<ActiveCall>, Vec<gpui::Subscription>)>,
477 leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
478 database_id: WorkspaceId,
479 app_state: Arc<AppState>,
480 _subscriptions: Vec<Subscription>,
481 _apply_leader_updates: Task<Result<()>>,
482 _observe_current_user: Task<Result<()>>,
483 pane_history_timestamp: Arc<AtomicUsize>,
484}
485
486#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
487pub struct ViewId {
488 pub creator: PeerId,
489 pub id: u64,
490}
491
492#[derive(Default)]
493struct LeaderState {
494 followers: HashSet<PeerId>,
495}
496
497type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
498
499#[derive(Default)]
500struct FollowerState {
501 active_view_id: Option<ViewId>,
502 items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
503}
504
505impl Workspace {
506 pub fn new(
507 workspace_id: WorkspaceId,
508 project: ModelHandle<Project>,
509 app_state: Arc<AppState>,
510 cx: &mut ViewContext<Self>,
511 ) -> Self {
512 cx.observe(&project, |_, _, cx| cx.notify()).detach();
513 cx.subscribe(&project, move |this, _, event, cx| {
514 match event {
515 project::Event::RemoteIdChanged(remote_id) => {
516 this.update_window_title(cx);
517 this.project_remote_id_changed(*remote_id, cx);
518 }
519
520 project::Event::CollaboratorLeft(peer_id) => {
521 this.collaborator_left(*peer_id, cx);
522 }
523
524 project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => {
525 this.update_window_title(cx);
526 this.serialize_workspace(cx);
527 }
528
529 project::Event::DisconnectedFromHost => {
530 this.update_window_edited(cx);
531 cx.blur();
532 }
533
534 project::Event::Closed => {
535 cx.remove_window();
536 }
537
538 project::Event::DeletedEntry(entry_id) => {
539 for pane in this.panes.iter() {
540 pane.update(cx, |pane, cx| {
541 pane.handle_deleted_project_item(*entry_id, cx)
542 });
543 }
544 }
545
546 _ => {}
547 }
548 cx.notify()
549 })
550 .detach();
551
552 let weak_handle = cx.weak_handle();
553 let pane_history_timestamp = Arc::new(AtomicUsize::new(0));
554
555 let center_pane = cx.add_view(|cx| {
556 Pane::new(
557 weak_handle.clone(),
558 app_state.background_actions,
559 pane_history_timestamp.clone(),
560 cx,
561 )
562 });
563 cx.subscribe(¢er_pane, Self::handle_pane_event).detach();
564 cx.focus(¢er_pane);
565 cx.emit(Event::PaneAdded(center_pane.clone()));
566
567 let mut current_user = app_state.user_store.read(cx).watch_current_user();
568 let mut connection_status = app_state.client.status();
569 let _observe_current_user = cx.spawn(|this, mut cx| async move {
570 current_user.recv().await;
571 connection_status.recv().await;
572 let mut stream =
573 Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
574
575 while stream.recv().await.is_some() {
576 this.update(&mut cx, |_, cx| cx.notify())?;
577 }
578 anyhow::Ok(())
579 });
580
581 // All leader updates are enqueued and then processed in a single task, so
582 // that each asynchronous operation can be run in order.
583 let (leader_updates_tx, mut leader_updates_rx) =
584 mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
585 let _apply_leader_updates = cx.spawn(|this, mut cx| async move {
586 while let Some((leader_id, update)) = leader_updates_rx.next().await {
587 Self::process_leader_update(&this, leader_id, update, &mut cx)
588 .await
589 .log_err();
590 }
591
592 Ok(())
593 });
594
595 cx.emit_global(WorkspaceCreated(weak_handle.clone()));
596
597 let left_dock = cx.add_view(|_| Dock::new(DockPosition::Left));
598 let bottom_dock = cx.add_view(|_| Dock::new(DockPosition::Bottom));
599 let right_dock = cx.add_view(|_| Dock::new(DockPosition::Right));
600 let left_dock_buttons =
601 cx.add_view(|cx| PanelButtons::new(left_dock.clone(), weak_handle.clone(), cx));
602 let bottom_dock_buttons =
603 cx.add_view(|cx| PanelButtons::new(bottom_dock.clone(), weak_handle.clone(), cx));
604 let right_dock_buttons =
605 cx.add_view(|cx| PanelButtons::new(right_dock.clone(), weak_handle.clone(), cx));
606 let status_bar = cx.add_view(|cx| {
607 let mut status_bar = StatusBar::new(¢er_pane.clone(), cx);
608 status_bar.add_left_item(left_dock_buttons, cx);
609 status_bar.add_right_item(right_dock_buttons, cx);
610 status_bar.add_right_item(bottom_dock_buttons, cx);
611 status_bar
612 });
613
614 cx.update_default_global::<DragAndDrop<Workspace>, _, _>(|drag_and_drop, _| {
615 drag_and_drop.register_container(weak_handle.clone());
616 });
617
618 let mut active_call = None;
619 if cx.has_global::<ModelHandle<ActiveCall>>() {
620 let call = cx.global::<ModelHandle<ActiveCall>>().clone();
621 let mut subscriptions = Vec::new();
622 subscriptions.push(cx.subscribe(&call, Self::on_active_call_event));
623 active_call = Some((call, subscriptions));
624 }
625
626 let subscriptions = vec![
627 cx.observe_fullscreen(|_, _, cx| cx.notify()),
628 cx.observe_window_activation(Self::on_window_activation_changed),
629 cx.observe_window_bounds(move |_, mut bounds, display, cx| {
630 // Transform fixed bounds to be stored in terms of the containing display
631 if let WindowBounds::Fixed(mut window_bounds) = bounds {
632 if let Some(screen) = cx.platform().screen_by_id(display) {
633 let screen_bounds = screen.bounds();
634 window_bounds
635 .set_origin_x(window_bounds.origin_x() - screen_bounds.origin_x());
636 window_bounds
637 .set_origin_y(window_bounds.origin_y() - screen_bounds.origin_y());
638 bounds = WindowBounds::Fixed(window_bounds);
639 }
640 }
641
642 cx.background()
643 .spawn(DB.set_window_bounds(workspace_id, bounds, display))
644 .detach_and_log_err(cx);
645 }),
646 cx.observe(&left_dock, |this, _, cx| {
647 this.serialize_workspace(cx);
648 cx.notify();
649 }),
650 cx.observe(&bottom_dock, |this, _, cx| {
651 this.serialize_workspace(cx);
652 cx.notify();
653 }),
654 cx.observe(&right_dock, |this, _, cx| {
655 this.serialize_workspace(cx);
656 cx.notify();
657 }),
658 ];
659
660 let mut this = Workspace {
661 weak_self: weak_handle.clone(),
662 modal: None,
663 center: PaneGroup::new(center_pane.clone()),
664 panes: vec![center_pane.clone()],
665 panes_by_item: Default::default(),
666 active_pane: center_pane.clone(),
667 last_active_center_pane: Some(center_pane.downgrade()),
668 status_bar,
669 titlebar_item: None,
670 notifications: Default::default(),
671 remote_entity_subscription: None,
672 left_dock,
673 bottom_dock,
674 right_dock,
675 project: project.clone(),
676 leader_state: Default::default(),
677 follower_states_by_leader: Default::default(),
678 last_leaders_by_pane: Default::default(),
679 window_edited: false,
680 active_call,
681 database_id: workspace_id,
682 app_state,
683 _observe_current_user,
684 _apply_leader_updates,
685 leader_updates_tx,
686 _subscriptions: subscriptions,
687 pane_history_timestamp,
688 };
689 this.project_remote_id_changed(project.read(cx).remote_id(), cx);
690 cx.defer(|this, cx| this.update_window_title(cx));
691 this
692 }
693
694 fn new_local(
695 abs_paths: Vec<PathBuf>,
696 app_state: Arc<AppState>,
697 requesting_window_id: Option<usize>,
698 cx: &mut AppContext,
699 ) -> Task<(
700 WeakViewHandle<Workspace>,
701 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
702 )> {
703 let project_handle = Project::local(
704 app_state.client.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 = persistence::DB.workspace_for_roots(&abs_paths.as_slice());
713
714 let paths_to_open = Arc::new(abs_paths);
715
716 // Get project paths for all of the abs_paths
717 let mut worktree_roots: HashSet<Arc<Path>> = Default::default();
718 let mut project_paths: Vec<(PathBuf, Option<ProjectPath>)> =
719 Vec::with_capacity(paths_to_open.len());
720 for path in paths_to_open.iter().cloned() {
721 if let Some((worktree, project_entry)) = cx
722 .update(|cx| {
723 Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
724 })
725 .await
726 .log_err()
727 {
728 worktree_roots.insert(worktree.read_with(&mut cx, |tree, _| tree.abs_path()));
729 project_paths.push((path, Some(project_entry)));
730 } else {
731 project_paths.push((path, None));
732 }
733 }
734
735 let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
736 serialized_workspace.id
737 } else {
738 DB.next_id().await.unwrap_or(0)
739 };
740
741 let window_bounds_override =
742 ZED_WINDOW_POSITION
743 .zip(*ZED_WINDOW_SIZE)
744 .map(|(position, size)| {
745 WindowBounds::Fixed(RectF::new(
746 cx.platform().screens()[0].bounds().origin() + position,
747 size,
748 ))
749 });
750
751 let build_workspace = |cx: &mut ViewContext<Workspace>| {
752 Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx)
753 };
754
755 let workspace = requesting_window_id
756 .and_then(|window_id| {
757 cx.update(|cx| cx.replace_root_view(window_id, |cx| build_workspace(cx)))
758 })
759 .unwrap_or_else(|| {
760 let (bounds, display) = if let Some(bounds) = window_bounds_override {
761 (Some(bounds), None)
762 } else {
763 serialized_workspace
764 .as_ref()
765 .and_then(|serialized_workspace| {
766 let display = serialized_workspace.display?;
767 let mut bounds = serialized_workspace.bounds?;
768
769 // Stored bounds are relative to the containing display.
770 // So convert back to global coordinates if that screen still exists
771 if let WindowBounds::Fixed(mut window_bounds) = bounds {
772 if let Some(screen) = cx.platform().screen_by_id(display) {
773 let screen_bounds = screen.bounds();
774 window_bounds.set_origin_x(
775 window_bounds.origin_x() + screen_bounds.origin_x(),
776 );
777 window_bounds.set_origin_y(
778 window_bounds.origin_y() + screen_bounds.origin_y(),
779 );
780 bounds = WindowBounds::Fixed(window_bounds);
781 } else {
782 // Screen no longer exists. Return none here.
783 return None;
784 }
785 }
786
787 Some((bounds, display))
788 })
789 .unzip()
790 };
791
792 // Use the serialized workspace to construct the new window
793 cx.add_window(
794 (app_state.build_window_options)(bounds, display, cx.platform().as_ref()),
795 |cx| build_workspace(cx),
796 )
797 .1
798 });
799
800 (app_state.initialize_workspace)(
801 workspace.downgrade(),
802 serialized_workspace.is_some(),
803 app_state.clone(),
804 cx.clone(),
805 )
806 .await
807 .log_err();
808
809 cx.update_window(workspace.window_id(), |cx| cx.activate_window());
810
811 let workspace = workspace.downgrade();
812 notify_if_database_failed(&workspace, &mut cx);
813 let opened_items = open_items(
814 serialized_workspace,
815 &workspace,
816 project_paths,
817 app_state,
818 cx,
819 )
820 .await;
821
822 (workspace, opened_items)
823 })
824 }
825
826 pub fn weak_handle(&self) -> WeakViewHandle<Self> {
827 self.weak_self.clone()
828 }
829
830 pub fn left_dock(&self) -> &ViewHandle<Dock> {
831 &self.left_dock
832 }
833
834 pub fn bottom_dock(&self) -> &ViewHandle<Dock> {
835 &self.bottom_dock
836 }
837
838 pub fn right_dock(&self) -> &ViewHandle<Dock> {
839 &self.right_dock
840 }
841
842 pub fn add_panel<T: Panel>(&mut self, panel: ViewHandle<T>, cx: &mut ViewContext<Self>) {
843 let dock = match panel.position(cx) {
844 DockPosition::Left => &self.left_dock,
845 DockPosition::Bottom => &self.bottom_dock,
846 DockPosition::Right => &self.right_dock,
847 };
848
849 cx.subscribe(&panel, {
850 let mut dock = dock.clone();
851 let mut prev_position = panel.position(cx);
852 move |this, panel, event, cx| {
853 if T::should_change_position_on_event(event) {
854 let new_position = panel.read(cx).position(cx);
855 let mut was_visible = false;
856 dock.update(cx, |dock, cx| {
857 prev_position = new_position;
858
859 was_visible = dock.is_open()
860 && dock
861 .active_panel()
862 .map_or(false, |active_panel| active_panel.id() == panel.id());
863 dock.remove_panel(&panel, cx);
864 });
865 dock = match panel.read(cx).position(cx) {
866 DockPosition::Left => &this.left_dock,
867 DockPosition::Bottom => &this.bottom_dock,
868 DockPosition::Right => &this.right_dock,
869 }
870 .clone();
871 dock.update(cx, |dock, cx| {
872 dock.add_panel(panel.clone(), cx);
873 if was_visible {
874 dock.set_open(true, cx);
875 dock.activate_panel(dock.panels_len() - 1, cx);
876 }
877 });
878 } else if T::should_zoom_in_on_event(event) {
879 this.zoom_out(cx);
880 dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, true, cx));
881 } else if T::should_zoom_out_on_event(event) {
882 this.zoom_out(cx);
883 } else if T::is_focus_event(event) {
884 cx.notify();
885 }
886 }
887 })
888 .detach();
889
890 dock.update(cx, |dock, cx| dock.add_panel(panel, cx));
891 }
892
893 pub fn status_bar(&self) -> &ViewHandle<StatusBar> {
894 &self.status_bar
895 }
896
897 pub fn app_state(&self) -> &Arc<AppState> {
898 &self.app_state
899 }
900
901 pub fn user_store(&self) -> &ModelHandle<UserStore> {
902 &self.app_state.user_store
903 }
904
905 pub fn project(&self) -> &ModelHandle<Project> {
906 &self.project
907 }
908
909 pub fn recent_navigation_history(
910 &self,
911 limit: Option<usize>,
912 cx: &AppContext,
913 ) -> Vec<ProjectPath> {
914 let mut history: HashMap<ProjectPath, usize> = HashMap::default();
915 for pane in &self.panes {
916 let pane = pane.read(cx);
917 pane.nav_history()
918 .for_each_entry(cx, |entry, project_path| {
919 let timestamp = entry.timestamp;
920 match history.entry(project_path) {
921 hash_map::Entry::Occupied(mut entry) => {
922 if ×tamp > entry.get() {
923 entry.insert(timestamp);
924 }
925 }
926 hash_map::Entry::Vacant(entry) => {
927 entry.insert(timestamp);
928 }
929 }
930 });
931 }
932
933 history
934 .into_iter()
935 .sorted_by_key(|(_, timestamp)| *timestamp)
936 .map(|(project_path, _)| project_path)
937 .rev()
938 .take(limit.unwrap_or(usize::MAX))
939 .collect()
940 }
941
942 pub fn client(&self) -> &Client {
943 &self.app_state.client
944 }
945
946 pub fn set_titlebar_item(&mut self, item: AnyViewHandle, cx: &mut ViewContext<Self>) {
947 self.titlebar_item = Some(item);
948 cx.notify();
949 }
950
951 pub fn titlebar_item(&self) -> Option<AnyViewHandle> {
952 self.titlebar_item.clone()
953 }
954
955 /// Call the given callback with a workspace whose project is local.
956 ///
957 /// If the given workspace has a local project, then it will be passed
958 /// to the callback. Otherwise, a new empty window will be created.
959 pub fn with_local_workspace<T, F>(
960 &mut self,
961 cx: &mut ViewContext<Self>,
962 callback: F,
963 ) -> Task<Result<T>>
964 where
965 T: 'static,
966 F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
967 {
968 if self.project.read(cx).is_local() {
969 Task::Ready(Some(Ok(callback(self, cx))))
970 } else {
971 let task = Self::new_local(Vec::new(), self.app_state.clone(), None, cx);
972 cx.spawn(|_vh, mut cx| async move {
973 let (workspace, _) = task.await;
974 workspace.update(&mut cx, callback)
975 })
976 }
977 }
978
979 pub fn worktrees<'a>(
980 &self,
981 cx: &'a AppContext,
982 ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
983 self.project.read(cx).worktrees(cx)
984 }
985
986 pub fn visible_worktrees<'a>(
987 &self,
988 cx: &'a AppContext,
989 ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
990 self.project.read(cx).visible_worktrees(cx)
991 }
992
993 pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future<Output = ()> + 'static {
994 let futures = self
995 .worktrees(cx)
996 .filter_map(|worktree| worktree.read(cx).as_local())
997 .map(|worktree| worktree.scan_complete())
998 .collect::<Vec<_>>();
999 async move {
1000 for future in futures {
1001 future.await;
1002 }
1003 }
1004 }
1005
1006 pub fn close_global(_: &CloseWindow, cx: &mut AppContext) {
1007 cx.spawn(|mut cx| async move {
1008 let id = cx
1009 .window_ids()
1010 .into_iter()
1011 .find(|&id| cx.window_is_active(id));
1012 if let Some(id) = id {
1013 //This can only get called when the window's project connection has been lost
1014 //so we don't need to prompt the user for anything and instead just close the window
1015 cx.remove_window(id);
1016 }
1017 })
1018 .detach();
1019 }
1020
1021 pub fn close(
1022 &mut self,
1023 _: &CloseWindow,
1024 cx: &mut ViewContext<Self>,
1025 ) -> Option<Task<Result<()>>> {
1026 let window_id = cx.window_id();
1027 let prepare = self.prepare_to_close(false, cx);
1028 Some(cx.spawn(|_, mut cx| async move {
1029 if prepare.await? {
1030 cx.remove_window(window_id);
1031 }
1032 Ok(())
1033 }))
1034 }
1035
1036 pub fn prepare_to_close(
1037 &mut self,
1038 quitting: bool,
1039 cx: &mut ViewContext<Self>,
1040 ) -> Task<Result<bool>> {
1041 let active_call = self.active_call().cloned();
1042 let window_id = cx.window_id();
1043
1044 cx.spawn(|this, mut cx| async move {
1045 let workspace_count = cx
1046 .window_ids()
1047 .into_iter()
1048 .filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
1049 .count();
1050
1051 if let Some(active_call) = active_call {
1052 if !quitting
1053 && workspace_count == 1
1054 && active_call.read_with(&cx, |call, _| call.room().is_some())
1055 {
1056 let answer = cx.prompt(
1057 window_id,
1058 PromptLevel::Warning,
1059 "Do you want to leave the current call?",
1060 &["Close window and hang up", "Cancel"],
1061 );
1062
1063 if let Some(mut answer) = answer {
1064 if answer.next().await == Some(1) {
1065 return anyhow::Ok(false);
1066 } else {
1067 active_call
1068 .update(&mut cx, |call, cx| call.hang_up(cx))
1069 .await
1070 .log_err();
1071 }
1072 }
1073 }
1074 }
1075
1076 Ok(this
1077 .update(&mut cx, |this, cx| this.save_all_internal(true, cx))?
1078 .await?)
1079 })
1080 }
1081
1082 fn save_all(&mut self, _: &SaveAll, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
1083 let save_all = self.save_all_internal(false, cx);
1084 Some(cx.foreground().spawn(async move {
1085 save_all.await?;
1086 Ok(())
1087 }))
1088 }
1089
1090 fn save_all_internal(
1091 &mut self,
1092 should_prompt_to_save: bool,
1093 cx: &mut ViewContext<Self>,
1094 ) -> Task<Result<bool>> {
1095 if self.project.read(cx).is_read_only() {
1096 return Task::ready(Ok(true));
1097 }
1098
1099 let dirty_items = self
1100 .panes
1101 .iter()
1102 .flat_map(|pane| {
1103 pane.read(cx).items().filter_map(|item| {
1104 if item.is_dirty(cx) {
1105 Some((pane.downgrade(), item.boxed_clone()))
1106 } else {
1107 None
1108 }
1109 })
1110 })
1111 .collect::<Vec<_>>();
1112
1113 let project = self.project.clone();
1114 cx.spawn(|_, mut cx| async move {
1115 for (pane, item) in dirty_items {
1116 let (singleton, project_entry_ids) =
1117 cx.read(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)));
1118 if singleton || !project_entry_ids.is_empty() {
1119 if let Some(ix) =
1120 pane.read_with(&cx, |pane, _| pane.index_for_item(item.as_ref()))?
1121 {
1122 if !Pane::save_item(
1123 project.clone(),
1124 &pane,
1125 ix,
1126 &*item,
1127 should_prompt_to_save,
1128 &mut cx,
1129 )
1130 .await?
1131 {
1132 return Ok(false);
1133 }
1134 }
1135 }
1136 }
1137 Ok(true)
1138 })
1139 }
1140
1141 pub fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
1142 let mut paths = cx.prompt_for_paths(PathPromptOptions {
1143 files: true,
1144 directories: true,
1145 multiple: true,
1146 });
1147
1148 Some(cx.spawn(|this, mut cx| async move {
1149 if let Some(paths) = paths.recv().await.flatten() {
1150 if let Some(task) = this
1151 .update(&mut cx, |this, cx| this.open_workspace_for_paths(paths, cx))
1152 .log_err()
1153 {
1154 task.await?
1155 }
1156 }
1157 Ok(())
1158 }))
1159 }
1160
1161 pub fn open_workspace_for_paths(
1162 &mut self,
1163 paths: Vec<PathBuf>,
1164 cx: &mut ViewContext<Self>,
1165 ) -> Task<Result<()>> {
1166 let window_id = cx.window_id();
1167 let is_remote = self.project.read(cx).is_remote();
1168 let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
1169 let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
1170 let close_task = if is_remote || has_worktree || has_dirty_items {
1171 None
1172 } else {
1173 Some(self.prepare_to_close(false, cx))
1174 };
1175 let app_state = self.app_state.clone();
1176
1177 cx.spawn(|_, mut cx| async move {
1178 let window_id_to_replace = if let Some(close_task) = close_task {
1179 if !close_task.await? {
1180 return Ok(());
1181 }
1182 Some(window_id)
1183 } else {
1184 None
1185 };
1186 cx.update(|cx| open_paths(&paths, &app_state, window_id_to_replace, cx))
1187 .await?;
1188 Ok(())
1189 })
1190 }
1191
1192 #[allow(clippy::type_complexity)]
1193 pub fn open_paths(
1194 &mut self,
1195 mut abs_paths: Vec<PathBuf>,
1196 visible: bool,
1197 cx: &mut ViewContext<Self>,
1198 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
1199 log::info!("open paths {:?}", abs_paths);
1200
1201 let fs = self.app_state.fs.clone();
1202
1203 // Sort the paths to ensure we add worktrees for parents before their children.
1204 abs_paths.sort_unstable();
1205 cx.spawn(|this, mut cx| async move {
1206 let mut project_paths = Vec::new();
1207 for path in &abs_paths {
1208 if let Some(project_path) = this
1209 .update(&mut cx, |this, cx| {
1210 Workspace::project_path_for_path(this.project.clone(), path, visible, cx)
1211 })
1212 .log_err()
1213 {
1214 project_paths.push(project_path.await.log_err());
1215 } else {
1216 project_paths.push(None);
1217 }
1218 }
1219
1220 let tasks = abs_paths
1221 .iter()
1222 .cloned()
1223 .zip(project_paths.into_iter())
1224 .map(|(abs_path, project_path)| {
1225 let this = this.clone();
1226 cx.spawn(|mut cx| {
1227 let fs = fs.clone();
1228 async move {
1229 let (_worktree, project_path) = project_path?;
1230 if fs.is_file(&abs_path).await {
1231 Some(
1232 this.update(&mut cx, |this, cx| {
1233 this.open_path(project_path, None, true, cx)
1234 })
1235 .log_err()?
1236 .await,
1237 )
1238 } else {
1239 None
1240 }
1241 }
1242 })
1243 })
1244 .collect::<Vec<_>>();
1245
1246 futures::future::join_all(tasks).await
1247 })
1248 }
1249
1250 fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
1251 let mut paths = cx.prompt_for_paths(PathPromptOptions {
1252 files: false,
1253 directories: true,
1254 multiple: true,
1255 });
1256 cx.spawn(|this, mut cx| async move {
1257 if let Some(paths) = paths.recv().await.flatten() {
1258 let results = this
1259 .update(&mut cx, |this, cx| this.open_paths(paths, true, cx))?
1260 .await;
1261 for result in results.into_iter().flatten() {
1262 result.log_err();
1263 }
1264 }
1265 anyhow::Ok(())
1266 })
1267 .detach_and_log_err(cx);
1268 }
1269
1270 fn project_path_for_path(
1271 project: ModelHandle<Project>,
1272 abs_path: &Path,
1273 visible: bool,
1274 cx: &mut AppContext,
1275 ) -> Task<Result<(ModelHandle<Worktree>, ProjectPath)>> {
1276 let entry = project.update(cx, |project, cx| {
1277 project.find_or_create_local_worktree(abs_path, visible, cx)
1278 });
1279 cx.spawn(|cx| async move {
1280 let (worktree, path) = entry.await?;
1281 let worktree_id = worktree.read_with(&cx, |t, _| t.id());
1282 Ok((
1283 worktree,
1284 ProjectPath {
1285 worktree_id,
1286 path: path.into(),
1287 },
1288 ))
1289 })
1290 }
1291
1292 /// Returns the modal that was toggled closed if it was open.
1293 pub fn toggle_modal<V, F>(
1294 &mut self,
1295 cx: &mut ViewContext<Self>,
1296 add_view: F,
1297 ) -> Option<ViewHandle<V>>
1298 where
1299 V: 'static + Modal,
1300 F: FnOnce(&mut Self, &mut ViewContext<Self>) -> ViewHandle<V>,
1301 {
1302 cx.notify();
1303 // Whatever modal was visible is getting clobbered. If its the same type as V, then return
1304 // it. Otherwise, create a new modal and set it as active.
1305 let already_open_modal = self.modal.take().and_then(|modal| modal.downcast::<V>());
1306 if let Some(already_open_modal) = already_open_modal {
1307 cx.focus_self();
1308 Some(already_open_modal)
1309 } else {
1310 let modal = add_view(self, cx);
1311 cx.subscribe(&modal, |this, _, event, cx| {
1312 if V::dismiss_on_event(event) {
1313 this.dismiss_modal(cx);
1314 }
1315 })
1316 .detach();
1317 cx.focus(&modal);
1318 self.modal = Some(modal.into_any());
1319 None
1320 }
1321 }
1322
1323 pub fn modal<V: 'static + View>(&self) -> Option<ViewHandle<V>> {
1324 self.modal
1325 .as_ref()
1326 .and_then(|modal| modal.clone().downcast::<V>())
1327 }
1328
1329 pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) {
1330 if self.modal.take().is_some() {
1331 cx.focus(&self.active_pane);
1332 cx.notify();
1333 }
1334 }
1335
1336 fn zoomed(&self, cx: &WindowContext) -> Option<AnyViewHandle> {
1337 self.zoomed_panel_for_dock(DockPosition::Left, cx)
1338 .or_else(|| self.zoomed_panel_for_dock(DockPosition::Bottom, cx))
1339 .or_else(|| self.zoomed_panel_for_dock(DockPosition::Right, cx))
1340 .or_else(|| self.zoomed_pane(cx))
1341 }
1342
1343 fn zoomed_panel_for_dock(
1344 &self,
1345 position: DockPosition,
1346 cx: &WindowContext,
1347 ) -> Option<AnyViewHandle> {
1348 let (dock, other_docks) = match position {
1349 DockPosition::Left => (&self.left_dock, [&self.bottom_dock, &self.right_dock]),
1350 DockPosition::Bottom => (&self.bottom_dock, [&self.left_dock, &self.right_dock]),
1351 DockPosition::Right => (&self.right_dock, [&self.left_dock, &self.bottom_dock]),
1352 };
1353
1354 let zoomed_panel = dock.read(&cx).zoomed_panel(cx)?;
1355 if other_docks.iter().all(|dock| !dock.read(cx).has_focus(cx))
1356 && !self.active_pane.read(cx).has_focus()
1357 {
1358 Some(zoomed_panel.as_any().clone())
1359 } else {
1360 None
1361 }
1362 }
1363
1364 fn zoomed_pane(&self, cx: &WindowContext) -> Option<AnyViewHandle> {
1365 let active_pane = self.active_pane.read(cx);
1366 let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock];
1367 if active_pane.is_zoomed() && docks.iter().all(|dock| !dock.read(cx).has_focus(cx)) {
1368 Some(self.active_pane.clone().into_any())
1369 } else {
1370 None
1371 }
1372 }
1373
1374 pub fn items<'a>(
1375 &'a self,
1376 cx: &'a AppContext,
1377 ) -> impl 'a + Iterator<Item = &Box<dyn ItemHandle>> {
1378 self.panes.iter().flat_map(|pane| pane.read(cx).items())
1379 }
1380
1381 pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
1382 self.items_of_type(cx).max_by_key(|item| item.id())
1383 }
1384
1385 pub fn items_of_type<'a, T: Item>(
1386 &'a self,
1387 cx: &'a AppContext,
1388 ) -> impl 'a + Iterator<Item = ViewHandle<T>> {
1389 self.panes
1390 .iter()
1391 .flat_map(|pane| pane.read(cx).items_of_type())
1392 }
1393
1394 pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
1395 self.active_pane().read(cx).active_item()
1396 }
1397
1398 fn active_project_path(&self, cx: &ViewContext<Self>) -> Option<ProjectPath> {
1399 self.active_item(cx).and_then(|item| item.project_path(cx))
1400 }
1401
1402 pub fn save_active_item(
1403 &mut self,
1404 force_name_change: bool,
1405 cx: &mut ViewContext<Self>,
1406 ) -> Task<Result<()>> {
1407 let project = self.project.clone();
1408 if let Some(item) = self.active_item(cx) {
1409 if !force_name_change && item.can_save(cx) {
1410 if item.has_conflict(cx) {
1411 const CONFLICT_MESSAGE: &str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1412
1413 let mut answer = cx.prompt(
1414 PromptLevel::Warning,
1415 CONFLICT_MESSAGE,
1416 &["Overwrite", "Cancel"],
1417 );
1418 cx.spawn(|this, mut cx| async move {
1419 let answer = answer.recv().await;
1420 if answer == Some(0) {
1421 this.update(&mut cx, |this, cx| item.save(this.project.clone(), cx))?
1422 .await?;
1423 }
1424 Ok(())
1425 })
1426 } else {
1427 item.save(self.project.clone(), cx)
1428 }
1429 } else if item.is_singleton(cx) {
1430 let worktree = self.worktrees(cx).next();
1431 let start_abs_path = worktree
1432 .and_then(|w| w.read(cx).as_local())
1433 .map_or(Path::new(""), |w| w.abs_path())
1434 .to_path_buf();
1435 let mut abs_path = cx.prompt_for_new_path(&start_abs_path);
1436 cx.spawn(|this, mut cx| async move {
1437 if let Some(abs_path) = abs_path.recv().await.flatten() {
1438 this.update(&mut cx, |_, cx| item.save_as(project, abs_path, cx))?
1439 .await?;
1440 }
1441 Ok(())
1442 })
1443 } else {
1444 Task::ready(Ok(()))
1445 }
1446 } else {
1447 Task::ready(Ok(()))
1448 }
1449 }
1450
1451 pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext<Self>) {
1452 let dock = match dock_side {
1453 DockPosition::Left => &mut self.left_dock,
1454 DockPosition::Bottom => &mut self.bottom_dock,
1455 DockPosition::Right => &mut self.right_dock,
1456 };
1457 dock.update(cx, |dock, cx| {
1458 let open = !dock.is_open();
1459 dock.set_open(open, cx);
1460 });
1461
1462 self.serialize_workspace(cx);
1463
1464 cx.focus_self();
1465 cx.notify();
1466 }
1467
1468 pub fn toggle_panel(&mut self, action: &TogglePanel, cx: &mut ViewContext<Self>) {
1469 let dock = match action.dock_position {
1470 DockPosition::Left => &mut self.left_dock,
1471 DockPosition::Bottom => &mut self.bottom_dock,
1472 DockPosition::Right => &mut self.right_dock,
1473 };
1474 let active_item = dock.update(cx, move |dock, cx| {
1475 if dock.is_open() && dock.active_panel_index() == action.panel_index {
1476 dock.set_open(false, cx);
1477 None
1478 } else {
1479 dock.set_open(true, cx);
1480 dock.activate_panel(action.panel_index, cx);
1481 dock.active_panel().cloned()
1482 }
1483 });
1484
1485 if let Some(active_item) = active_item {
1486 if active_item.has_focus(cx) {
1487 cx.focus_self();
1488 } else {
1489 cx.focus(active_item.as_any());
1490 }
1491 } else {
1492 cx.focus_self();
1493 }
1494
1495 self.serialize_workspace(cx);
1496
1497 cx.notify();
1498 }
1499
1500 pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
1501 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
1502 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
1503 let active_item = dock.update(cx, |dock, cx| {
1504 dock.set_open(true, cx);
1505 dock.activate_panel(panel_index, cx);
1506 dock.active_panel().cloned()
1507 });
1508 if let Some(active_item) = active_item {
1509 if active_item.has_focus(cx) {
1510 cx.focus_self();
1511 } else {
1512 cx.focus(active_item.as_any());
1513 }
1514 }
1515
1516 self.serialize_workspace(cx);
1517 cx.notify();
1518 break;
1519 }
1520 }
1521 }
1522
1523 fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
1524 for pane in &self.panes {
1525 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
1526 }
1527
1528 self.left_dock.update(cx, |dock, cx| dock.zoom_out(cx));
1529 self.bottom_dock.update(cx, |dock, cx| dock.zoom_out(cx));
1530 self.right_dock.update(cx, |dock, cx| dock.zoom_out(cx));
1531
1532 cx.notify();
1533 }
1534
1535 fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
1536 let pane = cx.add_view(|cx| {
1537 Pane::new(
1538 self.weak_handle(),
1539 self.app_state.background_actions,
1540 self.pane_history_timestamp.clone(),
1541 cx,
1542 )
1543 });
1544 cx.subscribe(&pane, Self::handle_pane_event).detach();
1545 self.panes.push(pane.clone());
1546 cx.focus(&pane);
1547 cx.emit(Event::PaneAdded(pane.clone()));
1548 pane
1549 }
1550
1551 pub fn add_item_to_center(
1552 &mut self,
1553 item: Box<dyn ItemHandle>,
1554 cx: &mut ViewContext<Self>,
1555 ) -> bool {
1556 if let Some(center_pane) = self.last_active_center_pane.clone() {
1557 if let Some(center_pane) = center_pane.upgrade(cx) {
1558 Pane::add_item(self, ¢er_pane, item, true, true, None, cx);
1559 true
1560 } else {
1561 false
1562 }
1563 } else {
1564 false
1565 }
1566 }
1567
1568 pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
1569 let active_pane = self.active_pane().clone();
1570 Pane::add_item(self, &active_pane, item, true, true, None, cx);
1571 }
1572
1573 pub fn open_path(
1574 &mut self,
1575 path: impl Into<ProjectPath>,
1576 pane: Option<WeakViewHandle<Pane>>,
1577 focus_item: bool,
1578 cx: &mut ViewContext<Self>,
1579 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
1580 let pane = pane.unwrap_or_else(|| {
1581 self.last_active_center_pane.clone().unwrap_or_else(|| {
1582 self.panes
1583 .first()
1584 .expect("There must be an active pane")
1585 .downgrade()
1586 })
1587 });
1588
1589 let task = self.load_path(path.into(), cx);
1590 cx.spawn(|this, mut cx| async move {
1591 let (project_entry_id, build_item) = task.await?;
1592 let pane = pane
1593 .upgrade(&cx)
1594 .ok_or_else(|| anyhow!("pane was closed"))?;
1595 this.update(&mut cx, |this, cx| {
1596 Pane::open_item(this, pane, project_entry_id, focus_item, cx, build_item)
1597 })
1598 })
1599 }
1600
1601 pub(crate) fn load_path(
1602 &mut self,
1603 path: ProjectPath,
1604 cx: &mut ViewContext<Self>,
1605 ) -> Task<
1606 Result<(
1607 ProjectEntryId,
1608 impl 'static + FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
1609 )>,
1610 > {
1611 let project = self.project().clone();
1612 let project_item = project.update(cx, |project, cx| project.open_path(path, cx));
1613 cx.spawn(|_, mut cx| async move {
1614 let (project_entry_id, project_item) = project_item.await?;
1615 let build_item = cx.update(|cx| {
1616 cx.default_global::<ProjectItemBuilders>()
1617 .get(&project_item.model_type())
1618 .ok_or_else(|| anyhow!("no item builder for project item"))
1619 .cloned()
1620 })?;
1621 let build_item =
1622 move |cx: &mut ViewContext<Pane>| build_item(project, project_item, cx);
1623 Ok((project_entry_id, build_item))
1624 })
1625 }
1626
1627 pub fn open_project_item<T>(
1628 &mut self,
1629 project_item: ModelHandle<T::Item>,
1630 cx: &mut ViewContext<Self>,
1631 ) -> ViewHandle<T>
1632 where
1633 T: ProjectItem,
1634 {
1635 use project::Item as _;
1636
1637 let entry_id = project_item.read(cx).entry_id(cx);
1638 if let Some(item) = entry_id
1639 .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
1640 .and_then(|item| item.downcast())
1641 {
1642 self.activate_item(&item, cx);
1643 return item;
1644 }
1645
1646 let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
1647 self.add_item(Box::new(item.clone()), cx);
1648 item
1649 }
1650
1651 pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
1652 if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
1653 let pane = self.active_pane.clone();
1654 Pane::add_item(self, &pane, Box::new(shared_screen), false, true, None, cx);
1655 }
1656 }
1657
1658 pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext<Self>) -> bool {
1659 let result = self.panes.iter().find_map(|pane| {
1660 pane.read(cx)
1661 .index_for_item(item)
1662 .map(|ix| (pane.clone(), ix))
1663 });
1664 if let Some((pane, ix)) = result {
1665 pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
1666 true
1667 } else {
1668 false
1669 }
1670 }
1671
1672 fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
1673 let panes = self.center.panes();
1674 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
1675 cx.focus(&pane);
1676 } else {
1677 self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx);
1678 }
1679 }
1680
1681 pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
1682 let panes = self.center.panes();
1683 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
1684 let next_ix = (ix + 1) % panes.len();
1685 let next_pane = panes[next_ix].clone();
1686 cx.focus(&next_pane);
1687 }
1688 }
1689
1690 pub fn activate_previous_pane(&mut self, cx: &mut ViewContext<Self>) {
1691 let panes = self.center.panes();
1692 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
1693 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
1694 let prev_pane = panes[prev_ix].clone();
1695 cx.focus(&prev_pane);
1696 }
1697 }
1698
1699 fn handle_pane_focused(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1700 if self.active_pane != pane {
1701 self.active_pane
1702 .update(cx, |pane, cx| pane.set_active(false, cx));
1703 self.active_pane = pane.clone();
1704 self.active_pane
1705 .update(cx, |pane, cx| pane.set_active(true, cx));
1706 self.status_bar.update(cx, |status_bar, cx| {
1707 status_bar.set_active_pane(&self.active_pane, cx);
1708 });
1709 self.active_item_path_changed(cx);
1710 self.last_active_center_pane = Some(pane.downgrade());
1711 }
1712
1713 self.update_followers(
1714 proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
1715 id: self.active_item(cx).and_then(|item| {
1716 item.to_followable_item_handle(cx)?
1717 .remote_id(&self.app_state.client, cx)
1718 .map(|id| id.to_proto())
1719 }),
1720 leader_id: self.leader_for_pane(&pane),
1721 }),
1722 cx,
1723 );
1724
1725 cx.notify();
1726 }
1727
1728 fn handle_pane_event(
1729 &mut self,
1730 pane: ViewHandle<Pane>,
1731 event: &pane::Event,
1732 cx: &mut ViewContext<Self>,
1733 ) {
1734 match event {
1735 pane::Event::Split(direction) => {
1736 self.split_pane(pane, *direction, cx);
1737 }
1738 pane::Event::Remove => self.remove_pane(pane, cx),
1739 pane::Event::ActivateItem { local } => {
1740 if *local {
1741 self.unfollow(&pane, cx);
1742 }
1743 if &pane == self.active_pane() {
1744 self.active_item_path_changed(cx);
1745 }
1746 }
1747 pane::Event::ChangeItemTitle => {
1748 if pane == self.active_pane {
1749 self.active_item_path_changed(cx);
1750 }
1751 self.update_window_edited(cx);
1752 }
1753 pane::Event::RemoveItem { item_id } => {
1754 self.update_window_edited(cx);
1755 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
1756 if entry.get().id() == pane.id() {
1757 entry.remove();
1758 }
1759 }
1760 }
1761 pane::Event::Focus => {
1762 self.handle_pane_focused(pane.clone(), cx);
1763 }
1764 pane::Event::ZoomIn => {
1765 if pane == self.active_pane {
1766 self.zoom_out(cx);
1767 pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
1768 cx.notify();
1769 }
1770 }
1771 pane::Event::ZoomOut => self.zoom_out(cx),
1772 }
1773
1774 self.serialize_workspace(cx);
1775 }
1776
1777 pub fn split_pane(
1778 &mut self,
1779 pane: ViewHandle<Pane>,
1780 direction: SplitDirection,
1781 cx: &mut ViewContext<Self>,
1782 ) -> Option<ViewHandle<Pane>> {
1783 let item = pane.read(cx).active_item()?;
1784 let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
1785 let new_pane = self.add_pane(cx);
1786 Pane::add_item(self, &new_pane, clone, true, true, None, cx);
1787 self.center.split(&pane, &new_pane, direction).unwrap();
1788 Some(new_pane)
1789 } else {
1790 None
1791 };
1792 cx.notify();
1793 maybe_pane_handle
1794 }
1795
1796 pub fn split_pane_with_item(
1797 &mut self,
1798 pane_to_split: WeakViewHandle<Pane>,
1799 split_direction: SplitDirection,
1800 from: WeakViewHandle<Pane>,
1801 item_id_to_move: usize,
1802 cx: &mut ViewContext<Self>,
1803 ) {
1804 let Some(pane_to_split) = pane_to_split.upgrade(cx) else { return; };
1805 let Some(from) = from.upgrade(cx) else { return; };
1806
1807 let new_pane = self.add_pane(cx);
1808 Pane::move_item(self, from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
1809 self.center
1810 .split(&pane_to_split, &new_pane, split_direction)
1811 .unwrap();
1812 cx.notify();
1813 }
1814
1815 pub fn split_pane_with_project_entry(
1816 &mut self,
1817 pane_to_split: WeakViewHandle<Pane>,
1818 split_direction: SplitDirection,
1819 project_entry: ProjectEntryId,
1820 cx: &mut ViewContext<Self>,
1821 ) -> Option<Task<Result<()>>> {
1822 let pane_to_split = pane_to_split.upgrade(cx)?;
1823 let new_pane = self.add_pane(cx);
1824 self.center
1825 .split(&pane_to_split, &new_pane, split_direction)
1826 .unwrap();
1827
1828 let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
1829 let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
1830 Some(cx.foreground().spawn(async move {
1831 task.await?;
1832 Ok(())
1833 }))
1834 }
1835
1836 fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1837 if self.center.remove(&pane).unwrap() {
1838 self.force_remove_pane(&pane, cx);
1839 self.unfollow(&pane, cx);
1840 self.last_leaders_by_pane.remove(&pane.downgrade());
1841 for removed_item in pane.read(cx).items() {
1842 self.panes_by_item.remove(&removed_item.id());
1843 }
1844
1845 cx.notify();
1846 } else {
1847 self.active_item_path_changed(cx);
1848 }
1849 }
1850
1851 pub fn panes(&self) -> &[ViewHandle<Pane>] {
1852 &self.panes
1853 }
1854
1855 pub fn active_pane(&self) -> &ViewHandle<Pane> {
1856 &self.active_pane
1857 }
1858
1859 fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
1860 if let Some(remote_id) = remote_id {
1861 self.remote_entity_subscription = Some(
1862 self.app_state
1863 .client
1864 .add_view_for_remote_entity(remote_id, cx),
1865 );
1866 } else {
1867 self.remote_entity_subscription.take();
1868 }
1869 }
1870
1871 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
1872 self.leader_state.followers.remove(&peer_id);
1873 if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
1874 for state in states_by_pane.into_values() {
1875 for item in state.items_by_leader_view_id.into_values() {
1876 item.set_leader_replica_id(None, cx);
1877 }
1878 }
1879 }
1880 cx.notify();
1881 }
1882
1883 pub fn toggle_follow(
1884 &mut self,
1885 leader_id: PeerId,
1886 cx: &mut ViewContext<Self>,
1887 ) -> Option<Task<Result<()>>> {
1888 let pane = self.active_pane().clone();
1889
1890 if let Some(prev_leader_id) = self.unfollow(&pane, cx) {
1891 if leader_id == prev_leader_id {
1892 return None;
1893 }
1894 }
1895
1896 self.last_leaders_by_pane
1897 .insert(pane.downgrade(), leader_id);
1898 self.follower_states_by_leader
1899 .entry(leader_id)
1900 .or_default()
1901 .insert(pane.clone(), Default::default());
1902 cx.notify();
1903
1904 let project_id = self.project.read(cx).remote_id()?;
1905 let request = self.app_state.client.request(proto::Follow {
1906 project_id,
1907 leader_id: Some(leader_id),
1908 });
1909
1910 Some(cx.spawn(|this, mut cx| async move {
1911 let response = request.await?;
1912 this.update(&mut cx, |this, _| {
1913 let state = this
1914 .follower_states_by_leader
1915 .get_mut(&leader_id)
1916 .and_then(|states_by_pane| states_by_pane.get_mut(&pane))
1917 .ok_or_else(|| anyhow!("following interrupted"))?;
1918 state.active_view_id = if let Some(active_view_id) = response.active_view_id {
1919 Some(ViewId::from_proto(active_view_id)?)
1920 } else {
1921 None
1922 };
1923 Ok::<_, anyhow::Error>(())
1924 })??;
1925 Self::add_views_from_leader(
1926 this.clone(),
1927 leader_id,
1928 vec![pane],
1929 response.views,
1930 &mut cx,
1931 )
1932 .await?;
1933 this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
1934 Ok(())
1935 }))
1936 }
1937
1938 pub fn follow_next_collaborator(
1939 &mut self,
1940 _: &FollowNextCollaborator,
1941 cx: &mut ViewContext<Self>,
1942 ) -> Option<Task<Result<()>>> {
1943 let collaborators = self.project.read(cx).collaborators();
1944 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
1945 let mut collaborators = collaborators.keys().copied();
1946 for peer_id in collaborators.by_ref() {
1947 if peer_id == leader_id {
1948 break;
1949 }
1950 }
1951 collaborators.next()
1952 } else if let Some(last_leader_id) =
1953 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
1954 {
1955 if collaborators.contains_key(last_leader_id) {
1956 Some(*last_leader_id)
1957 } else {
1958 None
1959 }
1960 } else {
1961 None
1962 };
1963
1964 next_leader_id
1965 .or_else(|| collaborators.keys().copied().next())
1966 .and_then(|leader_id| self.toggle_follow(leader_id, cx))
1967 }
1968
1969 pub fn unfollow(
1970 &mut self,
1971 pane: &ViewHandle<Pane>,
1972 cx: &mut ViewContext<Self>,
1973 ) -> Option<PeerId> {
1974 for (leader_id, states_by_pane) in &mut self.follower_states_by_leader {
1975 let leader_id = *leader_id;
1976 if let Some(state) = states_by_pane.remove(pane) {
1977 for (_, item) in state.items_by_leader_view_id {
1978 item.set_leader_replica_id(None, cx);
1979 }
1980
1981 if states_by_pane.is_empty() {
1982 self.follower_states_by_leader.remove(&leader_id);
1983 if let Some(project_id) = self.project.read(cx).remote_id() {
1984 self.app_state
1985 .client
1986 .send(proto::Unfollow {
1987 project_id,
1988 leader_id: Some(leader_id),
1989 })
1990 .log_err();
1991 }
1992 }
1993
1994 cx.notify();
1995 return Some(leader_id);
1996 }
1997 }
1998 None
1999 }
2000
2001 pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
2002 self.follower_states_by_leader.contains_key(&peer_id)
2003 }
2004
2005 pub fn is_followed_by(&self, peer_id: PeerId) -> bool {
2006 self.leader_state.followers.contains(&peer_id)
2007 }
2008
2009 fn render_titlebar(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
2010 // TODO: There should be a better system in place for this
2011 // (https://github.com/zed-industries/zed/issues/1290)
2012 let is_fullscreen = cx.window_is_fullscreen();
2013 let container_theme = if is_fullscreen {
2014 let mut container_theme = theme.workspace.titlebar.container;
2015 container_theme.padding.left = container_theme.padding.right;
2016 container_theme
2017 } else {
2018 theme.workspace.titlebar.container
2019 };
2020
2021 enum TitleBar {}
2022 MouseEventHandler::<TitleBar, _>::new(0, cx, |_, cx| {
2023 Stack::new()
2024 .with_children(
2025 self.titlebar_item
2026 .as_ref()
2027 .map(|item| ChildView::new(item, cx)),
2028 )
2029 .contained()
2030 .with_style(container_theme)
2031 })
2032 .on_click(MouseButton::Left, |event, _, cx| {
2033 if event.click_count == 2 {
2034 cx.zoom_window();
2035 }
2036 })
2037 .constrained()
2038 .with_height(theme.workspace.titlebar.height)
2039 .into_any_named("titlebar")
2040 }
2041
2042 fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
2043 let active_entry = self.active_project_path(cx);
2044 self.project
2045 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
2046 self.update_window_title(cx);
2047 }
2048
2049 fn update_window_title(&mut self, cx: &mut ViewContext<Self>) {
2050 let project = self.project().read(cx);
2051 let mut title = String::new();
2052
2053 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
2054 let filename = path
2055 .path
2056 .file_name()
2057 .map(|s| s.to_string_lossy())
2058 .or_else(|| {
2059 Some(Cow::Borrowed(
2060 project
2061 .worktree_for_id(path.worktree_id, cx)?
2062 .read(cx)
2063 .root_name(),
2064 ))
2065 });
2066
2067 if let Some(filename) = filename {
2068 title.push_str(filename.as_ref());
2069 title.push_str(" β ");
2070 }
2071 }
2072
2073 for (i, name) in project.worktree_root_names(cx).enumerate() {
2074 if i > 0 {
2075 title.push_str(", ");
2076 }
2077 title.push_str(name);
2078 }
2079
2080 if title.is_empty() {
2081 title = "empty project".to_string();
2082 }
2083
2084 if project.is_remote() {
2085 title.push_str(" β");
2086 } else if project.is_shared() {
2087 title.push_str(" β");
2088 }
2089
2090 cx.set_window_title(&title);
2091 }
2092
2093 fn update_window_edited(&mut self, cx: &mut ViewContext<Self>) {
2094 let is_edited = !self.project.read(cx).is_read_only()
2095 && self
2096 .items(cx)
2097 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
2098 if is_edited != self.window_edited {
2099 self.window_edited = is_edited;
2100 cx.set_window_edited(self.window_edited)
2101 }
2102 }
2103
2104 fn render_disconnected_overlay(
2105 &self,
2106 cx: &mut ViewContext<Workspace>,
2107 ) -> Option<AnyElement<Workspace>> {
2108 if self.project.read(cx).is_read_only() {
2109 enum DisconnectedOverlay {}
2110 Some(
2111 MouseEventHandler::<DisconnectedOverlay, _>::new(0, cx, |_, cx| {
2112 let theme = &theme::current(cx);
2113 Label::new(
2114 "Your connection to the remote project has been lost.",
2115 theme.workspace.disconnected_overlay.text.clone(),
2116 )
2117 .aligned()
2118 .contained()
2119 .with_style(theme.workspace.disconnected_overlay.container)
2120 })
2121 .with_cursor_style(CursorStyle::Arrow)
2122 .capture_all()
2123 .into_any_named("disconnected overlay"),
2124 )
2125 } else {
2126 None
2127 }
2128 }
2129
2130 fn render_notifications(
2131 &self,
2132 theme: &theme::Workspace,
2133 cx: &AppContext,
2134 ) -> Option<AnyElement<Workspace>> {
2135 if self.notifications.is_empty() {
2136 None
2137 } else {
2138 Some(
2139 Flex::column()
2140 .with_children(self.notifications.iter().map(|(_, _, notification)| {
2141 ChildView::new(notification.as_any(), cx)
2142 .contained()
2143 .with_style(theme.notification)
2144 }))
2145 .constrained()
2146 .with_width(theme.notifications.width)
2147 .contained()
2148 .with_style(theme.notifications.container)
2149 .aligned()
2150 .bottom()
2151 .right()
2152 .into_any(),
2153 )
2154 }
2155 }
2156
2157 // RPC handlers
2158
2159 async fn handle_follow(
2160 this: WeakViewHandle<Self>,
2161 envelope: TypedEnvelope<proto::Follow>,
2162 _: Arc<Client>,
2163 mut cx: AsyncAppContext,
2164 ) -> Result<proto::FollowResponse> {
2165 this.update(&mut cx, |this, cx| {
2166 let client = &this.app_state.client;
2167 this.leader_state
2168 .followers
2169 .insert(envelope.original_sender_id()?);
2170
2171 let active_view_id = this.active_item(cx).and_then(|i| {
2172 Some(
2173 i.to_followable_item_handle(cx)?
2174 .remote_id(client, cx)?
2175 .to_proto(),
2176 )
2177 });
2178
2179 cx.notify();
2180
2181 Ok(proto::FollowResponse {
2182 active_view_id,
2183 views: this
2184 .panes()
2185 .iter()
2186 .flat_map(|pane| {
2187 let leader_id = this.leader_for_pane(pane);
2188 pane.read(cx).items().filter_map({
2189 let cx = &cx;
2190 move |item| {
2191 let item = item.to_followable_item_handle(cx)?;
2192 let id = item.remote_id(client, cx)?.to_proto();
2193 let variant = item.to_state_proto(cx)?;
2194 Some(proto::View {
2195 id: Some(id),
2196 leader_id,
2197 variant: Some(variant),
2198 })
2199 }
2200 })
2201 })
2202 .collect(),
2203 })
2204 })?
2205 }
2206
2207 async fn handle_unfollow(
2208 this: WeakViewHandle<Self>,
2209 envelope: TypedEnvelope<proto::Unfollow>,
2210 _: Arc<Client>,
2211 mut cx: AsyncAppContext,
2212 ) -> Result<()> {
2213 this.update(&mut cx, |this, cx| {
2214 this.leader_state
2215 .followers
2216 .remove(&envelope.original_sender_id()?);
2217 cx.notify();
2218 Ok(())
2219 })?
2220 }
2221
2222 async fn handle_update_followers(
2223 this: WeakViewHandle<Self>,
2224 envelope: TypedEnvelope<proto::UpdateFollowers>,
2225 _: Arc<Client>,
2226 cx: AsyncAppContext,
2227 ) -> Result<()> {
2228 let leader_id = envelope.original_sender_id()?;
2229 this.read_with(&cx, |this, _| {
2230 this.leader_updates_tx
2231 .unbounded_send((leader_id, envelope.payload))
2232 })??;
2233 Ok(())
2234 }
2235
2236 async fn process_leader_update(
2237 this: &WeakViewHandle<Self>,
2238 leader_id: PeerId,
2239 update: proto::UpdateFollowers,
2240 cx: &mut AsyncAppContext,
2241 ) -> Result<()> {
2242 match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
2243 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
2244 this.update(cx, |this, _| {
2245 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2246 for state in state.values_mut() {
2247 state.active_view_id =
2248 if let Some(active_view_id) = update_active_view.id.clone() {
2249 Some(ViewId::from_proto(active_view_id)?)
2250 } else {
2251 None
2252 };
2253 }
2254 }
2255 anyhow::Ok(())
2256 })??;
2257 }
2258 proto::update_followers::Variant::UpdateView(update_view) => {
2259 let variant = update_view
2260 .variant
2261 .ok_or_else(|| anyhow!("missing update view variant"))?;
2262 let id = update_view
2263 .id
2264 .ok_or_else(|| anyhow!("missing update view id"))?;
2265 let mut tasks = Vec::new();
2266 this.update(cx, |this, cx| {
2267 let project = this.project.clone();
2268 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2269 for state in state.values_mut() {
2270 let view_id = ViewId::from_proto(id.clone())?;
2271 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
2272 tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
2273 }
2274 }
2275 }
2276 anyhow::Ok(())
2277 })??;
2278 try_join_all(tasks).await.log_err();
2279 }
2280 proto::update_followers::Variant::CreateView(view) => {
2281 let panes = this.read_with(cx, |this, _| {
2282 this.follower_states_by_leader
2283 .get(&leader_id)
2284 .into_iter()
2285 .flat_map(|states_by_pane| states_by_pane.keys())
2286 .cloned()
2287 .collect()
2288 })?;
2289 Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?;
2290 }
2291 }
2292 this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
2293 Ok(())
2294 }
2295
2296 async fn add_views_from_leader(
2297 this: WeakViewHandle<Self>,
2298 leader_id: PeerId,
2299 panes: Vec<ViewHandle<Pane>>,
2300 views: Vec<proto::View>,
2301 cx: &mut AsyncAppContext,
2302 ) -> Result<()> {
2303 let project = this.read_with(cx, |this, _| this.project.clone())?;
2304 let replica_id = project
2305 .read_with(cx, |project, _| {
2306 project
2307 .collaborators()
2308 .get(&leader_id)
2309 .map(|c| c.replica_id)
2310 })
2311 .ok_or_else(|| anyhow!("no such collaborator {}", leader_id))?;
2312
2313 let item_builders = cx.update(|cx| {
2314 cx.default_global::<FollowableItemBuilders>()
2315 .values()
2316 .map(|b| b.0)
2317 .collect::<Vec<_>>()
2318 });
2319
2320 let mut item_tasks_by_pane = HashMap::default();
2321 for pane in panes {
2322 let mut item_tasks = Vec::new();
2323 let mut leader_view_ids = Vec::new();
2324 for view in &views {
2325 let Some(id) = &view.id else { continue };
2326 let id = ViewId::from_proto(id.clone())?;
2327 let mut variant = view.variant.clone();
2328 if variant.is_none() {
2329 Err(anyhow!("missing variant"))?;
2330 }
2331 for build_item in &item_builders {
2332 let task = cx.update(|cx| {
2333 build_item(pane.clone(), project.clone(), id, &mut variant, cx)
2334 });
2335 if let Some(task) = task {
2336 item_tasks.push(task);
2337 leader_view_ids.push(id);
2338 break;
2339 } else {
2340 assert!(variant.is_some());
2341 }
2342 }
2343 }
2344
2345 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
2346 }
2347
2348 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
2349 let items = futures::future::try_join_all(item_tasks).await?;
2350 this.update(cx, |this, cx| {
2351 let state = this
2352 .follower_states_by_leader
2353 .get_mut(&leader_id)?
2354 .get_mut(&pane)?;
2355
2356 for (id, item) in leader_view_ids.into_iter().zip(items) {
2357 item.set_leader_replica_id(Some(replica_id), cx);
2358 state.items_by_leader_view_id.insert(id, item);
2359 }
2360
2361 Some(())
2362 })?;
2363 }
2364 Ok(())
2365 }
2366
2367 fn update_followers(
2368 &self,
2369 update: proto::update_followers::Variant,
2370 cx: &AppContext,
2371 ) -> Option<()> {
2372 let project_id = self.project.read(cx).remote_id()?;
2373 if !self.leader_state.followers.is_empty() {
2374 self.app_state
2375 .client
2376 .send(proto::UpdateFollowers {
2377 project_id,
2378 follower_ids: self.leader_state.followers.iter().copied().collect(),
2379 variant: Some(update),
2380 })
2381 .log_err();
2382 }
2383 None
2384 }
2385
2386 pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
2387 self.follower_states_by_leader
2388 .iter()
2389 .find_map(|(leader_id, state)| {
2390 if state.contains_key(pane) {
2391 Some(*leader_id)
2392 } else {
2393 None
2394 }
2395 })
2396 }
2397
2398 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
2399 cx.notify();
2400
2401 let call = self.active_call()?;
2402 let room = call.read(cx).room()?.read(cx);
2403 let participant = room.remote_participant_for_peer_id(leader_id)?;
2404 let mut items_to_activate = Vec::new();
2405 match participant.location {
2406 call::ParticipantLocation::SharedProject { project_id } => {
2407 if Some(project_id) == self.project.read(cx).remote_id() {
2408 for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
2409 if let Some(item) = state
2410 .active_view_id
2411 .and_then(|id| state.items_by_leader_view_id.get(&id))
2412 {
2413 items_to_activate.push((pane.clone(), item.boxed_clone()));
2414 } else {
2415 if let Some(shared_screen) =
2416 self.shared_screen_for_peer(leader_id, pane, cx)
2417 {
2418 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2419 }
2420 }
2421 }
2422 }
2423 }
2424 call::ParticipantLocation::UnsharedProject => {}
2425 call::ParticipantLocation::External => {
2426 for (pane, _) in self.follower_states_by_leader.get(&leader_id)? {
2427 if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
2428 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2429 }
2430 }
2431 }
2432 }
2433
2434 for (pane, item) in items_to_activate {
2435 let pane_was_focused = pane.read(cx).has_focus();
2436 if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
2437 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
2438 } else {
2439 Pane::add_item(self, &pane, item.boxed_clone(), false, false, None, cx);
2440 }
2441
2442 if pane_was_focused {
2443 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
2444 }
2445 }
2446
2447 None
2448 }
2449
2450 fn shared_screen_for_peer(
2451 &self,
2452 peer_id: PeerId,
2453 pane: &ViewHandle<Pane>,
2454 cx: &mut ViewContext<Self>,
2455 ) -> Option<ViewHandle<SharedScreen>> {
2456 let call = self.active_call()?;
2457 let room = call.read(cx).room()?.read(cx);
2458 let participant = room.remote_participant_for_peer_id(peer_id)?;
2459 let track = participant.tracks.values().next()?.clone();
2460 let user = participant.user.clone();
2461
2462 for item in pane.read(cx).items_of_type::<SharedScreen>() {
2463 if item.read(cx).peer_id == peer_id {
2464 return Some(item);
2465 }
2466 }
2467
2468 Some(cx.add_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
2469 }
2470
2471 pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
2472 if active {
2473 cx.background()
2474 .spawn(persistence::DB.update_timestamp(self.database_id()))
2475 .detach();
2476 } else {
2477 for pane in &self.panes {
2478 pane.update(cx, |pane, cx| {
2479 if let Some(item) = pane.active_item() {
2480 item.workspace_deactivated(cx);
2481 }
2482 if matches!(
2483 settings::get::<WorkspaceSettings>(cx).autosave,
2484 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
2485 ) {
2486 for item in pane.items() {
2487 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
2488 .detach_and_log_err(cx);
2489 }
2490 }
2491 });
2492 }
2493 }
2494 }
2495
2496 fn active_call(&self) -> Option<&ModelHandle<ActiveCall>> {
2497 self.active_call.as_ref().map(|(call, _)| call)
2498 }
2499
2500 fn on_active_call_event(
2501 &mut self,
2502 _: ModelHandle<ActiveCall>,
2503 event: &call::room::Event,
2504 cx: &mut ViewContext<Self>,
2505 ) {
2506 match event {
2507 call::room::Event::ParticipantLocationChanged { participant_id }
2508 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
2509 self.leader_updated(*participant_id, cx);
2510 }
2511 _ => {}
2512 }
2513 }
2514
2515 pub fn database_id(&self) -> WorkspaceId {
2516 self.database_id
2517 }
2518
2519 fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
2520 let project = self.project().read(cx);
2521
2522 if project.is_local() {
2523 Some(
2524 project
2525 .visible_worktrees(cx)
2526 .map(|worktree| worktree.read(cx).abs_path())
2527 .collect::<Vec<_>>()
2528 .into(),
2529 )
2530 } else {
2531 None
2532 }
2533 }
2534
2535 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
2536 match member {
2537 Member::Axis(PaneAxis { members, .. }) => {
2538 for child in members.iter() {
2539 self.remove_panes(child.clone(), cx)
2540 }
2541 }
2542 Member::Pane(pane) => {
2543 self.force_remove_pane(&pane, cx);
2544 }
2545 }
2546 }
2547
2548 fn force_remove_pane(&mut self, pane: &ViewHandle<Pane>, cx: &mut ViewContext<Workspace>) {
2549 self.panes.retain(|p| p != pane);
2550 cx.focus(self.panes.last().unwrap());
2551 if self.last_active_center_pane == Some(pane.downgrade()) {
2552 self.last_active_center_pane = None;
2553 }
2554 cx.notify();
2555 }
2556
2557 fn serialize_workspace(&self, cx: &AppContext) {
2558 fn serialize_pane_handle(
2559 pane_handle: &ViewHandle<Pane>,
2560 cx: &AppContext,
2561 ) -> SerializedPane {
2562 let (items, active) = {
2563 let pane = pane_handle.read(cx);
2564 let active_item_id = pane.active_item().map(|item| item.id());
2565 (
2566 pane.items()
2567 .filter_map(|item_handle| {
2568 Some(SerializedItem {
2569 kind: Arc::from(item_handle.serialized_item_kind()?),
2570 item_id: item_handle.id(),
2571 active: Some(item_handle.id()) == active_item_id,
2572 })
2573 })
2574 .collect::<Vec<_>>(),
2575 pane.is_active(),
2576 )
2577 };
2578
2579 SerializedPane::new(items, active)
2580 }
2581
2582 fn build_serialized_pane_group(
2583 pane_group: &Member,
2584 cx: &AppContext,
2585 ) -> SerializedPaneGroup {
2586 match pane_group {
2587 Member::Axis(PaneAxis { axis, members }) => SerializedPaneGroup::Group {
2588 axis: *axis,
2589 children: members
2590 .iter()
2591 .map(|member| build_serialized_pane_group(member, cx))
2592 .collect::<Vec<_>>(),
2593 },
2594 Member::Pane(pane_handle) => {
2595 SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx))
2596 }
2597 }
2598 }
2599
2600 fn build_serialized_docks(this: &Workspace, cx: &AppContext) -> DockStructure {
2601 let left_dock = this.left_dock.read(cx);
2602 let left_visible = left_dock.is_open();
2603 let left_active_panel = left_dock.active_panel().and_then(|panel| {
2604 Some(
2605 cx.view_ui_name(panel.as_any().window_id(), panel.id())?
2606 .to_string(),
2607 )
2608 });
2609
2610 let right_dock = this.right_dock.read(cx);
2611 let right_visible = right_dock.is_open();
2612 let right_active_panel = right_dock.active_panel().and_then(|panel| {
2613 Some(
2614 cx.view_ui_name(panel.as_any().window_id(), panel.id())?
2615 .to_string(),
2616 )
2617 });
2618
2619 let bottom_dock = this.bottom_dock.read(cx);
2620 let bottom_visible = bottom_dock.is_open();
2621 let bottom_active_panel = bottom_dock.active_panel().and_then(|panel| {
2622 Some(
2623 cx.view_ui_name(panel.as_any().window_id(), panel.id())?
2624 .to_string(),
2625 )
2626 });
2627
2628 DockStructure {
2629 left: DockData {
2630 visible: left_visible,
2631 active_panel: left_active_panel,
2632 },
2633 right: DockData {
2634 visible: right_visible,
2635 active_panel: right_active_panel,
2636 },
2637 bottom: DockData {
2638 visible: bottom_visible,
2639 active_panel: bottom_active_panel,
2640 },
2641 }
2642 }
2643
2644 if let Some(location) = self.location(cx) {
2645 // Load bearing special case:
2646 // - with_local_workspace() relies on this to not have other stuff open
2647 // when you open your log
2648 if !location.paths().is_empty() {
2649 let center_group = build_serialized_pane_group(&self.center.root, cx);
2650 let docks = build_serialized_docks(self, cx);
2651
2652 let serialized_workspace = SerializedWorkspace {
2653 id: self.database_id,
2654 location,
2655 center_group,
2656 bounds: Default::default(),
2657 display: Default::default(),
2658 docks,
2659 };
2660
2661 cx.background()
2662 .spawn(persistence::DB.save_workspace(serialized_workspace))
2663 .detach();
2664 }
2665 }
2666 }
2667
2668 pub(crate) fn load_workspace(
2669 workspace: WeakViewHandle<Workspace>,
2670 serialized_workspace: SerializedWorkspace,
2671 paths_to_open: Vec<Option<ProjectPath>>,
2672 cx: &mut AppContext,
2673 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
2674 cx.spawn(|mut cx| async move {
2675 let result = async_iife! {{
2676 let (project, old_center_pane) =
2677 workspace.read_with(&cx, |workspace, _| {
2678 (
2679 workspace.project().clone(),
2680 workspace.last_active_center_pane.clone(),
2681 )
2682 })?;
2683
2684 let mut center_items = None;
2685 let mut center_group = None;
2686 // Traverse the splits tree and add to things
2687 if let Some((group, active_pane, items)) = serialized_workspace
2688 .center_group
2689 .deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
2690 .await {
2691 center_items = Some(items);
2692 center_group = Some((group, active_pane))
2693 }
2694
2695 let resulting_list = cx.read(|cx| {
2696 let mut opened_items = center_items
2697 .unwrap_or_default()
2698 .into_iter()
2699 .filter_map(|item| {
2700 let item = item?;
2701 let project_path = item.project_path(cx)?;
2702 Some((project_path, item))
2703 })
2704 .collect::<HashMap<_, _>>();
2705
2706 paths_to_open
2707 .into_iter()
2708 .map(|path_to_open| {
2709 path_to_open.map(|path_to_open| {
2710 Ok(opened_items.remove(&path_to_open))
2711 })
2712 .transpose()
2713 .map(|item| item.flatten())
2714 .transpose()
2715 })
2716 .collect::<Vec<_>>()
2717 });
2718
2719 // Remove old panes from workspace panes list
2720 workspace.update(&mut cx, |workspace, cx| {
2721 if let Some((center_group, active_pane)) = center_group {
2722 workspace.remove_panes(workspace.center.root.clone(), cx);
2723
2724 // Swap workspace center group
2725 workspace.center = PaneGroup::with_root(center_group);
2726
2727 // Change the focus to the workspace first so that we retrigger focus in on the pane.
2728 cx.focus_self();
2729
2730 if let Some(active_pane) = active_pane {
2731 cx.focus(&active_pane);
2732 } else {
2733 cx.focus(workspace.panes.last().unwrap());
2734 }
2735 } else {
2736 let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx));
2737 if let Some(old_center_handle) = old_center_handle {
2738 cx.focus(&old_center_handle)
2739 } else {
2740 cx.focus_self()
2741 }
2742 }
2743
2744 let docks = serialized_workspace.docks;
2745 workspace.left_dock.update(cx, |dock, cx| {
2746 dock.set_open(docks.left.visible, cx);
2747 if let Some(active_panel) = docks.left.active_panel {
2748 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
2749 dock.activate_panel(ix, cx);
2750 }
2751 }
2752 });
2753 workspace.right_dock.update(cx, |dock, cx| {
2754 dock.set_open(docks.right.visible, cx);
2755 if let Some(active_panel) = docks.right.active_panel {
2756 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
2757 dock.activate_panel(ix, cx);
2758 }
2759 }
2760 });
2761 workspace.bottom_dock.update(cx, |dock, cx| {
2762 dock.set_open(docks.bottom.visible, cx);
2763 if let Some(active_panel) = docks.bottom.active_panel {
2764 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
2765 dock.activate_panel(ix, cx);
2766 }
2767 }
2768 });
2769
2770 cx.notify();
2771 })?;
2772
2773 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
2774 workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?;
2775
2776 Ok::<_, anyhow::Error>(resulting_list)
2777 }};
2778
2779 result.await.unwrap_or_default()
2780 })
2781 }
2782
2783 #[cfg(any(test, feature = "test-support"))]
2784 pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
2785 let app_state = Arc::new(AppState {
2786 languages: project.read(cx).languages().clone(),
2787 client: project.read(cx).client(),
2788 user_store: project.read(cx).user_store(),
2789 fs: project.read(cx).fs().clone(),
2790 build_window_options: |_, _, _| Default::default(),
2791 initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
2792 background_actions: || &[],
2793 });
2794 Self::new(0, project, app_state, cx)
2795 }
2796
2797 fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option<AnyElement<Self>> {
2798 let dock = match position {
2799 DockPosition::Left => &self.left_dock,
2800 DockPosition::Right => &self.right_dock,
2801 DockPosition::Bottom => &self.bottom_dock,
2802 };
2803 let active_panel = dock.read(cx).active_panel()?;
2804 let element = if Some(active_panel.as_any()) == self.zoomed(cx).as_ref() {
2805 dock.read(cx).render_placeholder(cx)
2806 } else {
2807 ChildView::new(dock, cx).into_any()
2808 };
2809
2810 Some(
2811 element
2812 .constrained()
2813 .dynamically(move |constraint, _, cx| match position {
2814 DockPosition::Left | DockPosition::Right => SizeConstraint::new(
2815 Vector2F::new(20., constraint.min.y()),
2816 Vector2F::new(cx.window_size().x() * 0.8, constraint.max.y()),
2817 ),
2818 _ => constraint,
2819 })
2820 .into_any(),
2821 )
2822 }
2823}
2824
2825async fn open_items(
2826 serialized_workspace: Option<SerializedWorkspace>,
2827 workspace: &WeakViewHandle<Workspace>,
2828 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
2829 app_state: Arc<AppState>,
2830 mut cx: AsyncAppContext,
2831) -> Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>> {
2832 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
2833
2834 if let Some(serialized_workspace) = serialized_workspace {
2835 let workspace = workspace.clone();
2836 let restored_items = cx
2837 .update(|cx| {
2838 Workspace::load_workspace(
2839 workspace,
2840 serialized_workspace,
2841 project_paths_to_open
2842 .iter()
2843 .map(|(_, project_path)| project_path)
2844 .cloned()
2845 .collect(),
2846 cx,
2847 )
2848 })
2849 .await;
2850
2851 let restored_project_paths = cx.read(|cx| {
2852 restored_items
2853 .iter()
2854 .filter_map(|item| item.as_ref()?.as_ref().ok()?.project_path(cx))
2855 .collect::<HashSet<_>>()
2856 });
2857
2858 opened_items = restored_items;
2859 project_paths_to_open
2860 .iter_mut()
2861 .for_each(|(_, project_path)| {
2862 if let Some(project_path_to_open) = project_path {
2863 if restored_project_paths.contains(project_path_to_open) {
2864 *project_path = None;
2865 }
2866 }
2867 });
2868 } else {
2869 for _ in 0..project_paths_to_open.len() {
2870 opened_items.push(None);
2871 }
2872 }
2873 assert!(opened_items.len() == project_paths_to_open.len());
2874
2875 let tasks =
2876 project_paths_to_open
2877 .into_iter()
2878 .enumerate()
2879 .map(|(i, (abs_path, project_path))| {
2880 let workspace = workspace.clone();
2881 cx.spawn(|mut cx| {
2882 let fs = app_state.fs.clone();
2883 async move {
2884 let file_project_path = project_path?;
2885 if fs.is_file(&abs_path).await {
2886 Some((
2887 i,
2888 workspace
2889 .update(&mut cx, |workspace, cx| {
2890 workspace.open_path(file_project_path, None, true, cx)
2891 })
2892 .log_err()?
2893 .await,
2894 ))
2895 } else {
2896 None
2897 }
2898 }
2899 })
2900 });
2901
2902 for maybe_opened_path in futures::future::join_all(tasks.into_iter())
2903 .await
2904 .into_iter()
2905 {
2906 if let Some((i, path_open_result)) = maybe_opened_path {
2907 opened_items[i] = Some(path_open_result);
2908 }
2909 }
2910
2911 opened_items
2912}
2913
2914fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
2915 const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
2916
2917 workspace
2918 .update(cx, |workspace, cx| {
2919 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
2920 workspace.show_notification_once(0, cx, |cx| {
2921 cx.add_view(|_| {
2922 MessageNotification::new("Failed to load any database file.")
2923 .with_click_message("Click to let us know about this error")
2924 .on_click(|cx| cx.platform().open_url(REPORT_ISSUE_URL))
2925 })
2926 });
2927 } else {
2928 let backup_path = (*db::BACKUP_DB_PATH).read();
2929 if let Some(backup_path) = backup_path.clone() {
2930 workspace.show_notification_once(0, cx, move |cx| {
2931 cx.add_view(move |_| {
2932 MessageNotification::new(format!(
2933 "Database file was corrupted. Old database backed up to {}",
2934 backup_path.display()
2935 ))
2936 .with_click_message("Click to show old database in finder")
2937 .on_click(move |cx| {
2938 cx.platform().open_url(&backup_path.to_string_lossy())
2939 })
2940 })
2941 });
2942 }
2943 }
2944 })
2945 .log_err();
2946}
2947
2948impl Entity for Workspace {
2949 type Event = Event;
2950}
2951
2952impl View for Workspace {
2953 fn ui_name() -> &'static str {
2954 "Workspace"
2955 }
2956
2957 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
2958 let theme = theme::current(cx).clone();
2959 Stack::new()
2960 .with_child(
2961 Flex::column()
2962 .with_child(self.render_titlebar(&theme, cx))
2963 .with_child(
2964 Stack::new()
2965 .with_child({
2966 let project = self.project.clone();
2967 Flex::row()
2968 .with_children(self.render_dock(DockPosition::Left, cx))
2969 .with_child(
2970 Flex::column()
2971 .with_child(
2972 FlexItem::new(self.center.render(
2973 &project,
2974 &theme,
2975 &self.follower_states_by_leader,
2976 self.active_call(),
2977 self.active_pane(),
2978 self.zoomed(cx).as_ref(),
2979 &self.app_state,
2980 cx,
2981 ))
2982 .flex(1., true),
2983 )
2984 .with_children(
2985 self.render_dock(DockPosition::Bottom, cx),
2986 )
2987 .flex(1., true),
2988 )
2989 .with_children(self.render_dock(DockPosition::Right, cx))
2990 })
2991 .with_child(Overlay::new(
2992 Stack::new()
2993 .with_children(self.zoomed(cx).map(|zoomed| {
2994 enum ZoomBackground {}
2995
2996 ChildView::new(&zoomed, cx)
2997 .contained()
2998 .with_style(theme.workspace.zoomed_foreground)
2999 .aligned()
3000 .contained()
3001 .with_style(theme.workspace.zoomed_background)
3002 .mouse::<ZoomBackground>(0)
3003 .capture_all()
3004 .on_down(MouseButton::Left, |_, this: &mut Self, cx| {
3005 this.zoom_out(cx);
3006 })
3007 }))
3008 .with_children(self.modal.as_ref().map(|modal| {
3009 ChildView::new(modal, cx)
3010 .contained()
3011 .with_style(theme.workspace.modal)
3012 .aligned()
3013 .top()
3014 }))
3015 .with_children(self.render_notifications(&theme.workspace, cx)),
3016 ))
3017 .flex(1.0, true),
3018 )
3019 .with_child(ChildView::new(&self.status_bar, cx))
3020 .contained()
3021 .with_background_color(theme.workspace.background),
3022 )
3023 .with_children(DragAndDrop::render(cx))
3024 .with_children(self.render_disconnected_overlay(cx))
3025 .into_any_named("workspace")
3026 }
3027
3028 fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
3029 if cx.is_self_focused() {
3030 cx.focus(&self.active_pane);
3031 }
3032 cx.notify();
3033 }
3034}
3035
3036impl ViewId {
3037 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
3038 Ok(Self {
3039 creator: message
3040 .creator
3041 .ok_or_else(|| anyhow!("creator is missing"))?,
3042 id: message.id,
3043 })
3044 }
3045
3046 pub(crate) fn to_proto(&self) -> proto::ViewId {
3047 proto::ViewId {
3048 creator: Some(self.creator),
3049 id: self.id,
3050 }
3051 }
3052}
3053
3054pub trait WorkspaceHandle {
3055 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
3056}
3057
3058impl WorkspaceHandle for ViewHandle<Workspace> {
3059 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
3060 self.read(cx)
3061 .worktrees(cx)
3062 .flat_map(|worktree| {
3063 let worktree_id = worktree.read(cx).id();
3064 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
3065 worktree_id,
3066 path: f.path.clone(),
3067 })
3068 })
3069 .collect::<Vec<_>>()
3070 }
3071}
3072
3073impl std::fmt::Debug for OpenPaths {
3074 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3075 f.debug_struct("OpenPaths")
3076 .field("paths", &self.paths)
3077 .finish()
3078 }
3079}
3080
3081pub struct WorkspaceCreated(WeakViewHandle<Workspace>);
3082
3083pub fn activate_workspace_for_project(
3084 cx: &mut AsyncAppContext,
3085 predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
3086) -> Option<WeakViewHandle<Workspace>> {
3087 for window_id in cx.window_ids() {
3088 let handle = cx
3089 .update_window(window_id, |cx| {
3090 if let Some(workspace_handle) = cx.root_view().clone().downcast::<Workspace>() {
3091 let project = workspace_handle.read(cx).project.clone();
3092 if project.update(cx, &predicate) {
3093 cx.activate_window();
3094 return Some(workspace_handle.clone());
3095 }
3096 }
3097 None
3098 })
3099 .flatten();
3100
3101 if let Some(handle) = handle {
3102 return Some(handle.downgrade());
3103 }
3104 }
3105 None
3106}
3107
3108pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
3109 DB.last_workspace().await.log_err().flatten()
3110}
3111
3112#[allow(clippy::type_complexity)]
3113pub fn open_paths(
3114 abs_paths: &[PathBuf],
3115 app_state: &Arc<AppState>,
3116 requesting_window_id: Option<usize>,
3117 cx: &mut AppContext,
3118) -> Task<
3119 Result<(
3120 WeakViewHandle<Workspace>,
3121 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
3122 )>,
3123> {
3124 let app_state = app_state.clone();
3125 let abs_paths = abs_paths.to_vec();
3126 cx.spawn(|mut cx| async move {
3127 // Open paths in existing workspace if possible
3128 let existing = activate_workspace_for_project(&mut cx, |project, cx| {
3129 project.contains_paths(&abs_paths, cx)
3130 });
3131
3132 if let Some(existing) = existing {
3133 Ok((
3134 existing.clone(),
3135 existing
3136 .update(&mut cx, |workspace, cx| {
3137 workspace.open_paths(abs_paths, true, cx)
3138 })?
3139 .await,
3140 ))
3141 } else {
3142 Ok(cx
3143 .update(|cx| {
3144 Workspace::new_local(abs_paths, app_state.clone(), requesting_window_id, cx)
3145 })
3146 .await)
3147 }
3148 })
3149}
3150
3151pub fn open_new(
3152 app_state: &Arc<AppState>,
3153 cx: &mut AppContext,
3154 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static,
3155) -> Task<()> {
3156 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
3157 cx.spawn(|mut cx| async move {
3158 let (workspace, opened_paths) = task.await;
3159
3160 workspace
3161 .update(&mut cx, |workspace, cx| {
3162 if opened_paths.is_empty() {
3163 init(workspace, cx)
3164 }
3165 })
3166 .log_err();
3167 })
3168}
3169
3170pub fn create_and_open_local_file(
3171 path: &'static Path,
3172 cx: &mut ViewContext<Workspace>,
3173 default_content: impl 'static + Send + FnOnce() -> Rope,
3174) -> Task<Result<Box<dyn ItemHandle>>> {
3175 cx.spawn(|workspace, mut cx| async move {
3176 let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?;
3177 if !fs.is_file(path).await {
3178 fs.create_file(path, Default::default()).await?;
3179 fs.save(path, &default_content(), Default::default())
3180 .await?;
3181 }
3182
3183 let mut items = workspace
3184 .update(&mut cx, |workspace, cx| {
3185 workspace.with_local_workspace(cx, |workspace, cx| {
3186 workspace.open_paths(vec![path.to_path_buf()], false, cx)
3187 })
3188 })?
3189 .await?
3190 .await;
3191
3192 let item = items.pop().flatten();
3193 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
3194 })
3195}
3196
3197pub fn join_remote_project(
3198 project_id: u64,
3199 follow_user_id: u64,
3200 app_state: Arc<AppState>,
3201 cx: &mut AppContext,
3202) -> Task<Result<()>> {
3203 cx.spawn(|mut cx| async move {
3204 let existing_workspace = cx
3205 .window_ids()
3206 .into_iter()
3207 .filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
3208 .find(|workspace| {
3209 cx.read_window(workspace.window_id(), |cx| {
3210 workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
3211 })
3212 .unwrap_or(false)
3213 });
3214
3215 let workspace = if let Some(existing_workspace) = existing_workspace {
3216 existing_workspace.downgrade()
3217 } else {
3218 let active_call = cx.read(ActiveCall::global);
3219 let room = active_call
3220 .read_with(&cx, |call, _| call.room().cloned())
3221 .ok_or_else(|| anyhow!("not in a call"))?;
3222 let project = room
3223 .update(&mut cx, |room, cx| {
3224 room.join_project(
3225 project_id,
3226 app_state.languages.clone(),
3227 app_state.fs.clone(),
3228 cx,
3229 )
3230 })
3231 .await?;
3232
3233 let (_, workspace) = cx.add_window(
3234 (app_state.build_window_options)(None, None, cx.platform().as_ref()),
3235 |cx| Workspace::new(0, project, app_state.clone(), cx),
3236 );
3237 (app_state.initialize_workspace)(
3238 workspace.downgrade(),
3239 false,
3240 app_state.clone(),
3241 cx.clone(),
3242 )
3243 .await
3244 .log_err();
3245
3246 workspace.downgrade()
3247 };
3248
3249 cx.activate_window(workspace.window_id());
3250 cx.platform().activate(true);
3251
3252 workspace.update(&mut cx, |workspace, cx| {
3253 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
3254 let follow_peer_id = room
3255 .read(cx)
3256 .remote_participants()
3257 .iter()
3258 .find(|(_, participant)| participant.user.id == follow_user_id)
3259 .map(|(_, p)| p.peer_id)
3260 .or_else(|| {
3261 // If we couldn't follow the given user, follow the host instead.
3262 let collaborator = workspace
3263 .project()
3264 .read(cx)
3265 .collaborators()
3266 .values()
3267 .find(|collaborator| collaborator.replica_id == 0)?;
3268 Some(collaborator.peer_id)
3269 });
3270
3271 if let Some(follow_peer_id) = follow_peer_id {
3272 if !workspace.is_being_followed(follow_peer_id) {
3273 workspace
3274 .toggle_follow(follow_peer_id, cx)
3275 .map(|follow| follow.detach_and_log_err(cx));
3276 }
3277 }
3278 }
3279 })?;
3280
3281 anyhow::Ok(())
3282 })
3283}
3284
3285pub fn restart(_: &Restart, cx: &mut AppContext) {
3286 let should_confirm = settings::get::<WorkspaceSettings>(cx).confirm_quit;
3287 cx.spawn(|mut cx| async move {
3288 let mut workspaces = cx
3289 .window_ids()
3290 .into_iter()
3291 .filter_map(|window_id| {
3292 Some(
3293 cx.root_view(window_id)?
3294 .clone()
3295 .downcast::<Workspace>()?
3296 .downgrade(),
3297 )
3298 })
3299 .collect::<Vec<_>>();
3300
3301 // If multiple windows have unsaved changes, and need a save prompt,
3302 // prompt in the active window before switching to a different window.
3303 workspaces.sort_by_key(|workspace| !cx.window_is_active(workspace.window_id()));
3304
3305 if let (true, Some(workspace)) = (should_confirm, workspaces.first()) {
3306 let answer = cx.prompt(
3307 workspace.window_id(),
3308 PromptLevel::Info,
3309 "Are you sure you want to restart?",
3310 &["Restart", "Cancel"],
3311 );
3312
3313 if let Some(mut answer) = answer {
3314 let answer = answer.next().await;
3315 if answer != Some(0) {
3316 return Ok(());
3317 }
3318 }
3319 }
3320
3321 // If the user cancels any save prompt, then keep the app open.
3322 for workspace in workspaces {
3323 if !workspace
3324 .update(&mut cx, |workspace, cx| {
3325 workspace.prepare_to_close(true, cx)
3326 })?
3327 .await?
3328 {
3329 return Ok(());
3330 }
3331 }
3332 cx.platform().restart();
3333 anyhow::Ok(())
3334 })
3335 .detach_and_log_err(cx);
3336}
3337
3338fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
3339 let mut parts = value.split(',');
3340 let width: usize = parts.next()?.parse().ok()?;
3341 let height: usize = parts.next()?.parse().ok()?;
3342 Some(vec2f(width as f32, height as f32))
3343}
3344
3345#[cfg(test)]
3346mod tests {
3347 use super::*;
3348 use crate::{
3349 dock::test::{TestPanel, TestPanelEvent},
3350 item::test::{TestItem, TestItemEvent, TestProjectItem},
3351 };
3352 use fs::FakeFs;
3353 use gpui::{executor::Deterministic, TestAppContext};
3354 use project::{Project, ProjectEntryId};
3355 use serde_json::json;
3356 use settings::SettingsStore;
3357 use std::{cell::RefCell, rc::Rc};
3358
3359 #[gpui::test]
3360 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
3361 init_test(cx);
3362
3363 let fs = FakeFs::new(cx.background());
3364 let project = Project::test(fs, [], cx).await;
3365 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3366
3367 // Adding an item with no ambiguity renders the tab without detail.
3368 let item1 = cx.add_view(window_id, |_| {
3369 let mut item = TestItem::new();
3370 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
3371 item
3372 });
3373 workspace.update(cx, |workspace, cx| {
3374 workspace.add_item(Box::new(item1.clone()), cx);
3375 });
3376 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None));
3377
3378 // Adding an item that creates ambiguity increases the level of detail on
3379 // both tabs.
3380 let item2 = cx.add_view(window_id, |_| {
3381 let mut item = TestItem::new();
3382 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
3383 item
3384 });
3385 workspace.update(cx, |workspace, cx| {
3386 workspace.add_item(Box::new(item2.clone()), cx);
3387 });
3388 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3389 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3390
3391 // Adding an item that creates ambiguity increases the level of detail only
3392 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
3393 // we stop at the highest detail available.
3394 let item3 = cx.add_view(window_id, |_| {
3395 let mut item = TestItem::new();
3396 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
3397 item
3398 });
3399 workspace.update(cx, |workspace, cx| {
3400 workspace.add_item(Box::new(item3.clone()), cx);
3401 });
3402 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3403 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
3404 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
3405 }
3406
3407 #[gpui::test]
3408 async fn test_tracking_active_path(cx: &mut TestAppContext) {
3409 init_test(cx);
3410
3411 let fs = FakeFs::new(cx.background());
3412 fs.insert_tree(
3413 "/root1",
3414 json!({
3415 "one.txt": "",
3416 "two.txt": "",
3417 }),
3418 )
3419 .await;
3420 fs.insert_tree(
3421 "/root2",
3422 json!({
3423 "three.txt": "",
3424 }),
3425 )
3426 .await;
3427
3428 let project = Project::test(fs, ["root1".as_ref()], cx).await;
3429 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3430 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3431 let worktree_id = project.read_with(cx, |project, cx| {
3432 project.worktrees(cx).next().unwrap().read(cx).id()
3433 });
3434
3435 let item1 = cx.add_view(window_id, |cx| {
3436 TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3437 });
3438 let item2 = cx.add_view(window_id, |cx| {
3439 TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
3440 });
3441
3442 // Add an item to an empty pane
3443 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
3444 project.read_with(cx, |project, cx| {
3445 assert_eq!(
3446 project.active_entry(),
3447 project
3448 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
3449 .map(|e| e.id)
3450 );
3451 });
3452 assert_eq!(
3453 cx.current_window_title(window_id).as_deref(),
3454 Some("one.txt β root1")
3455 );
3456
3457 // Add a second item to a non-empty pane
3458 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
3459 assert_eq!(
3460 cx.current_window_title(window_id).as_deref(),
3461 Some("two.txt β root1")
3462 );
3463 project.read_with(cx, |project, cx| {
3464 assert_eq!(
3465 project.active_entry(),
3466 project
3467 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
3468 .map(|e| e.id)
3469 );
3470 });
3471
3472 // Close the active item
3473 pane.update(cx, |pane, cx| {
3474 pane.close_active_item(&Default::default(), cx).unwrap()
3475 })
3476 .await
3477 .unwrap();
3478 assert_eq!(
3479 cx.current_window_title(window_id).as_deref(),
3480 Some("one.txt β root1")
3481 );
3482 project.read_with(cx, |project, cx| {
3483 assert_eq!(
3484 project.active_entry(),
3485 project
3486 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
3487 .map(|e| e.id)
3488 );
3489 });
3490
3491 // Add a project folder
3492 project
3493 .update(cx, |project, cx| {
3494 project.find_or_create_local_worktree("/root2", true, cx)
3495 })
3496 .await
3497 .unwrap();
3498 assert_eq!(
3499 cx.current_window_title(window_id).as_deref(),
3500 Some("one.txt β root1, root2")
3501 );
3502
3503 // Remove a project folder
3504 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
3505 assert_eq!(
3506 cx.current_window_title(window_id).as_deref(),
3507 Some("one.txt β root2")
3508 );
3509 }
3510
3511 #[gpui::test]
3512 async fn test_close_window(cx: &mut TestAppContext) {
3513 init_test(cx);
3514
3515 let fs = FakeFs::new(cx.background());
3516 fs.insert_tree("/root", json!({ "one": "" })).await;
3517
3518 let project = Project::test(fs, ["root".as_ref()], cx).await;
3519 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3520
3521 // When there are no dirty items, there's nothing to do.
3522 let item1 = cx.add_view(window_id, |_| TestItem::new());
3523 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
3524 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
3525 assert!(task.await.unwrap());
3526
3527 // When there are dirty untitled items, prompt to save each one. If the user
3528 // cancels any prompt, then abort.
3529 let item2 = cx.add_view(window_id, |_| TestItem::new().with_dirty(true));
3530 let item3 = cx.add_view(window_id, |cx| {
3531 TestItem::new()
3532 .with_dirty(true)
3533 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3534 });
3535 workspace.update(cx, |w, cx| {
3536 w.add_item(Box::new(item2.clone()), cx);
3537 w.add_item(Box::new(item3.clone()), cx);
3538 });
3539 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
3540 cx.foreground().run_until_parked();
3541 cx.simulate_prompt_answer(window_id, 2 /* cancel */);
3542 cx.foreground().run_until_parked();
3543 assert!(!cx.has_pending_prompt(window_id));
3544 assert!(!task.await.unwrap());
3545 }
3546
3547 #[gpui::test]
3548 async fn test_close_pane_items(cx: &mut TestAppContext) {
3549 init_test(cx);
3550
3551 let fs = FakeFs::new(cx.background());
3552
3553 let project = Project::test(fs, None, cx).await;
3554 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3555
3556 let item1 = cx.add_view(window_id, |cx| {
3557 TestItem::new()
3558 .with_dirty(true)
3559 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3560 });
3561 let item2 = cx.add_view(window_id, |cx| {
3562 TestItem::new()
3563 .with_dirty(true)
3564 .with_conflict(true)
3565 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
3566 });
3567 let item3 = cx.add_view(window_id, |cx| {
3568 TestItem::new()
3569 .with_dirty(true)
3570 .with_conflict(true)
3571 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
3572 });
3573 let item4 = cx.add_view(window_id, |cx| {
3574 TestItem::new()
3575 .with_dirty(true)
3576 .with_project_items(&[TestProjectItem::new_untitled(cx)])
3577 });
3578 let pane = workspace.update(cx, |workspace, cx| {
3579 workspace.add_item(Box::new(item1.clone()), cx);
3580 workspace.add_item(Box::new(item2.clone()), cx);
3581 workspace.add_item(Box::new(item3.clone()), cx);
3582 workspace.add_item(Box::new(item4.clone()), cx);
3583 workspace.active_pane().clone()
3584 });
3585
3586 let close_items = pane.update(cx, |pane, cx| {
3587 pane.activate_item(1, true, true, cx);
3588 assert_eq!(pane.active_item().unwrap().id(), item2.id());
3589 let item1_id = item1.id();
3590 let item3_id = item3.id();
3591 let item4_id = item4.id();
3592 pane.close_items(cx, move |id| [item1_id, item3_id, item4_id].contains(&id))
3593 });
3594 cx.foreground().run_until_parked();
3595
3596 // There's a prompt to save item 1.
3597 pane.read_with(cx, |pane, _| {
3598 assert_eq!(pane.items_len(), 4);
3599 assert_eq!(pane.active_item().unwrap().id(), item1.id());
3600 });
3601 assert!(cx.has_pending_prompt(window_id));
3602
3603 // Confirm saving item 1.
3604 cx.simulate_prompt_answer(window_id, 0);
3605 cx.foreground().run_until_parked();
3606
3607 // Item 1 is saved. There's a prompt to save item 3.
3608 pane.read_with(cx, |pane, cx| {
3609 assert_eq!(item1.read(cx).save_count, 1);
3610 assert_eq!(item1.read(cx).save_as_count, 0);
3611 assert_eq!(item1.read(cx).reload_count, 0);
3612 assert_eq!(pane.items_len(), 3);
3613 assert_eq!(pane.active_item().unwrap().id(), item3.id());
3614 });
3615 assert!(cx.has_pending_prompt(window_id));
3616
3617 // Cancel saving item 3.
3618 cx.simulate_prompt_answer(window_id, 1);
3619 cx.foreground().run_until_parked();
3620
3621 // Item 3 is reloaded. There's a prompt to save item 4.
3622 pane.read_with(cx, |pane, cx| {
3623 assert_eq!(item3.read(cx).save_count, 0);
3624 assert_eq!(item3.read(cx).save_as_count, 0);
3625 assert_eq!(item3.read(cx).reload_count, 1);
3626 assert_eq!(pane.items_len(), 2);
3627 assert_eq!(pane.active_item().unwrap().id(), item4.id());
3628 });
3629 assert!(cx.has_pending_prompt(window_id));
3630
3631 // Confirm saving item 4.
3632 cx.simulate_prompt_answer(window_id, 0);
3633 cx.foreground().run_until_parked();
3634
3635 // There's a prompt for a path for item 4.
3636 cx.simulate_new_path_selection(|_| Some(Default::default()));
3637 close_items.await.unwrap();
3638
3639 // The requested items are closed.
3640 pane.read_with(cx, |pane, cx| {
3641 assert_eq!(item4.read(cx).save_count, 0);
3642 assert_eq!(item4.read(cx).save_as_count, 1);
3643 assert_eq!(item4.read(cx).reload_count, 0);
3644 assert_eq!(pane.items_len(), 1);
3645 assert_eq!(pane.active_item().unwrap().id(), item2.id());
3646 });
3647 }
3648
3649 #[gpui::test]
3650 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
3651 init_test(cx);
3652
3653 let fs = FakeFs::new(cx.background());
3654
3655 let project = Project::test(fs, [], cx).await;
3656 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3657
3658 // Create several workspace items with single project entries, and two
3659 // workspace items with multiple project entries.
3660 let single_entry_items = (0..=4)
3661 .map(|project_entry_id| {
3662 cx.add_view(window_id, |cx| {
3663 TestItem::new()
3664 .with_dirty(true)
3665 .with_project_items(&[TestProjectItem::new(
3666 project_entry_id,
3667 &format!("{project_entry_id}.txt"),
3668 cx,
3669 )])
3670 })
3671 })
3672 .collect::<Vec<_>>();
3673 let item_2_3 = cx.add_view(window_id, |cx| {
3674 TestItem::new()
3675 .with_dirty(true)
3676 .with_singleton(false)
3677 .with_project_items(&[
3678 single_entry_items[2].read(cx).project_items[0].clone(),
3679 single_entry_items[3].read(cx).project_items[0].clone(),
3680 ])
3681 });
3682 let item_3_4 = cx.add_view(window_id, |cx| {
3683 TestItem::new()
3684 .with_dirty(true)
3685 .with_singleton(false)
3686 .with_project_items(&[
3687 single_entry_items[3].read(cx).project_items[0].clone(),
3688 single_entry_items[4].read(cx).project_items[0].clone(),
3689 ])
3690 });
3691
3692 // Create two panes that contain the following project entries:
3693 // left pane:
3694 // multi-entry items: (2, 3)
3695 // single-entry items: 0, 1, 2, 3, 4
3696 // right pane:
3697 // single-entry items: 1
3698 // multi-entry items: (3, 4)
3699 let left_pane = workspace.update(cx, |workspace, cx| {
3700 let left_pane = workspace.active_pane().clone();
3701 workspace.add_item(Box::new(item_2_3.clone()), cx);
3702 for item in single_entry_items {
3703 workspace.add_item(Box::new(item), cx);
3704 }
3705 left_pane.update(cx, |pane, cx| {
3706 pane.activate_item(2, true, true, cx);
3707 });
3708
3709 workspace
3710 .split_pane(left_pane.clone(), SplitDirection::Right, cx)
3711 .unwrap();
3712
3713 left_pane
3714 });
3715
3716 //Need to cause an effect flush in order to respect new focus
3717 workspace.update(cx, |workspace, cx| {
3718 workspace.add_item(Box::new(item_3_4.clone()), cx);
3719 cx.focus(&left_pane);
3720 });
3721
3722 // When closing all of the items in the left pane, we should be prompted twice:
3723 // once for project entry 0, and once for project entry 2. After those two
3724 // prompts, the task should complete.
3725
3726 let close = left_pane.update(cx, |pane, cx| pane.close_items(cx, |_| true));
3727 cx.foreground().run_until_parked();
3728 left_pane.read_with(cx, |pane, cx| {
3729 assert_eq!(
3730 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
3731 &[ProjectEntryId::from_proto(0)]
3732 );
3733 });
3734 cx.simulate_prompt_answer(window_id, 0);
3735
3736 cx.foreground().run_until_parked();
3737 left_pane.read_with(cx, |pane, cx| {
3738 assert_eq!(
3739 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
3740 &[ProjectEntryId::from_proto(2)]
3741 );
3742 });
3743 cx.simulate_prompt_answer(window_id, 0);
3744
3745 cx.foreground().run_until_parked();
3746 close.await.unwrap();
3747 left_pane.read_with(cx, |pane, _| {
3748 assert_eq!(pane.items_len(), 0);
3749 });
3750 }
3751
3752 #[gpui::test]
3753 async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
3754 init_test(cx);
3755
3756 let fs = FakeFs::new(cx.background());
3757
3758 let project = Project::test(fs, [], cx).await;
3759 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3760 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3761
3762 let item = cx.add_view(window_id, |cx| {
3763 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3764 });
3765 let item_id = item.id();
3766 workspace.update(cx, |workspace, cx| {
3767 workspace.add_item(Box::new(item.clone()), cx);
3768 });
3769
3770 // Autosave on window change.
3771 item.update(cx, |item, cx| {
3772 cx.update_global(|settings: &mut SettingsStore, cx| {
3773 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
3774 settings.autosave = Some(AutosaveSetting::OnWindowChange);
3775 })
3776 });
3777 item.is_dirty = true;
3778 });
3779
3780 // Deactivating the window saves the file.
3781 cx.simulate_window_activation(None);
3782 deterministic.run_until_parked();
3783 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
3784
3785 // Autosave on focus change.
3786 item.update(cx, |item, cx| {
3787 cx.focus_self();
3788 cx.update_global(|settings: &mut SettingsStore, cx| {
3789 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
3790 settings.autosave = Some(AutosaveSetting::OnFocusChange);
3791 })
3792 });
3793 item.is_dirty = true;
3794 });
3795
3796 // Blurring the item saves the file.
3797 item.update(cx, |_, cx| cx.blur());
3798 deterministic.run_until_parked();
3799 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
3800
3801 // Deactivating the window still saves the file.
3802 cx.simulate_window_activation(Some(window_id));
3803 item.update(cx, |item, cx| {
3804 cx.focus_self();
3805 item.is_dirty = true;
3806 });
3807 cx.simulate_window_activation(None);
3808
3809 deterministic.run_until_parked();
3810 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
3811
3812 // Autosave after delay.
3813 item.update(cx, |item, cx| {
3814 cx.update_global(|settings: &mut SettingsStore, cx| {
3815 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
3816 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
3817 })
3818 });
3819 item.is_dirty = true;
3820 cx.emit(TestItemEvent::Edit);
3821 });
3822
3823 // Delay hasn't fully expired, so the file is still dirty and unsaved.
3824 deterministic.advance_clock(Duration::from_millis(250));
3825 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
3826
3827 // After delay expires, the file is saved.
3828 deterministic.advance_clock(Duration::from_millis(250));
3829 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
3830
3831 // Autosave on focus change, ensuring closing the tab counts as such.
3832 item.update(cx, |item, cx| {
3833 cx.update_global(|settings: &mut SettingsStore, cx| {
3834 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
3835 settings.autosave = Some(AutosaveSetting::OnFocusChange);
3836 })
3837 });
3838 item.is_dirty = true;
3839 });
3840
3841 pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id))
3842 .await
3843 .unwrap();
3844 assert!(!cx.has_pending_prompt(window_id));
3845 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3846
3847 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
3848 workspace.update(cx, |workspace, cx| {
3849 workspace.add_item(Box::new(item.clone()), cx);
3850 });
3851 item.update(cx, |item, cx| {
3852 item.project_items[0].update(cx, |item, _| {
3853 item.entry_id = None;
3854 });
3855 item.is_dirty = true;
3856 cx.blur();
3857 });
3858 deterministic.run_until_parked();
3859 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3860
3861 // Ensure autosave is prevented for deleted files also when closing the buffer.
3862 let _close_items =
3863 pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id));
3864 deterministic.run_until_parked();
3865 assert!(cx.has_pending_prompt(window_id));
3866 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3867 }
3868
3869 #[gpui::test]
3870 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
3871 init_test(cx);
3872
3873 let fs = FakeFs::new(cx.background());
3874
3875 let project = Project::test(fs, [], cx).await;
3876 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3877
3878 let item = cx.add_view(window_id, |cx| {
3879 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3880 });
3881 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3882 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
3883 let toolbar_notify_count = Rc::new(RefCell::new(0));
3884
3885 workspace.update(cx, |workspace, cx| {
3886 workspace.add_item(Box::new(item.clone()), cx);
3887 let toolbar_notification_count = toolbar_notify_count.clone();
3888 cx.observe(&toolbar, move |_, _, _| {
3889 *toolbar_notification_count.borrow_mut() += 1
3890 })
3891 .detach();
3892 });
3893
3894 pane.read_with(cx, |pane, _| {
3895 assert!(!pane.can_navigate_backward());
3896 assert!(!pane.can_navigate_forward());
3897 });
3898
3899 item.update(cx, |item, cx| {
3900 item.set_state("one".to_string(), cx);
3901 });
3902
3903 // Toolbar must be notified to re-render the navigation buttons
3904 assert_eq!(*toolbar_notify_count.borrow(), 1);
3905
3906 pane.read_with(cx, |pane, _| {
3907 assert!(pane.can_navigate_backward());
3908 assert!(!pane.can_navigate_forward());
3909 });
3910
3911 workspace
3912 .update(cx, |workspace, cx| {
3913 Pane::go_back(workspace, Some(pane.downgrade()), cx)
3914 })
3915 .await
3916 .unwrap();
3917
3918 assert_eq!(*toolbar_notify_count.borrow(), 3);
3919 pane.read_with(cx, |pane, _| {
3920 assert!(!pane.can_navigate_backward());
3921 assert!(pane.can_navigate_forward());
3922 });
3923 }
3924
3925 #[gpui::test]
3926 async fn test_panels(cx: &mut gpui::TestAppContext) {
3927 init_test(cx);
3928 let fs = FakeFs::new(cx.background());
3929
3930 let project = Project::test(fs, [], cx).await;
3931 let (_window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3932
3933 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
3934 // Add panel_1 on the left, panel_2 on the right.
3935 let panel_1 = cx.add_view(|_| TestPanel {
3936 position: DockPosition::Left,
3937 });
3938 workspace.add_panel(panel_1.clone(), cx);
3939 workspace
3940 .left_dock()
3941 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
3942 let panel_2 = cx.add_view(|_| TestPanel {
3943 position: DockPosition::Right,
3944 });
3945 workspace.add_panel(panel_2.clone(), cx);
3946 workspace
3947 .right_dock()
3948 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
3949
3950 let left_dock = workspace.left_dock();
3951 assert_eq!(
3952 left_dock.read(cx).active_panel().unwrap().id(),
3953 panel_1.id()
3954 );
3955 assert_eq!(
3956 left_dock.read(cx).active_panel_size(cx).unwrap(),
3957 panel_1.size(cx)
3958 );
3959
3960 left_dock.update(cx, |left_dock, cx| left_dock.resize_active_panel(1337., cx));
3961 assert_eq!(
3962 workspace.right_dock().read(cx).active_panel().unwrap().id(),
3963 panel_2.id()
3964 );
3965
3966 (panel_1, panel_2)
3967 });
3968
3969 // Move panel_1 to the right
3970 panel_1.update(cx, |panel_1, cx| {
3971 panel_1.set_position(DockPosition::Right, cx)
3972 });
3973
3974 workspace.update(cx, |workspace, cx| {
3975 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
3976 // Since it was the only panel on the left, the left dock should now be closed.
3977 assert!(!workspace.left_dock().read(cx).is_open());
3978 assert!(workspace.left_dock().read(cx).active_panel().is_none());
3979 let right_dock = workspace.right_dock();
3980 assert_eq!(
3981 right_dock.read(cx).active_panel().unwrap().id(),
3982 panel_1.id()
3983 );
3984 assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
3985
3986 // Now we move panel_2Β to the left
3987 panel_2.set_position(DockPosition::Left, cx);
3988 });
3989
3990 workspace.update(cx, |workspace, cx| {
3991 // Since panel_2 was not visible on the right, we don't open the left dock.
3992 assert!(!workspace.left_dock().read(cx).is_open());
3993 // And the right dock is unaffected in it's displaying of panel_1
3994 assert!(workspace.right_dock().read(cx).is_open());
3995 assert_eq!(
3996 workspace.right_dock().read(cx).active_panel().unwrap().id(),
3997 panel_1.id()
3998 );
3999 });
4000
4001 // Move panel_1 back to the left
4002 panel_1.update(cx, |panel_1, cx| {
4003 panel_1.set_position(DockPosition::Left, cx)
4004 });
4005
4006 workspace.update(cx, |workspace, cx| {
4007 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
4008 let left_dock = workspace.left_dock();
4009 assert!(left_dock.read(cx).is_open());
4010 assert_eq!(
4011 left_dock.read(cx).active_panel().unwrap().id(),
4012 panel_1.id()
4013 );
4014 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
4015 // And right the dock should be closed as it no longer has any panels.
4016 assert!(!workspace.right_dock().read(cx).is_open());
4017
4018 // Now we move panel_1 to the bottom
4019 panel_1.set_position(DockPosition::Bottom, cx);
4020 });
4021
4022 workspace.update(cx, |workspace, cx| {
4023 // Since panel_1 was visible on the left, we close the left dock.
4024 assert!(!workspace.left_dock().read(cx).is_open());
4025 // The bottom dock is sized based on the panel's default size,
4026 // since the panel orientation changed from vertical to horizontal.
4027 let bottom_dock = workspace.bottom_dock();
4028 assert_eq!(
4029 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
4030 panel_1.size(cx),
4031 );
4032 // Close bottom dock and move panel_1 back to the left.
4033 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
4034 panel_1.set_position(DockPosition::Left, cx);
4035 });
4036
4037 // Emit activated event on panel 1
4038 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Activated));
4039
4040 // Now the left dock is open and panel_1 is active and focused.
4041 workspace.read_with(cx, |workspace, cx| {
4042 let left_dock = workspace.left_dock();
4043 assert!(left_dock.read(cx).is_open());
4044 assert_eq!(
4045 left_dock.read(cx).active_panel().unwrap().id(),
4046 panel_1.id()
4047 );
4048 assert!(panel_1.is_focused(cx));
4049 });
4050
4051 // Emit closed event on panel 2, which is not active
4052 panel_2.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
4053
4054 // Wo don't close the left dock, because panel_2 wasn't the active panel
4055 workspace.read_with(cx, |workspace, cx| {
4056 let left_dock = workspace.left_dock();
4057 assert!(left_dock.read(cx).is_open());
4058 assert_eq!(
4059 left_dock.read(cx).active_panel().unwrap().id(),
4060 panel_1.id()
4061 );
4062 });
4063
4064 // Emit closed event on panel 1, which is active
4065 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
4066
4067 // Now the left dock is closed, because panel_1 was the active panel
4068 workspace.read_with(cx, |workspace, cx| {
4069 let left_dock = workspace.left_dock();
4070 assert!(!left_dock.read(cx).is_open());
4071 });
4072 }
4073
4074 pub fn init_test(cx: &mut TestAppContext) {
4075 cx.foreground().forbid_parking();
4076 cx.update(|cx| {
4077 cx.set_global(SettingsStore::test(cx));
4078 theme::init((), cx);
4079 language::init(cx);
4080 crate::init_settings(cx);
4081 });
4082 }
4083}