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,
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 self.subscriptions.push(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
889 dock.update(cx, |dock, cx| dock.add_panel(panel, cx));
890 }
891
892 pub fn status_bar(&self) -> &ViewHandle<StatusBar> {
893 &self.status_bar
894 }
895
896 pub fn app_state(&self) -> &Arc<AppState> {
897 &self.app_state
898 }
899
900 pub fn user_store(&self) -> &ModelHandle<UserStore> {
901 &self.app_state.user_store
902 }
903
904 pub fn project(&self) -> &ModelHandle<Project> {
905 &self.project
906 }
907
908 pub fn recent_navigation_history(
909 &self,
910 limit: Option<usize>,
911 cx: &AppContext,
912 ) -> Vec<ProjectPath> {
913 let mut history: HashMap<ProjectPath, usize> = HashMap::default();
914 for pane in &self.panes {
915 let pane = pane.read(cx);
916 pane.nav_history()
917 .for_each_entry(cx, |entry, project_path| {
918 let timestamp = entry.timestamp;
919 match history.entry(project_path) {
920 hash_map::Entry::Occupied(mut entry) => {
921 if ×tamp > entry.get() {
922 entry.insert(timestamp);
923 }
924 }
925 hash_map::Entry::Vacant(entry) => {
926 entry.insert(timestamp);
927 }
928 }
929 });
930 }
931
932 history
933 .into_iter()
934 .sorted_by_key(|(_, timestamp)| *timestamp)
935 .map(|(project_path, _)| project_path)
936 .rev()
937 .take(limit.unwrap_or(usize::MAX))
938 .collect()
939 }
940
941 pub fn client(&self) -> &Client {
942 &self.app_state.client
943 }
944
945 pub fn set_titlebar_item(&mut self, item: AnyViewHandle, cx: &mut ViewContext<Self>) {
946 self.titlebar_item = Some(item);
947 cx.notify();
948 }
949
950 pub fn titlebar_item(&self) -> Option<AnyViewHandle> {
951 self.titlebar_item.clone()
952 }
953
954 /// Call the given callback with a workspace whose project is local.
955 ///
956 /// If the given workspace has a local project, then it will be passed
957 /// to the callback. Otherwise, a new empty window will be created.
958 pub fn with_local_workspace<T, F>(
959 &mut self,
960 cx: &mut ViewContext<Self>,
961 callback: F,
962 ) -> Task<Result<T>>
963 where
964 T: 'static,
965 F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
966 {
967 if self.project.read(cx).is_local() {
968 Task::Ready(Some(Ok(callback(self, cx))))
969 } else {
970 let task = Self::new_local(Vec::new(), self.app_state.clone(), None, cx);
971 cx.spawn(|_vh, mut cx| async move {
972 let (workspace, _) = task.await;
973 workspace.update(&mut cx, callback)
974 })
975 }
976 }
977
978 pub fn worktrees<'a>(
979 &self,
980 cx: &'a AppContext,
981 ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
982 self.project.read(cx).worktrees(cx)
983 }
984
985 pub fn visible_worktrees<'a>(
986 &self,
987 cx: &'a AppContext,
988 ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
989 self.project.read(cx).visible_worktrees(cx)
990 }
991
992 pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future<Output = ()> + 'static {
993 let futures = self
994 .worktrees(cx)
995 .filter_map(|worktree| worktree.read(cx).as_local())
996 .map(|worktree| worktree.scan_complete())
997 .collect::<Vec<_>>();
998 async move {
999 for future in futures {
1000 future.await;
1001 }
1002 }
1003 }
1004
1005 pub fn close_global(_: &CloseWindow, cx: &mut AppContext) {
1006 cx.spawn(|mut cx| async move {
1007 let id = cx
1008 .window_ids()
1009 .into_iter()
1010 .find(|&id| cx.window_is_active(id));
1011 if let Some(id) = id {
1012 //This can only get called when the window's project connection has been lost
1013 //so we don't need to prompt the user for anything and instead just close the window
1014 cx.remove_window(id);
1015 }
1016 })
1017 .detach();
1018 }
1019
1020 pub fn close(
1021 &mut self,
1022 _: &CloseWindow,
1023 cx: &mut ViewContext<Self>,
1024 ) -> Option<Task<Result<()>>> {
1025 let window_id = cx.window_id();
1026 let prepare = self.prepare_to_close(false, cx);
1027 Some(cx.spawn(|_, mut cx| async move {
1028 if prepare.await? {
1029 cx.remove_window(window_id);
1030 }
1031 Ok(())
1032 }))
1033 }
1034
1035 pub fn prepare_to_close(
1036 &mut self,
1037 quitting: bool,
1038 cx: &mut ViewContext<Self>,
1039 ) -> Task<Result<bool>> {
1040 let active_call = self.active_call().cloned();
1041 let window_id = cx.window_id();
1042
1043 cx.spawn(|this, mut cx| async move {
1044 let workspace_count = cx
1045 .window_ids()
1046 .into_iter()
1047 .filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
1048 .count();
1049
1050 if let Some(active_call) = active_call {
1051 if !quitting
1052 && workspace_count == 1
1053 && active_call.read_with(&cx, |call, _| call.room().is_some())
1054 {
1055 let answer = cx.prompt(
1056 window_id,
1057 PromptLevel::Warning,
1058 "Do you want to leave the current call?",
1059 &["Close window and hang up", "Cancel"],
1060 );
1061
1062 if let Some(mut answer) = answer {
1063 if answer.next().await == Some(1) {
1064 return anyhow::Ok(false);
1065 } else {
1066 active_call
1067 .update(&mut cx, |call, cx| call.hang_up(cx))
1068 .await
1069 .log_err();
1070 }
1071 }
1072 }
1073 }
1074
1075 Ok(this
1076 .update(&mut cx, |this, cx| this.save_all_internal(true, cx))?
1077 .await?)
1078 })
1079 }
1080
1081 fn save_all(&mut self, _: &SaveAll, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
1082 let save_all = self.save_all_internal(false, cx);
1083 Some(cx.foreground().spawn(async move {
1084 save_all.await?;
1085 Ok(())
1086 }))
1087 }
1088
1089 fn save_all_internal(
1090 &mut self,
1091 should_prompt_to_save: bool,
1092 cx: &mut ViewContext<Self>,
1093 ) -> Task<Result<bool>> {
1094 if self.project.read(cx).is_read_only() {
1095 return Task::ready(Ok(true));
1096 }
1097
1098 let dirty_items = self
1099 .panes
1100 .iter()
1101 .flat_map(|pane| {
1102 pane.read(cx).items().filter_map(|item| {
1103 if item.is_dirty(cx) {
1104 Some((pane.downgrade(), item.boxed_clone()))
1105 } else {
1106 None
1107 }
1108 })
1109 })
1110 .collect::<Vec<_>>();
1111
1112 let project = self.project.clone();
1113 cx.spawn(|_, mut cx| async move {
1114 for (pane, item) in dirty_items {
1115 let (singleton, project_entry_ids) =
1116 cx.read(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)));
1117 if singleton || !project_entry_ids.is_empty() {
1118 if let Some(ix) =
1119 pane.read_with(&cx, |pane, _| pane.index_for_item(item.as_ref()))?
1120 {
1121 if !Pane::save_item(
1122 project.clone(),
1123 &pane,
1124 ix,
1125 &*item,
1126 should_prompt_to_save,
1127 &mut cx,
1128 )
1129 .await?
1130 {
1131 return Ok(false);
1132 }
1133 }
1134 }
1135 }
1136 Ok(true)
1137 })
1138 }
1139
1140 pub fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
1141 let mut paths = cx.prompt_for_paths(PathPromptOptions {
1142 files: true,
1143 directories: true,
1144 multiple: true,
1145 });
1146
1147 Some(cx.spawn(|this, mut cx| async move {
1148 if let Some(paths) = paths.recv().await.flatten() {
1149 if let Some(task) = this
1150 .update(&mut cx, |this, cx| this.open_workspace_for_paths(paths, cx))
1151 .log_err()
1152 {
1153 task.await?
1154 }
1155 }
1156 Ok(())
1157 }))
1158 }
1159
1160 pub fn open_workspace_for_paths(
1161 &mut self,
1162 paths: Vec<PathBuf>,
1163 cx: &mut ViewContext<Self>,
1164 ) -> Task<Result<()>> {
1165 let window_id = cx.window_id();
1166 let is_remote = self.project.read(cx).is_remote();
1167 let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
1168 let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
1169 let close_task = if is_remote || has_worktree || has_dirty_items {
1170 None
1171 } else {
1172 Some(self.prepare_to_close(false, cx))
1173 };
1174 let app_state = self.app_state.clone();
1175
1176 cx.spawn(|_, mut cx| async move {
1177 let window_id_to_replace = if let Some(close_task) = close_task {
1178 if !close_task.await? {
1179 return Ok(());
1180 }
1181 Some(window_id)
1182 } else {
1183 None
1184 };
1185 cx.update(|cx| open_paths(&paths, &app_state, window_id_to_replace, cx))
1186 .await?;
1187 Ok(())
1188 })
1189 }
1190
1191 #[allow(clippy::type_complexity)]
1192 pub fn open_paths(
1193 &mut self,
1194 mut abs_paths: Vec<PathBuf>,
1195 visible: bool,
1196 cx: &mut ViewContext<Self>,
1197 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
1198 log::info!("open paths {:?}", abs_paths);
1199
1200 let fs = self.app_state.fs.clone();
1201
1202 // Sort the paths to ensure we add worktrees for parents before their children.
1203 abs_paths.sort_unstable();
1204 cx.spawn(|this, mut cx| async move {
1205 let mut project_paths = Vec::new();
1206 for path in &abs_paths {
1207 if let Some(project_path) = this
1208 .update(&mut cx, |this, cx| {
1209 Workspace::project_path_for_path(this.project.clone(), path, visible, cx)
1210 })
1211 .log_err()
1212 {
1213 project_paths.push(project_path.await.log_err());
1214 } else {
1215 project_paths.push(None);
1216 }
1217 }
1218
1219 let tasks = abs_paths
1220 .iter()
1221 .cloned()
1222 .zip(project_paths.into_iter())
1223 .map(|(abs_path, project_path)| {
1224 let this = this.clone();
1225 cx.spawn(|mut cx| {
1226 let fs = fs.clone();
1227 async move {
1228 let (_worktree, project_path) = project_path?;
1229 if fs.is_file(&abs_path).await {
1230 Some(
1231 this.update(&mut cx, |this, cx| {
1232 this.open_path(project_path, None, true, cx)
1233 })
1234 .log_err()?
1235 .await,
1236 )
1237 } else {
1238 None
1239 }
1240 }
1241 })
1242 })
1243 .collect::<Vec<_>>();
1244
1245 futures::future::join_all(tasks).await
1246 })
1247 }
1248
1249 fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
1250 let mut paths = cx.prompt_for_paths(PathPromptOptions {
1251 files: false,
1252 directories: true,
1253 multiple: true,
1254 });
1255 cx.spawn(|this, mut cx| async move {
1256 if let Some(paths) = paths.recv().await.flatten() {
1257 let results = this
1258 .update(&mut cx, |this, cx| this.open_paths(paths, true, cx))?
1259 .await;
1260 for result in results.into_iter().flatten() {
1261 result.log_err();
1262 }
1263 }
1264 anyhow::Ok(())
1265 })
1266 .detach_and_log_err(cx);
1267 }
1268
1269 fn project_path_for_path(
1270 project: ModelHandle<Project>,
1271 abs_path: &Path,
1272 visible: bool,
1273 cx: &mut AppContext,
1274 ) -> Task<Result<(ModelHandle<Worktree>, ProjectPath)>> {
1275 let entry = project.update(cx, |project, cx| {
1276 project.find_or_create_local_worktree(abs_path, visible, cx)
1277 });
1278 cx.spawn(|cx| async move {
1279 let (worktree, path) = entry.await?;
1280 let worktree_id = worktree.read_with(&cx, |t, _| t.id());
1281 Ok((
1282 worktree,
1283 ProjectPath {
1284 worktree_id,
1285 path: path.into(),
1286 },
1287 ))
1288 })
1289 }
1290
1291 /// Returns the modal that was toggled closed if it was open.
1292 pub fn toggle_modal<V, F>(
1293 &mut self,
1294 cx: &mut ViewContext<Self>,
1295 add_view: F,
1296 ) -> Option<ViewHandle<V>>
1297 where
1298 V: 'static + Modal,
1299 F: FnOnce(&mut Self, &mut ViewContext<Self>) -> ViewHandle<V>,
1300 {
1301 cx.notify();
1302 // Whatever modal was visible is getting clobbered. If its the same type as V, then return
1303 // it. Otherwise, create a new modal and set it as active.
1304 let already_open_modal = self.modal.take().and_then(|modal| modal.downcast::<V>());
1305 if let Some(already_open_modal) = already_open_modal {
1306 cx.focus_self();
1307 Some(already_open_modal)
1308 } else {
1309 let modal = add_view(self, cx);
1310 cx.subscribe(&modal, |this, _, event, cx| {
1311 if V::dismiss_on_event(event) {
1312 this.dismiss_modal(cx);
1313 }
1314 })
1315 .detach();
1316 cx.focus(&modal);
1317 self.modal = Some(modal.into_any());
1318 None
1319 }
1320 }
1321
1322 pub fn modal<V: 'static + View>(&self) -> Option<ViewHandle<V>> {
1323 self.modal
1324 .as_ref()
1325 .and_then(|modal| modal.clone().downcast::<V>())
1326 }
1327
1328 pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) {
1329 if self.modal.take().is_some() {
1330 cx.focus(&self.active_pane);
1331 cx.notify();
1332 }
1333 }
1334
1335 fn zoomed(&self, cx: &WindowContext) -> Option<AnyViewHandle> {
1336 self.zoomed_panel_for_dock(DockPosition::Left, cx)
1337 .or_else(|| self.zoomed_panel_for_dock(DockPosition::Bottom, cx))
1338 .or_else(|| self.zoomed_panel_for_dock(DockPosition::Right, cx))
1339 .or_else(|| self.zoomed_pane(cx))
1340 }
1341
1342 fn zoomed_panel_for_dock(
1343 &self,
1344 position: DockPosition,
1345 cx: &WindowContext,
1346 ) -> Option<AnyViewHandle> {
1347 let (dock, other_docks) = match position {
1348 DockPosition::Left => (&self.left_dock, [&self.bottom_dock, &self.right_dock]),
1349 DockPosition::Bottom => (&self.bottom_dock, [&self.left_dock, &self.right_dock]),
1350 DockPosition::Right => (&self.right_dock, [&self.left_dock, &self.bottom_dock]),
1351 };
1352
1353 let zoomed_panel = dock.read(&cx).zoomed_panel(cx)?;
1354 if other_docks.iter().all(|dock| !dock.read(cx).has_focus(cx))
1355 && !self.active_pane.read(cx).has_focus()
1356 {
1357 Some(zoomed_panel.as_any().clone())
1358 } else {
1359 None
1360 }
1361 }
1362
1363 fn zoomed_pane(&self, cx: &WindowContext) -> Option<AnyViewHandle> {
1364 let active_pane = self.active_pane.read(cx);
1365 let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock];
1366 if active_pane.is_zoomed() && docks.iter().all(|dock| !dock.read(cx).has_focus(cx)) {
1367 Some(self.active_pane.clone().into_any())
1368 } else {
1369 None
1370 }
1371 }
1372
1373 pub fn items<'a>(
1374 &'a self,
1375 cx: &'a AppContext,
1376 ) -> impl 'a + Iterator<Item = &Box<dyn ItemHandle>> {
1377 self.panes.iter().flat_map(|pane| pane.read(cx).items())
1378 }
1379
1380 pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
1381 self.items_of_type(cx).max_by_key(|item| item.id())
1382 }
1383
1384 pub fn items_of_type<'a, T: Item>(
1385 &'a self,
1386 cx: &'a AppContext,
1387 ) -> impl 'a + Iterator<Item = ViewHandle<T>> {
1388 self.panes
1389 .iter()
1390 .flat_map(|pane| pane.read(cx).items_of_type())
1391 }
1392
1393 pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
1394 self.active_pane().read(cx).active_item()
1395 }
1396
1397 fn active_project_path(&self, cx: &ViewContext<Self>) -> Option<ProjectPath> {
1398 self.active_item(cx).and_then(|item| item.project_path(cx))
1399 }
1400
1401 pub fn save_active_item(
1402 &mut self,
1403 force_name_change: bool,
1404 cx: &mut ViewContext<Self>,
1405 ) -> Task<Result<()>> {
1406 let project = self.project.clone();
1407 if let Some(item) = self.active_item(cx) {
1408 if !force_name_change && item.can_save(cx) {
1409 if item.has_conflict(cx) {
1410 const CONFLICT_MESSAGE: &str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1411
1412 let mut answer = cx.prompt(
1413 PromptLevel::Warning,
1414 CONFLICT_MESSAGE,
1415 &["Overwrite", "Cancel"],
1416 );
1417 cx.spawn(|this, mut cx| async move {
1418 let answer = answer.recv().await;
1419 if answer == Some(0) {
1420 this.update(&mut cx, |this, cx| item.save(this.project.clone(), cx))?
1421 .await?;
1422 }
1423 Ok(())
1424 })
1425 } else {
1426 item.save(self.project.clone(), cx)
1427 }
1428 } else if item.is_singleton(cx) {
1429 let worktree = self.worktrees(cx).next();
1430 let start_abs_path = worktree
1431 .and_then(|w| w.read(cx).as_local())
1432 .map_or(Path::new(""), |w| w.abs_path())
1433 .to_path_buf();
1434 let mut abs_path = cx.prompt_for_new_path(&start_abs_path);
1435 cx.spawn(|this, mut cx| async move {
1436 if let Some(abs_path) = abs_path.recv().await.flatten() {
1437 this.update(&mut cx, |_, cx| item.save_as(project, abs_path, cx))?
1438 .await?;
1439 }
1440 Ok(())
1441 })
1442 } else {
1443 Task::ready(Ok(()))
1444 }
1445 } else {
1446 Task::ready(Ok(()))
1447 }
1448 }
1449
1450 pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext<Self>) {
1451 let dock = match dock_side {
1452 DockPosition::Left => &mut self.left_dock,
1453 DockPosition::Bottom => &mut self.bottom_dock,
1454 DockPosition::Right => &mut self.right_dock,
1455 };
1456 dock.update(cx, |dock, cx| {
1457 let open = !dock.is_open();
1458 dock.set_open(open, cx);
1459 });
1460
1461 self.serialize_workspace(cx);
1462
1463 cx.focus_self();
1464 cx.notify();
1465 }
1466
1467 pub fn toggle_panel(&mut self, action: &TogglePanel, cx: &mut ViewContext<Self>) {
1468 let dock = match action.dock_position {
1469 DockPosition::Left => &mut self.left_dock,
1470 DockPosition::Bottom => &mut self.bottom_dock,
1471 DockPosition::Right => &mut self.right_dock,
1472 };
1473 let active_item = dock.update(cx, move |dock, cx| {
1474 if dock.is_open() && dock.active_panel_index() == action.panel_index {
1475 dock.set_open(false, cx);
1476 None
1477 } else {
1478 dock.set_open(true, cx);
1479 dock.activate_panel(action.panel_index, cx);
1480 dock.active_panel().cloned()
1481 }
1482 });
1483
1484 if let Some(active_item) = active_item {
1485 if active_item.has_focus(cx) {
1486 cx.focus_self();
1487 } else {
1488 cx.focus(active_item.as_any());
1489 }
1490 } else {
1491 cx.focus_self();
1492 }
1493
1494 self.serialize_workspace(cx);
1495
1496 cx.notify();
1497 }
1498
1499 pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
1500 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
1501 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
1502 let active_item = dock.update(cx, |dock, cx| {
1503 dock.set_open(true, cx);
1504 dock.activate_panel(panel_index, cx);
1505 dock.active_panel().cloned()
1506 });
1507 if let Some(active_item) = active_item {
1508 if active_item.has_focus(cx) {
1509 cx.focus_self();
1510 } else {
1511 cx.focus(active_item.as_any());
1512 }
1513 }
1514
1515 self.serialize_workspace(cx);
1516 cx.notify();
1517 break;
1518 }
1519 }
1520 }
1521
1522 fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
1523 for pane in &self.panes {
1524 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
1525 }
1526
1527 self.left_dock.update(cx, |dock, cx| dock.zoom_out(cx));
1528 self.bottom_dock.update(cx, |dock, cx| dock.zoom_out(cx));
1529 self.right_dock.update(cx, |dock, cx| dock.zoom_out(cx));
1530
1531 cx.notify();
1532 }
1533
1534 fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
1535 let pane = cx.add_view(|cx| {
1536 Pane::new(
1537 self.weak_handle(),
1538 self.app_state.background_actions,
1539 self.pane_history_timestamp.clone(),
1540 cx,
1541 )
1542 });
1543 cx.subscribe(&pane, Self::handle_pane_event).detach();
1544 self.panes.push(pane.clone());
1545 cx.focus(&pane);
1546 cx.emit(Event::PaneAdded(pane.clone()));
1547 pane
1548 }
1549
1550 pub fn add_item_to_center(
1551 &mut self,
1552 item: Box<dyn ItemHandle>,
1553 cx: &mut ViewContext<Self>,
1554 ) -> bool {
1555 if let Some(center_pane) = self.last_active_center_pane.clone() {
1556 if let Some(center_pane) = center_pane.upgrade(cx) {
1557 Pane::add_item(self, ¢er_pane, item, true, true, None, cx);
1558 true
1559 } else {
1560 false
1561 }
1562 } else {
1563 false
1564 }
1565 }
1566
1567 pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
1568 let active_pane = self.active_pane().clone();
1569 Pane::add_item(self, &active_pane, item, true, true, None, cx);
1570 }
1571
1572 pub fn open_path(
1573 &mut self,
1574 path: impl Into<ProjectPath>,
1575 pane: Option<WeakViewHandle<Pane>>,
1576 focus_item: bool,
1577 cx: &mut ViewContext<Self>,
1578 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
1579 let pane = pane.unwrap_or_else(|| {
1580 self.last_active_center_pane.clone().unwrap_or_else(|| {
1581 self.panes
1582 .first()
1583 .expect("There must be an active pane")
1584 .downgrade()
1585 })
1586 });
1587
1588 let task = self.load_path(path.into(), cx);
1589 cx.spawn(|this, mut cx| async move {
1590 let (project_entry_id, build_item) = task.await?;
1591 let pane = pane
1592 .upgrade(&cx)
1593 .ok_or_else(|| anyhow!("pane was closed"))?;
1594 this.update(&mut cx, |this, cx| {
1595 Pane::open_item(this, pane, project_entry_id, focus_item, cx, build_item)
1596 })
1597 })
1598 }
1599
1600 pub(crate) fn load_path(
1601 &mut self,
1602 path: ProjectPath,
1603 cx: &mut ViewContext<Self>,
1604 ) -> Task<
1605 Result<(
1606 ProjectEntryId,
1607 impl 'static + FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
1608 )>,
1609 > {
1610 let project = self.project().clone();
1611 let project_item = project.update(cx, |project, cx| project.open_path(path, cx));
1612 cx.spawn(|_, mut cx| async move {
1613 let (project_entry_id, project_item) = project_item.await?;
1614 let build_item = cx.update(|cx| {
1615 cx.default_global::<ProjectItemBuilders>()
1616 .get(&project_item.model_type())
1617 .ok_or_else(|| anyhow!("no item builder for project item"))
1618 .cloned()
1619 })?;
1620 let build_item =
1621 move |cx: &mut ViewContext<Pane>| build_item(project, project_item, cx);
1622 Ok((project_entry_id, build_item))
1623 })
1624 }
1625
1626 pub fn open_project_item<T>(
1627 &mut self,
1628 project_item: ModelHandle<T::Item>,
1629 cx: &mut ViewContext<Self>,
1630 ) -> ViewHandle<T>
1631 where
1632 T: ProjectItem,
1633 {
1634 use project::Item as _;
1635
1636 let entry_id = project_item.read(cx).entry_id(cx);
1637 if let Some(item) = entry_id
1638 .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
1639 .and_then(|item| item.downcast())
1640 {
1641 self.activate_item(&item, cx);
1642 return item;
1643 }
1644
1645 let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
1646 self.add_item(Box::new(item.clone()), cx);
1647 item
1648 }
1649
1650 pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
1651 if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
1652 let pane = self.active_pane.clone();
1653 Pane::add_item(self, &pane, Box::new(shared_screen), false, true, None, cx);
1654 }
1655 }
1656
1657 pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext<Self>) -> bool {
1658 let result = self.panes.iter().find_map(|pane| {
1659 pane.read(cx)
1660 .index_for_item(item)
1661 .map(|ix| (pane.clone(), ix))
1662 });
1663 if let Some((pane, ix)) = result {
1664 pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
1665 true
1666 } else {
1667 false
1668 }
1669 }
1670
1671 fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
1672 let panes = self.center.panes();
1673 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
1674 cx.focus(&pane);
1675 } else {
1676 self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx);
1677 }
1678 }
1679
1680 pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
1681 let panes = self.center.panes();
1682 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
1683 let next_ix = (ix + 1) % panes.len();
1684 let next_pane = panes[next_ix].clone();
1685 cx.focus(&next_pane);
1686 }
1687 }
1688
1689 pub fn activate_previous_pane(&mut self, cx: &mut ViewContext<Self>) {
1690 let panes = self.center.panes();
1691 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
1692 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
1693 let prev_pane = panes[prev_ix].clone();
1694 cx.focus(&prev_pane);
1695 }
1696 }
1697
1698 fn handle_pane_focused(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1699 if self.active_pane != pane {
1700 self.active_pane
1701 .update(cx, |pane, cx| pane.set_active(false, cx));
1702 self.active_pane = pane.clone();
1703 self.active_pane
1704 .update(cx, |pane, cx| pane.set_active(true, cx));
1705 self.status_bar.update(cx, |status_bar, cx| {
1706 status_bar.set_active_pane(&self.active_pane, cx);
1707 });
1708 self.active_item_path_changed(cx);
1709 self.last_active_center_pane = Some(pane.downgrade());
1710 }
1711
1712 self.update_followers(
1713 proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
1714 id: self.active_item(cx).and_then(|item| {
1715 item.to_followable_item_handle(cx)?
1716 .remote_id(&self.app_state.client, cx)
1717 .map(|id| id.to_proto())
1718 }),
1719 leader_id: self.leader_for_pane(&pane),
1720 }),
1721 cx,
1722 );
1723
1724 cx.notify();
1725 }
1726
1727 fn handle_pane_event(
1728 &mut self,
1729 pane: ViewHandle<Pane>,
1730 event: &pane::Event,
1731 cx: &mut ViewContext<Self>,
1732 ) {
1733 match event {
1734 pane::Event::Split(direction) => {
1735 self.split_pane(pane, *direction, cx);
1736 }
1737 pane::Event::Remove => self.remove_pane(pane, cx),
1738 pane::Event::ActivateItem { local } => {
1739 if *local {
1740 self.unfollow(&pane, cx);
1741 }
1742 if &pane == self.active_pane() {
1743 self.active_item_path_changed(cx);
1744 }
1745 }
1746 pane::Event::ChangeItemTitle => {
1747 if pane == self.active_pane {
1748 self.active_item_path_changed(cx);
1749 }
1750 self.update_window_edited(cx);
1751 }
1752 pane::Event::RemoveItem { item_id } => {
1753 self.update_window_edited(cx);
1754 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
1755 if entry.get().id() == pane.id() {
1756 entry.remove();
1757 }
1758 }
1759 }
1760 pane::Event::Focus => {
1761 self.handle_pane_focused(pane.clone(), cx);
1762 }
1763 pane::Event::ZoomIn => {
1764 if pane == self.active_pane {
1765 self.zoom_out(cx);
1766 pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
1767 cx.notify();
1768 }
1769 }
1770 pane::Event::ZoomOut => self.zoom_out(cx),
1771 }
1772
1773 self.serialize_workspace(cx);
1774 }
1775
1776 pub fn split_pane(
1777 &mut self,
1778 pane: ViewHandle<Pane>,
1779 direction: SplitDirection,
1780 cx: &mut ViewContext<Self>,
1781 ) -> Option<ViewHandle<Pane>> {
1782 let item = pane.read(cx).active_item()?;
1783 let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
1784 let new_pane = self.add_pane(cx);
1785 Pane::add_item(self, &new_pane, clone, true, true, None, cx);
1786 self.center.split(&pane, &new_pane, direction).unwrap();
1787 Some(new_pane)
1788 } else {
1789 None
1790 };
1791 cx.notify();
1792 maybe_pane_handle
1793 }
1794
1795 pub fn split_pane_with_item(
1796 &mut self,
1797 pane_to_split: WeakViewHandle<Pane>,
1798 split_direction: SplitDirection,
1799 from: WeakViewHandle<Pane>,
1800 item_id_to_move: usize,
1801 cx: &mut ViewContext<Self>,
1802 ) {
1803 let Some(pane_to_split) = pane_to_split.upgrade(cx) else { return; };
1804 let Some(from) = from.upgrade(cx) else { return; };
1805
1806 let new_pane = self.add_pane(cx);
1807 Pane::move_item(self, from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
1808 self.center
1809 .split(&pane_to_split, &new_pane, split_direction)
1810 .unwrap();
1811 cx.notify();
1812 }
1813
1814 pub fn split_pane_with_project_entry(
1815 &mut self,
1816 pane_to_split: WeakViewHandle<Pane>,
1817 split_direction: SplitDirection,
1818 project_entry: ProjectEntryId,
1819 cx: &mut ViewContext<Self>,
1820 ) -> Option<Task<Result<()>>> {
1821 let pane_to_split = pane_to_split.upgrade(cx)?;
1822 let new_pane = self.add_pane(cx);
1823 self.center
1824 .split(&pane_to_split, &new_pane, split_direction)
1825 .unwrap();
1826
1827 let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
1828 let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
1829 Some(cx.foreground().spawn(async move {
1830 task.await?;
1831 Ok(())
1832 }))
1833 }
1834
1835 fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1836 if self.center.remove(&pane).unwrap() {
1837 self.force_remove_pane(&pane, cx);
1838 self.unfollow(&pane, cx);
1839 self.last_leaders_by_pane.remove(&pane.downgrade());
1840 for removed_item in pane.read(cx).items() {
1841 self.panes_by_item.remove(&removed_item.id());
1842 }
1843
1844 cx.notify();
1845 } else {
1846 self.active_item_path_changed(cx);
1847 }
1848 }
1849
1850 pub fn panes(&self) -> &[ViewHandle<Pane>] {
1851 &self.panes
1852 }
1853
1854 pub fn active_pane(&self) -> &ViewHandle<Pane> {
1855 &self.active_pane
1856 }
1857
1858 fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
1859 if let Some(remote_id) = remote_id {
1860 self.remote_entity_subscription = Some(
1861 self.app_state
1862 .client
1863 .add_view_for_remote_entity(remote_id, cx),
1864 );
1865 } else {
1866 self.remote_entity_subscription.take();
1867 }
1868 }
1869
1870 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
1871 self.leader_state.followers.remove(&peer_id);
1872 if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
1873 for state in states_by_pane.into_values() {
1874 for item in state.items_by_leader_view_id.into_values() {
1875 item.set_leader_replica_id(None, cx);
1876 }
1877 }
1878 }
1879 cx.notify();
1880 }
1881
1882 pub fn toggle_follow(
1883 &mut self,
1884 leader_id: PeerId,
1885 cx: &mut ViewContext<Self>,
1886 ) -> Option<Task<Result<()>>> {
1887 let pane = self.active_pane().clone();
1888
1889 if let Some(prev_leader_id) = self.unfollow(&pane, cx) {
1890 if leader_id == prev_leader_id {
1891 return None;
1892 }
1893 }
1894
1895 self.last_leaders_by_pane
1896 .insert(pane.downgrade(), leader_id);
1897 self.follower_states_by_leader
1898 .entry(leader_id)
1899 .or_default()
1900 .insert(pane.clone(), Default::default());
1901 cx.notify();
1902
1903 let project_id = self.project.read(cx).remote_id()?;
1904 let request = self.app_state.client.request(proto::Follow {
1905 project_id,
1906 leader_id: Some(leader_id),
1907 });
1908
1909 Some(cx.spawn(|this, mut cx| async move {
1910 let response = request.await?;
1911 this.update(&mut cx, |this, _| {
1912 let state = this
1913 .follower_states_by_leader
1914 .get_mut(&leader_id)
1915 .and_then(|states_by_pane| states_by_pane.get_mut(&pane))
1916 .ok_or_else(|| anyhow!("following interrupted"))?;
1917 state.active_view_id = if let Some(active_view_id) = response.active_view_id {
1918 Some(ViewId::from_proto(active_view_id)?)
1919 } else {
1920 None
1921 };
1922 Ok::<_, anyhow::Error>(())
1923 })??;
1924 Self::add_views_from_leader(
1925 this.clone(),
1926 leader_id,
1927 vec![pane],
1928 response.views,
1929 &mut cx,
1930 )
1931 .await?;
1932 this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
1933 Ok(())
1934 }))
1935 }
1936
1937 pub fn follow_next_collaborator(
1938 &mut self,
1939 _: &FollowNextCollaborator,
1940 cx: &mut ViewContext<Self>,
1941 ) -> Option<Task<Result<()>>> {
1942 let collaborators = self.project.read(cx).collaborators();
1943 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
1944 let mut collaborators = collaborators.keys().copied();
1945 for peer_id in collaborators.by_ref() {
1946 if peer_id == leader_id {
1947 break;
1948 }
1949 }
1950 collaborators.next()
1951 } else if let Some(last_leader_id) =
1952 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
1953 {
1954 if collaborators.contains_key(last_leader_id) {
1955 Some(*last_leader_id)
1956 } else {
1957 None
1958 }
1959 } else {
1960 None
1961 };
1962
1963 next_leader_id
1964 .or_else(|| collaborators.keys().copied().next())
1965 .and_then(|leader_id| self.toggle_follow(leader_id, cx))
1966 }
1967
1968 pub fn unfollow(
1969 &mut self,
1970 pane: &ViewHandle<Pane>,
1971 cx: &mut ViewContext<Self>,
1972 ) -> Option<PeerId> {
1973 for (leader_id, states_by_pane) in &mut self.follower_states_by_leader {
1974 let leader_id = *leader_id;
1975 if let Some(state) = states_by_pane.remove(pane) {
1976 for (_, item) in state.items_by_leader_view_id {
1977 item.set_leader_replica_id(None, cx);
1978 }
1979
1980 if states_by_pane.is_empty() {
1981 self.follower_states_by_leader.remove(&leader_id);
1982 if let Some(project_id) = self.project.read(cx).remote_id() {
1983 self.app_state
1984 .client
1985 .send(proto::Unfollow {
1986 project_id,
1987 leader_id: Some(leader_id),
1988 })
1989 .log_err();
1990 }
1991 }
1992
1993 cx.notify();
1994 return Some(leader_id);
1995 }
1996 }
1997 None
1998 }
1999
2000 pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
2001 self.follower_states_by_leader.contains_key(&peer_id)
2002 }
2003
2004 pub fn is_followed_by(&self, peer_id: PeerId) -> bool {
2005 self.leader_state.followers.contains(&peer_id)
2006 }
2007
2008 fn render_titlebar(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
2009 // TODO: There should be a better system in place for this
2010 // (https://github.com/zed-industries/zed/issues/1290)
2011 let is_fullscreen = cx.window_is_fullscreen();
2012 let container_theme = if is_fullscreen {
2013 let mut container_theme = theme.workspace.titlebar.container;
2014 container_theme.padding.left = container_theme.padding.right;
2015 container_theme
2016 } else {
2017 theme.workspace.titlebar.container
2018 };
2019
2020 enum TitleBar {}
2021 MouseEventHandler::<TitleBar, _>::new(0, cx, |_, cx| {
2022 Stack::new()
2023 .with_children(
2024 self.titlebar_item
2025 .as_ref()
2026 .map(|item| ChildView::new(item, cx)),
2027 )
2028 .contained()
2029 .with_style(container_theme)
2030 })
2031 .on_click(MouseButton::Left, |event, _, cx| {
2032 if event.click_count == 2 {
2033 cx.zoom_window();
2034 }
2035 })
2036 .constrained()
2037 .with_height(theme.workspace.titlebar.height)
2038 .into_any_named("titlebar")
2039 }
2040
2041 fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
2042 let active_entry = self.active_project_path(cx);
2043 self.project
2044 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
2045 self.update_window_title(cx);
2046 }
2047
2048 fn update_window_title(&mut self, cx: &mut ViewContext<Self>) {
2049 let project = self.project().read(cx);
2050 let mut title = String::new();
2051
2052 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
2053 let filename = path
2054 .path
2055 .file_name()
2056 .map(|s| s.to_string_lossy())
2057 .or_else(|| {
2058 Some(Cow::Borrowed(
2059 project
2060 .worktree_for_id(path.worktree_id, cx)?
2061 .read(cx)
2062 .root_name(),
2063 ))
2064 });
2065
2066 if let Some(filename) = filename {
2067 title.push_str(filename.as_ref());
2068 title.push_str(" β ");
2069 }
2070 }
2071
2072 for (i, name) in project.worktree_root_names(cx).enumerate() {
2073 if i > 0 {
2074 title.push_str(", ");
2075 }
2076 title.push_str(name);
2077 }
2078
2079 if title.is_empty() {
2080 title = "empty project".to_string();
2081 }
2082
2083 if project.is_remote() {
2084 title.push_str(" β");
2085 } else if project.is_shared() {
2086 title.push_str(" β");
2087 }
2088
2089 cx.set_window_title(&title);
2090 }
2091
2092 fn update_window_edited(&mut self, cx: &mut ViewContext<Self>) {
2093 let is_edited = !self.project.read(cx).is_read_only()
2094 && self
2095 .items(cx)
2096 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
2097 if is_edited != self.window_edited {
2098 self.window_edited = is_edited;
2099 cx.set_window_edited(self.window_edited)
2100 }
2101 }
2102
2103 fn render_disconnected_overlay(
2104 &self,
2105 cx: &mut ViewContext<Workspace>,
2106 ) -> Option<AnyElement<Workspace>> {
2107 if self.project.read(cx).is_read_only() {
2108 enum DisconnectedOverlay {}
2109 Some(
2110 MouseEventHandler::<DisconnectedOverlay, _>::new(0, cx, |_, cx| {
2111 let theme = &theme::current(cx);
2112 Label::new(
2113 "Your connection to the remote project has been lost.",
2114 theme.workspace.disconnected_overlay.text.clone(),
2115 )
2116 .aligned()
2117 .contained()
2118 .with_style(theme.workspace.disconnected_overlay.container)
2119 })
2120 .with_cursor_style(CursorStyle::Arrow)
2121 .capture_all()
2122 .into_any_named("disconnected overlay"),
2123 )
2124 } else {
2125 None
2126 }
2127 }
2128
2129 fn render_notifications(
2130 &self,
2131 theme: &theme::Workspace,
2132 cx: &AppContext,
2133 ) -> Option<AnyElement<Workspace>> {
2134 if self.notifications.is_empty() {
2135 None
2136 } else {
2137 Some(
2138 Flex::column()
2139 .with_children(self.notifications.iter().map(|(_, _, notification)| {
2140 ChildView::new(notification.as_any(), cx)
2141 .contained()
2142 .with_style(theme.notification)
2143 }))
2144 .constrained()
2145 .with_width(theme.notifications.width)
2146 .contained()
2147 .with_style(theme.notifications.container)
2148 .aligned()
2149 .bottom()
2150 .right()
2151 .into_any(),
2152 )
2153 }
2154 }
2155
2156 // RPC handlers
2157
2158 async fn handle_follow(
2159 this: WeakViewHandle<Self>,
2160 envelope: TypedEnvelope<proto::Follow>,
2161 _: Arc<Client>,
2162 mut cx: AsyncAppContext,
2163 ) -> Result<proto::FollowResponse> {
2164 this.update(&mut cx, |this, cx| {
2165 let client = &this.app_state.client;
2166 this.leader_state
2167 .followers
2168 .insert(envelope.original_sender_id()?);
2169
2170 let active_view_id = this.active_item(cx).and_then(|i| {
2171 Some(
2172 i.to_followable_item_handle(cx)?
2173 .remote_id(client, cx)?
2174 .to_proto(),
2175 )
2176 });
2177
2178 cx.notify();
2179
2180 Ok(proto::FollowResponse {
2181 active_view_id,
2182 views: this
2183 .panes()
2184 .iter()
2185 .flat_map(|pane| {
2186 let leader_id = this.leader_for_pane(pane);
2187 pane.read(cx).items().filter_map({
2188 let cx = &cx;
2189 move |item| {
2190 let item = item.to_followable_item_handle(cx)?;
2191 let id = item.remote_id(client, cx)?.to_proto();
2192 let variant = item.to_state_proto(cx)?;
2193 Some(proto::View {
2194 id: Some(id),
2195 leader_id,
2196 variant: Some(variant),
2197 })
2198 }
2199 })
2200 })
2201 .collect(),
2202 })
2203 })?
2204 }
2205
2206 async fn handle_unfollow(
2207 this: WeakViewHandle<Self>,
2208 envelope: TypedEnvelope<proto::Unfollow>,
2209 _: Arc<Client>,
2210 mut cx: AsyncAppContext,
2211 ) -> Result<()> {
2212 this.update(&mut cx, |this, cx| {
2213 this.leader_state
2214 .followers
2215 .remove(&envelope.original_sender_id()?);
2216 cx.notify();
2217 Ok(())
2218 })?
2219 }
2220
2221 async fn handle_update_followers(
2222 this: WeakViewHandle<Self>,
2223 envelope: TypedEnvelope<proto::UpdateFollowers>,
2224 _: Arc<Client>,
2225 cx: AsyncAppContext,
2226 ) -> Result<()> {
2227 let leader_id = envelope.original_sender_id()?;
2228 this.read_with(&cx, |this, _| {
2229 this.leader_updates_tx
2230 .unbounded_send((leader_id, envelope.payload))
2231 })??;
2232 Ok(())
2233 }
2234
2235 async fn process_leader_update(
2236 this: &WeakViewHandle<Self>,
2237 leader_id: PeerId,
2238 update: proto::UpdateFollowers,
2239 cx: &mut AsyncAppContext,
2240 ) -> Result<()> {
2241 match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
2242 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
2243 this.update(cx, |this, _| {
2244 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2245 for state in state.values_mut() {
2246 state.active_view_id =
2247 if let Some(active_view_id) = update_active_view.id.clone() {
2248 Some(ViewId::from_proto(active_view_id)?)
2249 } else {
2250 None
2251 };
2252 }
2253 }
2254 anyhow::Ok(())
2255 })??;
2256 }
2257 proto::update_followers::Variant::UpdateView(update_view) => {
2258 let variant = update_view
2259 .variant
2260 .ok_or_else(|| anyhow!("missing update view variant"))?;
2261 let id = update_view
2262 .id
2263 .ok_or_else(|| anyhow!("missing update view id"))?;
2264 let mut tasks = Vec::new();
2265 this.update(cx, |this, cx| {
2266 let project = this.project.clone();
2267 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2268 for state in state.values_mut() {
2269 let view_id = ViewId::from_proto(id.clone())?;
2270 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
2271 tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
2272 }
2273 }
2274 }
2275 anyhow::Ok(())
2276 })??;
2277 try_join_all(tasks).await.log_err();
2278 }
2279 proto::update_followers::Variant::CreateView(view) => {
2280 let panes = this.read_with(cx, |this, _| {
2281 this.follower_states_by_leader
2282 .get(&leader_id)
2283 .into_iter()
2284 .flat_map(|states_by_pane| states_by_pane.keys())
2285 .cloned()
2286 .collect()
2287 })?;
2288 Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?;
2289 }
2290 }
2291 this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
2292 Ok(())
2293 }
2294
2295 async fn add_views_from_leader(
2296 this: WeakViewHandle<Self>,
2297 leader_id: PeerId,
2298 panes: Vec<ViewHandle<Pane>>,
2299 views: Vec<proto::View>,
2300 cx: &mut AsyncAppContext,
2301 ) -> Result<()> {
2302 let project = this.read_with(cx, |this, _| this.project.clone())?;
2303 let replica_id = project
2304 .read_with(cx, |project, _| {
2305 project
2306 .collaborators()
2307 .get(&leader_id)
2308 .map(|c| c.replica_id)
2309 })
2310 .ok_or_else(|| anyhow!("no such collaborator {}", leader_id))?;
2311
2312 let item_builders = cx.update(|cx| {
2313 cx.default_global::<FollowableItemBuilders>()
2314 .values()
2315 .map(|b| b.0)
2316 .collect::<Vec<_>>()
2317 });
2318
2319 let mut item_tasks_by_pane = HashMap::default();
2320 for pane in panes {
2321 let mut item_tasks = Vec::new();
2322 let mut leader_view_ids = Vec::new();
2323 for view in &views {
2324 let Some(id) = &view.id else { continue };
2325 let id = ViewId::from_proto(id.clone())?;
2326 let mut variant = view.variant.clone();
2327 if variant.is_none() {
2328 Err(anyhow!("missing variant"))?;
2329 }
2330 for build_item in &item_builders {
2331 let task = cx.update(|cx| {
2332 build_item(pane.clone(), project.clone(), id, &mut variant, cx)
2333 });
2334 if let Some(task) = task {
2335 item_tasks.push(task);
2336 leader_view_ids.push(id);
2337 break;
2338 } else {
2339 assert!(variant.is_some());
2340 }
2341 }
2342 }
2343
2344 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
2345 }
2346
2347 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
2348 let items = futures::future::try_join_all(item_tasks).await?;
2349 this.update(cx, |this, cx| {
2350 let state = this
2351 .follower_states_by_leader
2352 .get_mut(&leader_id)?
2353 .get_mut(&pane)?;
2354
2355 for (id, item) in leader_view_ids.into_iter().zip(items) {
2356 item.set_leader_replica_id(Some(replica_id), cx);
2357 state.items_by_leader_view_id.insert(id, item);
2358 }
2359
2360 Some(())
2361 })?;
2362 }
2363 Ok(())
2364 }
2365
2366 fn update_followers(
2367 &self,
2368 update: proto::update_followers::Variant,
2369 cx: &AppContext,
2370 ) -> Option<()> {
2371 let project_id = self.project.read(cx).remote_id()?;
2372 if !self.leader_state.followers.is_empty() {
2373 self.app_state
2374 .client
2375 .send(proto::UpdateFollowers {
2376 project_id,
2377 follower_ids: self.leader_state.followers.iter().copied().collect(),
2378 variant: Some(update),
2379 })
2380 .log_err();
2381 }
2382 None
2383 }
2384
2385 pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
2386 self.follower_states_by_leader
2387 .iter()
2388 .find_map(|(leader_id, state)| {
2389 if state.contains_key(pane) {
2390 Some(*leader_id)
2391 } else {
2392 None
2393 }
2394 })
2395 }
2396
2397 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
2398 cx.notify();
2399
2400 let call = self.active_call()?;
2401 let room = call.read(cx).room()?.read(cx);
2402 let participant = room.remote_participant_for_peer_id(leader_id)?;
2403 let mut items_to_activate = Vec::new();
2404 match participant.location {
2405 call::ParticipantLocation::SharedProject { project_id } => {
2406 if Some(project_id) == self.project.read(cx).remote_id() {
2407 for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
2408 if let Some(item) = state
2409 .active_view_id
2410 .and_then(|id| state.items_by_leader_view_id.get(&id))
2411 {
2412 items_to_activate.push((pane.clone(), item.boxed_clone()));
2413 } else {
2414 if let Some(shared_screen) =
2415 self.shared_screen_for_peer(leader_id, pane, cx)
2416 {
2417 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2418 }
2419 }
2420 }
2421 }
2422 }
2423 call::ParticipantLocation::UnsharedProject => {}
2424 call::ParticipantLocation::External => {
2425 for (pane, _) in self.follower_states_by_leader.get(&leader_id)? {
2426 if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
2427 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2428 }
2429 }
2430 }
2431 }
2432
2433 for (pane, item) in items_to_activate {
2434 let pane_was_focused = pane.read(cx).has_focus();
2435 if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
2436 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
2437 } else {
2438 Pane::add_item(self, &pane, item.boxed_clone(), false, false, None, cx);
2439 }
2440
2441 if pane_was_focused {
2442 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
2443 }
2444 }
2445
2446 None
2447 }
2448
2449 fn shared_screen_for_peer(
2450 &self,
2451 peer_id: PeerId,
2452 pane: &ViewHandle<Pane>,
2453 cx: &mut ViewContext<Self>,
2454 ) -> Option<ViewHandle<SharedScreen>> {
2455 let call = self.active_call()?;
2456 let room = call.read(cx).room()?.read(cx);
2457 let participant = room.remote_participant_for_peer_id(peer_id)?;
2458 let track = participant.tracks.values().next()?.clone();
2459 let user = participant.user.clone();
2460
2461 for item in pane.read(cx).items_of_type::<SharedScreen>() {
2462 if item.read(cx).peer_id == peer_id {
2463 return Some(item);
2464 }
2465 }
2466
2467 Some(cx.add_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
2468 }
2469
2470 pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
2471 if active {
2472 cx.background()
2473 .spawn(persistence::DB.update_timestamp(self.database_id()))
2474 .detach();
2475 } else {
2476 for pane in &self.panes {
2477 pane.update(cx, |pane, cx| {
2478 if let Some(item) = pane.active_item() {
2479 item.workspace_deactivated(cx);
2480 }
2481 if matches!(
2482 settings::get::<WorkspaceSettings>(cx).autosave,
2483 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
2484 ) {
2485 for item in pane.items() {
2486 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
2487 .detach_and_log_err(cx);
2488 }
2489 }
2490 });
2491 }
2492 }
2493 }
2494
2495 fn active_call(&self) -> Option<&ModelHandle<ActiveCall>> {
2496 self.active_call.as_ref().map(|(call, _)| call)
2497 }
2498
2499 fn on_active_call_event(
2500 &mut self,
2501 _: ModelHandle<ActiveCall>,
2502 event: &call::room::Event,
2503 cx: &mut ViewContext<Self>,
2504 ) {
2505 match event {
2506 call::room::Event::ParticipantLocationChanged { participant_id }
2507 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
2508 self.leader_updated(*participant_id, cx);
2509 }
2510 _ => {}
2511 }
2512 }
2513
2514 pub fn database_id(&self) -> WorkspaceId {
2515 self.database_id
2516 }
2517
2518 fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
2519 let project = self.project().read(cx);
2520
2521 if project.is_local() {
2522 Some(
2523 project
2524 .visible_worktrees(cx)
2525 .map(|worktree| worktree.read(cx).abs_path())
2526 .collect::<Vec<_>>()
2527 .into(),
2528 )
2529 } else {
2530 None
2531 }
2532 }
2533
2534 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
2535 match member {
2536 Member::Axis(PaneAxis { members, .. }) => {
2537 for child in members.iter() {
2538 self.remove_panes(child.clone(), cx)
2539 }
2540 }
2541 Member::Pane(pane) => {
2542 self.force_remove_pane(&pane, cx);
2543 }
2544 }
2545 }
2546
2547 fn force_remove_pane(&mut self, pane: &ViewHandle<Pane>, cx: &mut ViewContext<Workspace>) {
2548 self.panes.retain(|p| p != pane);
2549 cx.focus(self.panes.last().unwrap());
2550 if self.last_active_center_pane == Some(pane.downgrade()) {
2551 self.last_active_center_pane = None;
2552 }
2553 cx.notify();
2554 }
2555
2556 fn serialize_workspace(&self, cx: &AppContext) {
2557 fn serialize_pane_handle(
2558 pane_handle: &ViewHandle<Pane>,
2559 cx: &AppContext,
2560 ) -> SerializedPane {
2561 let (items, active) = {
2562 let pane = pane_handle.read(cx);
2563 let active_item_id = pane.active_item().map(|item| item.id());
2564 (
2565 pane.items()
2566 .filter_map(|item_handle| {
2567 Some(SerializedItem {
2568 kind: Arc::from(item_handle.serialized_item_kind()?),
2569 item_id: item_handle.id(),
2570 active: Some(item_handle.id()) == active_item_id,
2571 })
2572 })
2573 .collect::<Vec<_>>(),
2574 pane.is_active(),
2575 )
2576 };
2577
2578 SerializedPane::new(items, active)
2579 }
2580
2581 fn build_serialized_pane_group(
2582 pane_group: &Member,
2583 cx: &AppContext,
2584 ) -> SerializedPaneGroup {
2585 match pane_group {
2586 Member::Axis(PaneAxis { axis, members }) => SerializedPaneGroup::Group {
2587 axis: *axis,
2588 children: members
2589 .iter()
2590 .map(|member| build_serialized_pane_group(member, cx))
2591 .collect::<Vec<_>>(),
2592 },
2593 Member::Pane(pane_handle) => {
2594 SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx))
2595 }
2596 }
2597 }
2598
2599 fn build_serialized_docks(this: &Workspace, cx: &AppContext) -> DockStructure {
2600 let left_dock = this.left_dock.read(cx);
2601 let left_visible = left_dock.is_open();
2602 let left_active_panel = left_dock.active_panel().and_then(|panel| {
2603 Some(
2604 cx.view_ui_name(panel.as_any().window_id(), panel.id())?
2605 .to_string(),
2606 )
2607 });
2608
2609 let right_dock = this.right_dock.read(cx);
2610 let right_visible = right_dock.is_open();
2611 let right_active_panel = right_dock.active_panel().and_then(|panel| {
2612 Some(
2613 cx.view_ui_name(panel.as_any().window_id(), panel.id())?
2614 .to_string(),
2615 )
2616 });
2617
2618 let bottom_dock = this.bottom_dock.read(cx);
2619 let bottom_visible = bottom_dock.is_open();
2620 let bottom_active_panel = bottom_dock.active_panel().and_then(|panel| {
2621 Some(
2622 cx.view_ui_name(panel.as_any().window_id(), panel.id())?
2623 .to_string(),
2624 )
2625 });
2626
2627 DockStructure {
2628 left: DockData {
2629 visible: left_visible,
2630 active_panel: left_active_panel,
2631 },
2632 right: DockData {
2633 visible: right_visible,
2634 active_panel: right_active_panel,
2635 },
2636 bottom: DockData {
2637 visible: bottom_visible,
2638 active_panel: bottom_active_panel,
2639 },
2640 }
2641 }
2642
2643 if let Some(location) = self.location(cx) {
2644 // Load bearing special case:
2645 // - with_local_workspace() relies on this to not have other stuff open
2646 // when you open your log
2647 if !location.paths().is_empty() {
2648 let center_group = build_serialized_pane_group(&self.center.root, cx);
2649 let docks = build_serialized_docks(self, cx);
2650
2651 let serialized_workspace = SerializedWorkspace {
2652 id: self.database_id,
2653 location,
2654 center_group,
2655 bounds: Default::default(),
2656 display: Default::default(),
2657 docks,
2658 };
2659
2660 cx.background()
2661 .spawn(persistence::DB.save_workspace(serialized_workspace))
2662 .detach();
2663 }
2664 }
2665 }
2666
2667 pub(crate) fn load_workspace(
2668 workspace: WeakViewHandle<Workspace>,
2669 serialized_workspace: SerializedWorkspace,
2670 paths_to_open: Vec<Option<ProjectPath>>,
2671 cx: &mut AppContext,
2672 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
2673 cx.spawn(|mut cx| async move {
2674 let result = async_iife! {{
2675 let (project, old_center_pane) =
2676 workspace.read_with(&cx, |workspace, _| {
2677 (
2678 workspace.project().clone(),
2679 workspace.last_active_center_pane.clone(),
2680 )
2681 })?;
2682
2683 let mut center_items = None;
2684 let mut center_group = None;
2685 // Traverse the splits tree and add to things
2686 if let Some((group, active_pane, items)) = serialized_workspace
2687 .center_group
2688 .deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
2689 .await {
2690 center_items = Some(items);
2691 center_group = Some((group, active_pane))
2692 }
2693
2694 let resulting_list = cx.read(|cx| {
2695 let mut opened_items = center_items
2696 .unwrap_or_default()
2697 .into_iter()
2698 .filter_map(|item| {
2699 let item = item?;
2700 let project_path = item.project_path(cx)?;
2701 Some((project_path, item))
2702 })
2703 .collect::<HashMap<_, _>>();
2704
2705 paths_to_open
2706 .into_iter()
2707 .map(|path_to_open| {
2708 path_to_open.map(|path_to_open| {
2709 Ok(opened_items.remove(&path_to_open))
2710 })
2711 .transpose()
2712 .map(|item| item.flatten())
2713 .transpose()
2714 })
2715 .collect::<Vec<_>>()
2716 });
2717
2718 // Remove old panes from workspace panes list
2719 workspace.update(&mut cx, |workspace, cx| {
2720 if let Some((center_group, active_pane)) = center_group {
2721 workspace.remove_panes(workspace.center.root.clone(), cx);
2722
2723 // Swap workspace center group
2724 workspace.center = PaneGroup::with_root(center_group);
2725
2726 // Change the focus to the workspace first so that we retrigger focus in on the pane.
2727 cx.focus_self();
2728
2729 if let Some(active_pane) = active_pane {
2730 cx.focus(&active_pane);
2731 } else {
2732 cx.focus(workspace.panes.last().unwrap());
2733 }
2734 } else {
2735 let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx));
2736 if let Some(old_center_handle) = old_center_handle {
2737 cx.focus(&old_center_handle)
2738 } else {
2739 cx.focus_self()
2740 }
2741 }
2742
2743 let docks = serialized_workspace.docks;
2744 workspace.left_dock.update(cx, |dock, cx| {
2745 dock.set_open(docks.left.visible, cx);
2746 if let Some(active_panel) = docks.left.active_panel {
2747 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
2748 dock.activate_panel(ix, cx);
2749 }
2750 }
2751 });
2752 workspace.right_dock.update(cx, |dock, cx| {
2753 dock.set_open(docks.right.visible, cx);
2754 if let Some(active_panel) = docks.right.active_panel {
2755 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
2756 dock.activate_panel(ix, cx);
2757 }
2758 }
2759 });
2760 workspace.bottom_dock.update(cx, |dock, cx| {
2761 dock.set_open(docks.bottom.visible, cx);
2762 if let Some(active_panel) = docks.bottom.active_panel {
2763 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
2764 dock.activate_panel(ix, cx);
2765 }
2766 }
2767 });
2768
2769 cx.notify();
2770 })?;
2771
2772 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
2773 workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?;
2774
2775 Ok::<_, anyhow::Error>(resulting_list)
2776 }};
2777
2778 result.await.unwrap_or_default()
2779 })
2780 }
2781
2782 #[cfg(any(test, feature = "test-support"))]
2783 pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
2784 let app_state = Arc::new(AppState {
2785 languages: project.read(cx).languages().clone(),
2786 client: project.read(cx).client(),
2787 user_store: project.read(cx).user_store(),
2788 fs: project.read(cx).fs().clone(),
2789 build_window_options: |_, _, _| Default::default(),
2790 initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
2791 background_actions: || &[],
2792 });
2793 Self::new(0, project, app_state, cx)
2794 }
2795
2796 fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option<AnyElement<Self>> {
2797 let dock = match position {
2798 DockPosition::Left => &self.left_dock,
2799 DockPosition::Right => &self.right_dock,
2800 DockPosition::Bottom => &self.bottom_dock,
2801 };
2802 let active_panel = dock.read(cx).active_panel()?;
2803 let element = if Some(active_panel.as_any()) == self.zoomed(cx).as_ref() {
2804 dock.read(cx).render_placeholder(cx)
2805 } else {
2806 ChildView::new(dock, cx).into_any()
2807 };
2808
2809 Some(
2810 element
2811 .constrained()
2812 .dynamically(move |constraint, _, cx| match position {
2813 DockPosition::Left | DockPosition::Right => SizeConstraint::new(
2814 Vector2F::new(20., constraint.min.y()),
2815 Vector2F::new(cx.window_size().x() * 0.8, constraint.max.y()),
2816 ),
2817 _ => constraint,
2818 })
2819 .into_any(),
2820 )
2821 }
2822}
2823
2824async fn open_items(
2825 serialized_workspace: Option<SerializedWorkspace>,
2826 workspace: &WeakViewHandle<Workspace>,
2827 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
2828 app_state: Arc<AppState>,
2829 mut cx: AsyncAppContext,
2830) -> Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>> {
2831 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
2832
2833 if let Some(serialized_workspace) = serialized_workspace {
2834 let workspace = workspace.clone();
2835 let restored_items = cx
2836 .update(|cx| {
2837 Workspace::load_workspace(
2838 workspace,
2839 serialized_workspace,
2840 project_paths_to_open
2841 .iter()
2842 .map(|(_, project_path)| project_path)
2843 .cloned()
2844 .collect(),
2845 cx,
2846 )
2847 })
2848 .await;
2849
2850 let restored_project_paths = cx.read(|cx| {
2851 restored_items
2852 .iter()
2853 .filter_map(|item| item.as_ref()?.as_ref().ok()?.project_path(cx))
2854 .collect::<HashSet<_>>()
2855 });
2856
2857 opened_items = restored_items;
2858 project_paths_to_open
2859 .iter_mut()
2860 .for_each(|(_, project_path)| {
2861 if let Some(project_path_to_open) = project_path {
2862 if restored_project_paths.contains(project_path_to_open) {
2863 *project_path = None;
2864 }
2865 }
2866 });
2867 } else {
2868 for _ in 0..project_paths_to_open.len() {
2869 opened_items.push(None);
2870 }
2871 }
2872 assert!(opened_items.len() == project_paths_to_open.len());
2873
2874 let tasks =
2875 project_paths_to_open
2876 .into_iter()
2877 .enumerate()
2878 .map(|(i, (abs_path, project_path))| {
2879 let workspace = workspace.clone();
2880 cx.spawn(|mut cx| {
2881 let fs = app_state.fs.clone();
2882 async move {
2883 let file_project_path = project_path?;
2884 if fs.is_file(&abs_path).await {
2885 Some((
2886 i,
2887 workspace
2888 .update(&mut cx, |workspace, cx| {
2889 workspace.open_path(file_project_path, None, true, cx)
2890 })
2891 .log_err()?
2892 .await,
2893 ))
2894 } else {
2895 None
2896 }
2897 }
2898 })
2899 });
2900
2901 for maybe_opened_path in futures::future::join_all(tasks.into_iter())
2902 .await
2903 .into_iter()
2904 {
2905 if let Some((i, path_open_result)) = maybe_opened_path {
2906 opened_items[i] = Some(path_open_result);
2907 }
2908 }
2909
2910 opened_items
2911}
2912
2913fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
2914 const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
2915
2916 workspace
2917 .update(cx, |workspace, cx| {
2918 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
2919 workspace.show_notification_once(0, cx, |cx| {
2920 cx.add_view(|_| {
2921 MessageNotification::new("Failed to load any database file.")
2922 .with_click_message("Click to let us know about this error")
2923 .on_click(|cx| cx.platform().open_url(REPORT_ISSUE_URL))
2924 })
2925 });
2926 } else {
2927 let backup_path = (*db::BACKUP_DB_PATH).read();
2928 if let Some(backup_path) = backup_path.clone() {
2929 workspace.show_notification_once(0, cx, move |cx| {
2930 cx.add_view(move |_| {
2931 MessageNotification::new(format!(
2932 "Database file was corrupted. Old database backed up to {}",
2933 backup_path.display()
2934 ))
2935 .with_click_message("Click to show old database in finder")
2936 .on_click(move |cx| {
2937 cx.platform().open_url(&backup_path.to_string_lossy())
2938 })
2939 })
2940 });
2941 }
2942 }
2943 })
2944 .log_err();
2945}
2946
2947impl Entity for Workspace {
2948 type Event = Event;
2949}
2950
2951impl View for Workspace {
2952 fn ui_name() -> &'static str {
2953 "Workspace"
2954 }
2955
2956 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
2957 let theme = theme::current(cx).clone();
2958 Stack::new()
2959 .with_child(
2960 Flex::column()
2961 .with_child(self.render_titlebar(&theme, cx))
2962 .with_child(
2963 Stack::new()
2964 .with_child({
2965 let project = self.project.clone();
2966 Flex::row()
2967 .with_children(self.render_dock(DockPosition::Left, cx))
2968 .with_child(
2969 Flex::column()
2970 .with_child(
2971 FlexItem::new(self.center.render(
2972 &project,
2973 &theme,
2974 &self.follower_states_by_leader,
2975 self.active_call(),
2976 self.active_pane(),
2977 self.zoomed(cx).as_ref(),
2978 &self.app_state,
2979 cx,
2980 ))
2981 .flex(1., true),
2982 )
2983 .with_children(
2984 self.render_dock(DockPosition::Bottom, cx),
2985 )
2986 .flex(1., true),
2987 )
2988 .with_children(self.render_dock(DockPosition::Right, cx))
2989 })
2990 .with_child(Overlay::new(
2991 Stack::new()
2992 .with_children(self.zoomed(cx).map(|zoomed| {
2993 enum ZoomBackground {}
2994
2995 ChildView::new(&zoomed, cx)
2996 .contained()
2997 .with_style(theme.workspace.zoomed_foreground)
2998 .aligned()
2999 .contained()
3000 .with_style(theme.workspace.zoomed_background)
3001 .mouse::<ZoomBackground>(0)
3002 .capture_all()
3003 .on_down(MouseButton::Left, |_, this: &mut Self, cx| {
3004 this.zoom_out(cx);
3005 })
3006 }))
3007 .with_children(self.modal.as_ref().map(|modal| {
3008 ChildView::new(modal, cx)
3009 .contained()
3010 .with_style(theme.workspace.modal)
3011 .aligned()
3012 .top()
3013 }))
3014 .with_children(self.render_notifications(&theme.workspace, cx)),
3015 ))
3016 .flex(1.0, true),
3017 )
3018 .with_child(ChildView::new(&self.status_bar, cx))
3019 .contained()
3020 .with_background_color(theme.workspace.background),
3021 )
3022 .with_children(DragAndDrop::render(cx))
3023 .with_children(self.render_disconnected_overlay(cx))
3024 .into_any_named("workspace")
3025 }
3026
3027 fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
3028 if cx.is_self_focused() {
3029 cx.focus(&self.active_pane);
3030 }
3031 cx.notify();
3032 }
3033}
3034
3035impl ViewId {
3036 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
3037 Ok(Self {
3038 creator: message
3039 .creator
3040 .ok_or_else(|| anyhow!("creator is missing"))?,
3041 id: message.id,
3042 })
3043 }
3044
3045 pub(crate) fn to_proto(&self) -> proto::ViewId {
3046 proto::ViewId {
3047 creator: Some(self.creator),
3048 id: self.id,
3049 }
3050 }
3051}
3052
3053pub trait WorkspaceHandle {
3054 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
3055}
3056
3057impl WorkspaceHandle for ViewHandle<Workspace> {
3058 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
3059 self.read(cx)
3060 .worktrees(cx)
3061 .flat_map(|worktree| {
3062 let worktree_id = worktree.read(cx).id();
3063 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
3064 worktree_id,
3065 path: f.path.clone(),
3066 })
3067 })
3068 .collect::<Vec<_>>()
3069 }
3070}
3071
3072impl std::fmt::Debug for OpenPaths {
3073 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3074 f.debug_struct("OpenPaths")
3075 .field("paths", &self.paths)
3076 .finish()
3077 }
3078}
3079
3080pub struct WorkspaceCreated(WeakViewHandle<Workspace>);
3081
3082pub fn activate_workspace_for_project(
3083 cx: &mut AsyncAppContext,
3084 predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
3085) -> Option<WeakViewHandle<Workspace>> {
3086 for window_id in cx.window_ids() {
3087 let handle = cx
3088 .update_window(window_id, |cx| {
3089 if let Some(workspace_handle) = cx.root_view().clone().downcast::<Workspace>() {
3090 let project = workspace_handle.read(cx).project.clone();
3091 if project.update(cx, &predicate) {
3092 cx.activate_window();
3093 return Some(workspace_handle.clone());
3094 }
3095 }
3096 None
3097 })
3098 .flatten();
3099
3100 if let Some(handle) = handle {
3101 return Some(handle.downgrade());
3102 }
3103 }
3104 None
3105}
3106
3107pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
3108 DB.last_workspace().await.log_err().flatten()
3109}
3110
3111#[allow(clippy::type_complexity)]
3112pub fn open_paths(
3113 abs_paths: &[PathBuf],
3114 app_state: &Arc<AppState>,
3115 requesting_window_id: Option<usize>,
3116 cx: &mut AppContext,
3117) -> Task<
3118 Result<(
3119 WeakViewHandle<Workspace>,
3120 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
3121 )>,
3122> {
3123 let app_state = app_state.clone();
3124 let abs_paths = abs_paths.to_vec();
3125 cx.spawn(|mut cx| async move {
3126 // Open paths in existing workspace if possible
3127 let existing = activate_workspace_for_project(&mut cx, |project, cx| {
3128 project.contains_paths(&abs_paths, cx)
3129 });
3130
3131 if let Some(existing) = existing {
3132 Ok((
3133 existing.clone(),
3134 existing
3135 .update(&mut cx, |workspace, cx| {
3136 workspace.open_paths(abs_paths, true, cx)
3137 })?
3138 .await,
3139 ))
3140 } else {
3141 Ok(cx
3142 .update(|cx| {
3143 Workspace::new_local(abs_paths, app_state.clone(), requesting_window_id, cx)
3144 })
3145 .await)
3146 }
3147 })
3148}
3149
3150pub fn open_new(
3151 app_state: &Arc<AppState>,
3152 cx: &mut AppContext,
3153 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static,
3154) -> Task<()> {
3155 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
3156 cx.spawn(|mut cx| async move {
3157 let (workspace, opened_paths) = task.await;
3158
3159 workspace
3160 .update(&mut cx, |workspace, cx| {
3161 if opened_paths.is_empty() {
3162 init(workspace, cx)
3163 }
3164 })
3165 .log_err();
3166 })
3167}
3168
3169pub fn create_and_open_local_file(
3170 path: &'static Path,
3171 cx: &mut ViewContext<Workspace>,
3172 default_content: impl 'static + Send + FnOnce() -> Rope,
3173) -> Task<Result<Box<dyn ItemHandle>>> {
3174 cx.spawn(|workspace, mut cx| async move {
3175 let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?;
3176 if !fs.is_file(path).await {
3177 fs.create_file(path, Default::default()).await?;
3178 fs.save(path, &default_content(), Default::default())
3179 .await?;
3180 }
3181
3182 let mut items = workspace
3183 .update(&mut cx, |workspace, cx| {
3184 workspace.with_local_workspace(cx, |workspace, cx| {
3185 workspace.open_paths(vec![path.to_path_buf()], false, cx)
3186 })
3187 })?
3188 .await?
3189 .await;
3190
3191 let item = items.pop().flatten();
3192 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
3193 })
3194}
3195
3196pub fn join_remote_project(
3197 project_id: u64,
3198 follow_user_id: u64,
3199 app_state: Arc<AppState>,
3200 cx: &mut AppContext,
3201) -> Task<Result<()>> {
3202 cx.spawn(|mut cx| async move {
3203 let existing_workspace = cx
3204 .window_ids()
3205 .into_iter()
3206 .filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
3207 .find(|workspace| {
3208 cx.read_window(workspace.window_id(), |cx| {
3209 workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
3210 })
3211 .unwrap_or(false)
3212 });
3213
3214 let workspace = if let Some(existing_workspace) = existing_workspace {
3215 existing_workspace.downgrade()
3216 } else {
3217 let active_call = cx.read(ActiveCall::global);
3218 let room = active_call
3219 .read_with(&cx, |call, _| call.room().cloned())
3220 .ok_or_else(|| anyhow!("not in a call"))?;
3221 let project = room
3222 .update(&mut cx, |room, cx| {
3223 room.join_project(
3224 project_id,
3225 app_state.languages.clone(),
3226 app_state.fs.clone(),
3227 cx,
3228 )
3229 })
3230 .await?;
3231
3232 let (_, workspace) = cx.add_window(
3233 (app_state.build_window_options)(None, None, cx.platform().as_ref()),
3234 |cx| Workspace::new(0, project, app_state.clone(), cx),
3235 );
3236 (app_state.initialize_workspace)(
3237 workspace.downgrade(),
3238 false,
3239 app_state.clone(),
3240 cx.clone(),
3241 )
3242 .await
3243 .log_err();
3244
3245 workspace.downgrade()
3246 };
3247
3248 cx.activate_window(workspace.window_id());
3249 cx.platform().activate(true);
3250
3251 workspace.update(&mut cx, |workspace, cx| {
3252 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
3253 let follow_peer_id = room
3254 .read(cx)
3255 .remote_participants()
3256 .iter()
3257 .find(|(_, participant)| participant.user.id == follow_user_id)
3258 .map(|(_, p)| p.peer_id)
3259 .or_else(|| {
3260 // If we couldn't follow the given user, follow the host instead.
3261 let collaborator = workspace
3262 .project()
3263 .read(cx)
3264 .collaborators()
3265 .values()
3266 .find(|collaborator| collaborator.replica_id == 0)?;
3267 Some(collaborator.peer_id)
3268 });
3269
3270 if let Some(follow_peer_id) = follow_peer_id {
3271 if !workspace.is_being_followed(follow_peer_id) {
3272 workspace
3273 .toggle_follow(follow_peer_id, cx)
3274 .map(|follow| follow.detach_and_log_err(cx));
3275 }
3276 }
3277 }
3278 })?;
3279
3280 anyhow::Ok(())
3281 })
3282}
3283
3284pub fn restart(_: &Restart, cx: &mut AppContext) {
3285 let should_confirm = settings::get::<WorkspaceSettings>(cx).confirm_quit;
3286 cx.spawn(|mut cx| async move {
3287 let mut workspaces = cx
3288 .window_ids()
3289 .into_iter()
3290 .filter_map(|window_id| {
3291 Some(
3292 cx.root_view(window_id)?
3293 .clone()
3294 .downcast::<Workspace>()?
3295 .downgrade(),
3296 )
3297 })
3298 .collect::<Vec<_>>();
3299
3300 // If multiple windows have unsaved changes, and need a save prompt,
3301 // prompt in the active window before switching to a different window.
3302 workspaces.sort_by_key(|workspace| !cx.window_is_active(workspace.window_id()));
3303
3304 if let (true, Some(workspace)) = (should_confirm, workspaces.first()) {
3305 let answer = cx.prompt(
3306 workspace.window_id(),
3307 PromptLevel::Info,
3308 "Are you sure you want to restart?",
3309 &["Restart", "Cancel"],
3310 );
3311
3312 if let Some(mut answer) = answer {
3313 let answer = answer.next().await;
3314 if answer != Some(0) {
3315 return Ok(());
3316 }
3317 }
3318 }
3319
3320 // If the user cancels any save prompt, then keep the app open.
3321 for workspace in workspaces {
3322 if !workspace
3323 .update(&mut cx, |workspace, cx| {
3324 workspace.prepare_to_close(true, cx)
3325 })?
3326 .await?
3327 {
3328 return Ok(());
3329 }
3330 }
3331 cx.platform().restart();
3332 anyhow::Ok(())
3333 })
3334 .detach_and_log_err(cx);
3335}
3336
3337fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
3338 let mut parts = value.split(',');
3339 let width: usize = parts.next()?.parse().ok()?;
3340 let height: usize = parts.next()?.parse().ok()?;
3341 Some(vec2f(width as f32, height as f32))
3342}
3343
3344#[cfg(test)]
3345mod tests {
3346 use super::*;
3347 use crate::{
3348 dock::test::{TestPanel, TestPanelEvent},
3349 item::test::{TestItem, TestItemEvent, TestProjectItem},
3350 };
3351 use fs::FakeFs;
3352 use gpui::{executor::Deterministic, TestAppContext};
3353 use project::{Project, ProjectEntryId};
3354 use serde_json::json;
3355 use settings::SettingsStore;
3356 use std::{cell::RefCell, rc::Rc};
3357
3358 #[gpui::test]
3359 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
3360 init_test(cx);
3361
3362 let fs = FakeFs::new(cx.background());
3363 let project = Project::test(fs, [], cx).await;
3364 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3365
3366 // Adding an item with no ambiguity renders the tab without detail.
3367 let item1 = cx.add_view(window_id, |_| {
3368 let mut item = TestItem::new();
3369 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
3370 item
3371 });
3372 workspace.update(cx, |workspace, cx| {
3373 workspace.add_item(Box::new(item1.clone()), cx);
3374 });
3375 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None));
3376
3377 // Adding an item that creates ambiguity increases the level of detail on
3378 // both tabs.
3379 let item2 = cx.add_view(window_id, |_| {
3380 let mut item = TestItem::new();
3381 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
3382 item
3383 });
3384 workspace.update(cx, |workspace, cx| {
3385 workspace.add_item(Box::new(item2.clone()), cx);
3386 });
3387 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3388 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3389
3390 // Adding an item that creates ambiguity increases the level of detail only
3391 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
3392 // we stop at the highest detail available.
3393 let item3 = cx.add_view(window_id, |_| {
3394 let mut item = TestItem::new();
3395 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
3396 item
3397 });
3398 workspace.update(cx, |workspace, cx| {
3399 workspace.add_item(Box::new(item3.clone()), cx);
3400 });
3401 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3402 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
3403 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
3404 }
3405
3406 #[gpui::test]
3407 async fn test_tracking_active_path(cx: &mut TestAppContext) {
3408 init_test(cx);
3409
3410 let fs = FakeFs::new(cx.background());
3411 fs.insert_tree(
3412 "/root1",
3413 json!({
3414 "one.txt": "",
3415 "two.txt": "",
3416 }),
3417 )
3418 .await;
3419 fs.insert_tree(
3420 "/root2",
3421 json!({
3422 "three.txt": "",
3423 }),
3424 )
3425 .await;
3426
3427 let project = Project::test(fs, ["root1".as_ref()], cx).await;
3428 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3429 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3430 let worktree_id = project.read_with(cx, |project, cx| {
3431 project.worktrees(cx).next().unwrap().read(cx).id()
3432 });
3433
3434 let item1 = cx.add_view(window_id, |cx| {
3435 TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3436 });
3437 let item2 = cx.add_view(window_id, |cx| {
3438 TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
3439 });
3440
3441 // Add an item to an empty pane
3442 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
3443 project.read_with(cx, |project, cx| {
3444 assert_eq!(
3445 project.active_entry(),
3446 project
3447 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
3448 .map(|e| e.id)
3449 );
3450 });
3451 assert_eq!(
3452 cx.current_window_title(window_id).as_deref(),
3453 Some("one.txt β root1")
3454 );
3455
3456 // Add a second item to a non-empty pane
3457 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
3458 assert_eq!(
3459 cx.current_window_title(window_id).as_deref(),
3460 Some("two.txt β root1")
3461 );
3462 project.read_with(cx, |project, cx| {
3463 assert_eq!(
3464 project.active_entry(),
3465 project
3466 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
3467 .map(|e| e.id)
3468 );
3469 });
3470
3471 // Close the active item
3472 pane.update(cx, |pane, cx| {
3473 pane.close_active_item(&Default::default(), cx).unwrap()
3474 })
3475 .await
3476 .unwrap();
3477 assert_eq!(
3478 cx.current_window_title(window_id).as_deref(),
3479 Some("one.txt β root1")
3480 );
3481 project.read_with(cx, |project, cx| {
3482 assert_eq!(
3483 project.active_entry(),
3484 project
3485 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
3486 .map(|e| e.id)
3487 );
3488 });
3489
3490 // Add a project folder
3491 project
3492 .update(cx, |project, cx| {
3493 project.find_or_create_local_worktree("/root2", true, cx)
3494 })
3495 .await
3496 .unwrap();
3497 assert_eq!(
3498 cx.current_window_title(window_id).as_deref(),
3499 Some("one.txt β root1, root2")
3500 );
3501
3502 // Remove a project folder
3503 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
3504 assert_eq!(
3505 cx.current_window_title(window_id).as_deref(),
3506 Some("one.txt β root2")
3507 );
3508 }
3509
3510 #[gpui::test]
3511 async fn test_close_window(cx: &mut TestAppContext) {
3512 init_test(cx);
3513
3514 let fs = FakeFs::new(cx.background());
3515 fs.insert_tree("/root", json!({ "one": "" })).await;
3516
3517 let project = Project::test(fs, ["root".as_ref()], cx).await;
3518 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3519
3520 // When there are no dirty items, there's nothing to do.
3521 let item1 = cx.add_view(window_id, |_| TestItem::new());
3522 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
3523 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
3524 assert!(task.await.unwrap());
3525
3526 // When there are dirty untitled items, prompt to save each one. If the user
3527 // cancels any prompt, then abort.
3528 let item2 = cx.add_view(window_id, |_| TestItem::new().with_dirty(true));
3529 let item3 = cx.add_view(window_id, |cx| {
3530 TestItem::new()
3531 .with_dirty(true)
3532 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3533 });
3534 workspace.update(cx, |w, cx| {
3535 w.add_item(Box::new(item2.clone()), cx);
3536 w.add_item(Box::new(item3.clone()), cx);
3537 });
3538 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
3539 cx.foreground().run_until_parked();
3540 cx.simulate_prompt_answer(window_id, 2 /* cancel */);
3541 cx.foreground().run_until_parked();
3542 assert!(!cx.has_pending_prompt(window_id));
3543 assert!(!task.await.unwrap());
3544 }
3545
3546 #[gpui::test]
3547 async fn test_close_pane_items(cx: &mut TestAppContext) {
3548 init_test(cx);
3549
3550 let fs = FakeFs::new(cx.background());
3551
3552 let project = Project::test(fs, None, cx).await;
3553 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3554
3555 let item1 = cx.add_view(window_id, |cx| {
3556 TestItem::new()
3557 .with_dirty(true)
3558 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3559 });
3560 let item2 = cx.add_view(window_id, |cx| {
3561 TestItem::new()
3562 .with_dirty(true)
3563 .with_conflict(true)
3564 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
3565 });
3566 let item3 = cx.add_view(window_id, |cx| {
3567 TestItem::new()
3568 .with_dirty(true)
3569 .with_conflict(true)
3570 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
3571 });
3572 let item4 = cx.add_view(window_id, |cx| {
3573 TestItem::new()
3574 .with_dirty(true)
3575 .with_project_items(&[TestProjectItem::new_untitled(cx)])
3576 });
3577 let pane = workspace.update(cx, |workspace, cx| {
3578 workspace.add_item(Box::new(item1.clone()), cx);
3579 workspace.add_item(Box::new(item2.clone()), cx);
3580 workspace.add_item(Box::new(item3.clone()), cx);
3581 workspace.add_item(Box::new(item4.clone()), cx);
3582 workspace.active_pane().clone()
3583 });
3584
3585 let close_items = pane.update(cx, |pane, cx| {
3586 pane.activate_item(1, true, true, cx);
3587 assert_eq!(pane.active_item().unwrap().id(), item2.id());
3588 let item1_id = item1.id();
3589 let item3_id = item3.id();
3590 let item4_id = item4.id();
3591 pane.close_items(cx, move |id| [item1_id, item3_id, item4_id].contains(&id))
3592 });
3593 cx.foreground().run_until_parked();
3594
3595 // There's a prompt to save item 1.
3596 pane.read_with(cx, |pane, _| {
3597 assert_eq!(pane.items_len(), 4);
3598 assert_eq!(pane.active_item().unwrap().id(), item1.id());
3599 });
3600 assert!(cx.has_pending_prompt(window_id));
3601
3602 // Confirm saving item 1.
3603 cx.simulate_prompt_answer(window_id, 0);
3604 cx.foreground().run_until_parked();
3605
3606 // Item 1 is saved. There's a prompt to save item 3.
3607 pane.read_with(cx, |pane, cx| {
3608 assert_eq!(item1.read(cx).save_count, 1);
3609 assert_eq!(item1.read(cx).save_as_count, 0);
3610 assert_eq!(item1.read(cx).reload_count, 0);
3611 assert_eq!(pane.items_len(), 3);
3612 assert_eq!(pane.active_item().unwrap().id(), item3.id());
3613 });
3614 assert!(cx.has_pending_prompt(window_id));
3615
3616 // Cancel saving item 3.
3617 cx.simulate_prompt_answer(window_id, 1);
3618 cx.foreground().run_until_parked();
3619
3620 // Item 3 is reloaded. There's a prompt to save item 4.
3621 pane.read_with(cx, |pane, cx| {
3622 assert_eq!(item3.read(cx).save_count, 0);
3623 assert_eq!(item3.read(cx).save_as_count, 0);
3624 assert_eq!(item3.read(cx).reload_count, 1);
3625 assert_eq!(pane.items_len(), 2);
3626 assert_eq!(pane.active_item().unwrap().id(), item4.id());
3627 });
3628 assert!(cx.has_pending_prompt(window_id));
3629
3630 // Confirm saving item 4.
3631 cx.simulate_prompt_answer(window_id, 0);
3632 cx.foreground().run_until_parked();
3633
3634 // There's a prompt for a path for item 4.
3635 cx.simulate_new_path_selection(|_| Some(Default::default()));
3636 close_items.await.unwrap();
3637
3638 // The requested items are closed.
3639 pane.read_with(cx, |pane, cx| {
3640 assert_eq!(item4.read(cx).save_count, 0);
3641 assert_eq!(item4.read(cx).save_as_count, 1);
3642 assert_eq!(item4.read(cx).reload_count, 0);
3643 assert_eq!(pane.items_len(), 1);
3644 assert_eq!(pane.active_item().unwrap().id(), item2.id());
3645 });
3646 }
3647
3648 #[gpui::test]
3649 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
3650 init_test(cx);
3651
3652 let fs = FakeFs::new(cx.background());
3653
3654 let project = Project::test(fs, [], cx).await;
3655 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3656
3657 // Create several workspace items with single project entries, and two
3658 // workspace items with multiple project entries.
3659 let single_entry_items = (0..=4)
3660 .map(|project_entry_id| {
3661 cx.add_view(window_id, |cx| {
3662 TestItem::new()
3663 .with_dirty(true)
3664 .with_project_items(&[TestProjectItem::new(
3665 project_entry_id,
3666 &format!("{project_entry_id}.txt"),
3667 cx,
3668 )])
3669 })
3670 })
3671 .collect::<Vec<_>>();
3672 let item_2_3 = cx.add_view(window_id, |cx| {
3673 TestItem::new()
3674 .with_dirty(true)
3675 .with_singleton(false)
3676 .with_project_items(&[
3677 single_entry_items[2].read(cx).project_items[0].clone(),
3678 single_entry_items[3].read(cx).project_items[0].clone(),
3679 ])
3680 });
3681 let item_3_4 = cx.add_view(window_id, |cx| {
3682 TestItem::new()
3683 .with_dirty(true)
3684 .with_singleton(false)
3685 .with_project_items(&[
3686 single_entry_items[3].read(cx).project_items[0].clone(),
3687 single_entry_items[4].read(cx).project_items[0].clone(),
3688 ])
3689 });
3690
3691 // Create two panes that contain the following project entries:
3692 // left pane:
3693 // multi-entry items: (2, 3)
3694 // single-entry items: 0, 1, 2, 3, 4
3695 // right pane:
3696 // single-entry items: 1
3697 // multi-entry items: (3, 4)
3698 let left_pane = workspace.update(cx, |workspace, cx| {
3699 let left_pane = workspace.active_pane().clone();
3700 workspace.add_item(Box::new(item_2_3.clone()), cx);
3701 for item in single_entry_items {
3702 workspace.add_item(Box::new(item), cx);
3703 }
3704 left_pane.update(cx, |pane, cx| {
3705 pane.activate_item(2, true, true, cx);
3706 });
3707
3708 workspace
3709 .split_pane(left_pane.clone(), SplitDirection::Right, cx)
3710 .unwrap();
3711
3712 left_pane
3713 });
3714
3715 //Need to cause an effect flush in order to respect new focus
3716 workspace.update(cx, |workspace, cx| {
3717 workspace.add_item(Box::new(item_3_4.clone()), cx);
3718 cx.focus(&left_pane);
3719 });
3720
3721 // When closing all of the items in the left pane, we should be prompted twice:
3722 // once for project entry 0, and once for project entry 2. After those two
3723 // prompts, the task should complete.
3724
3725 let close = left_pane.update(cx, |pane, cx| pane.close_items(cx, |_| true));
3726 cx.foreground().run_until_parked();
3727 left_pane.read_with(cx, |pane, cx| {
3728 assert_eq!(
3729 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
3730 &[ProjectEntryId::from_proto(0)]
3731 );
3732 });
3733 cx.simulate_prompt_answer(window_id, 0);
3734
3735 cx.foreground().run_until_parked();
3736 left_pane.read_with(cx, |pane, cx| {
3737 assert_eq!(
3738 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
3739 &[ProjectEntryId::from_proto(2)]
3740 );
3741 });
3742 cx.simulate_prompt_answer(window_id, 0);
3743
3744 cx.foreground().run_until_parked();
3745 close.await.unwrap();
3746 left_pane.read_with(cx, |pane, _| {
3747 assert_eq!(pane.items_len(), 0);
3748 });
3749 }
3750
3751 #[gpui::test]
3752 async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
3753 init_test(cx);
3754
3755 let fs = FakeFs::new(cx.background());
3756
3757 let project = Project::test(fs, [], cx).await;
3758 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3759 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3760
3761 let item = cx.add_view(window_id, |cx| {
3762 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3763 });
3764 let item_id = item.id();
3765 workspace.update(cx, |workspace, cx| {
3766 workspace.add_item(Box::new(item.clone()), cx);
3767 });
3768
3769 // Autosave on window change.
3770 item.update(cx, |item, cx| {
3771 cx.update_global(|settings: &mut SettingsStore, cx| {
3772 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
3773 settings.autosave = Some(AutosaveSetting::OnWindowChange);
3774 })
3775 });
3776 item.is_dirty = true;
3777 });
3778
3779 // Deactivating the window saves the file.
3780 cx.simulate_window_activation(None);
3781 deterministic.run_until_parked();
3782 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
3783
3784 // Autosave on focus change.
3785 item.update(cx, |item, cx| {
3786 cx.focus_self();
3787 cx.update_global(|settings: &mut SettingsStore, cx| {
3788 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
3789 settings.autosave = Some(AutosaveSetting::OnFocusChange);
3790 })
3791 });
3792 item.is_dirty = true;
3793 });
3794
3795 // Blurring the item saves the file.
3796 item.update(cx, |_, cx| cx.blur());
3797 deterministic.run_until_parked();
3798 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
3799
3800 // Deactivating the window still saves the file.
3801 cx.simulate_window_activation(Some(window_id));
3802 item.update(cx, |item, cx| {
3803 cx.focus_self();
3804 item.is_dirty = true;
3805 });
3806 cx.simulate_window_activation(None);
3807
3808 deterministic.run_until_parked();
3809 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
3810
3811 // Autosave after delay.
3812 item.update(cx, |item, cx| {
3813 cx.update_global(|settings: &mut SettingsStore, cx| {
3814 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
3815 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
3816 })
3817 });
3818 item.is_dirty = true;
3819 cx.emit(TestItemEvent::Edit);
3820 });
3821
3822 // Delay hasn't fully expired, so the file is still dirty and unsaved.
3823 deterministic.advance_clock(Duration::from_millis(250));
3824 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
3825
3826 // After delay expires, the file is saved.
3827 deterministic.advance_clock(Duration::from_millis(250));
3828 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
3829
3830 // Autosave on focus change, ensuring closing the tab counts as such.
3831 item.update(cx, |item, cx| {
3832 cx.update_global(|settings: &mut SettingsStore, cx| {
3833 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
3834 settings.autosave = Some(AutosaveSetting::OnFocusChange);
3835 })
3836 });
3837 item.is_dirty = true;
3838 });
3839
3840 pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id))
3841 .await
3842 .unwrap();
3843 assert!(!cx.has_pending_prompt(window_id));
3844 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3845
3846 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
3847 workspace.update(cx, |workspace, cx| {
3848 workspace.add_item(Box::new(item.clone()), cx);
3849 });
3850 item.update(cx, |item, cx| {
3851 item.project_items[0].update(cx, |item, _| {
3852 item.entry_id = None;
3853 });
3854 item.is_dirty = true;
3855 cx.blur();
3856 });
3857 deterministic.run_until_parked();
3858 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3859
3860 // Ensure autosave is prevented for deleted files also when closing the buffer.
3861 let _close_items =
3862 pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id));
3863 deterministic.run_until_parked();
3864 assert!(cx.has_pending_prompt(window_id));
3865 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3866 }
3867
3868 #[gpui::test]
3869 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
3870 init_test(cx);
3871
3872 let fs = FakeFs::new(cx.background());
3873
3874 let project = Project::test(fs, [], cx).await;
3875 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3876
3877 let item = cx.add_view(window_id, |cx| {
3878 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3879 });
3880 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3881 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
3882 let toolbar_notify_count = Rc::new(RefCell::new(0));
3883
3884 workspace.update(cx, |workspace, cx| {
3885 workspace.add_item(Box::new(item.clone()), cx);
3886 let toolbar_notification_count = toolbar_notify_count.clone();
3887 cx.observe(&toolbar, move |_, _, _| {
3888 *toolbar_notification_count.borrow_mut() += 1
3889 })
3890 .detach();
3891 });
3892
3893 pane.read_with(cx, |pane, _| {
3894 assert!(!pane.can_navigate_backward());
3895 assert!(!pane.can_navigate_forward());
3896 });
3897
3898 item.update(cx, |item, cx| {
3899 item.set_state("one".to_string(), cx);
3900 });
3901
3902 // Toolbar must be notified to re-render the navigation buttons
3903 assert_eq!(*toolbar_notify_count.borrow(), 1);
3904
3905 pane.read_with(cx, |pane, _| {
3906 assert!(pane.can_navigate_backward());
3907 assert!(!pane.can_navigate_forward());
3908 });
3909
3910 workspace
3911 .update(cx, |workspace, cx| {
3912 Pane::go_back(workspace, Some(pane.downgrade()), cx)
3913 })
3914 .await
3915 .unwrap();
3916
3917 assert_eq!(*toolbar_notify_count.borrow(), 3);
3918 pane.read_with(cx, |pane, _| {
3919 assert!(!pane.can_navigate_backward());
3920 assert!(pane.can_navigate_forward());
3921 });
3922 }
3923
3924 #[gpui::test]
3925 async fn test_panels(cx: &mut gpui::TestAppContext) {
3926 init_test(cx);
3927 let fs = FakeFs::new(cx.background());
3928
3929 let project = Project::test(fs, [], cx).await;
3930 let (_window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3931
3932 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
3933 // Add panel_1 on the left, panel_2 on the right.
3934 let panel_1 = cx.add_view(|_| TestPanel::new(DockPosition::Left));
3935 workspace.add_panel(panel_1.clone(), cx);
3936 workspace
3937 .left_dock()
3938 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
3939 let panel_2 = cx.add_view(|_| TestPanel::new(DockPosition::Right));
3940 workspace.add_panel(panel_2.clone(), cx);
3941 workspace
3942 .right_dock()
3943 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
3944
3945 let left_dock = workspace.left_dock();
3946 assert_eq!(
3947 left_dock.read(cx).active_panel().unwrap().id(),
3948 panel_1.id()
3949 );
3950 assert_eq!(
3951 left_dock.read(cx).active_panel_size(cx).unwrap(),
3952 panel_1.size(cx)
3953 );
3954
3955 left_dock.update(cx, |left_dock, cx| left_dock.resize_active_panel(1337., cx));
3956 assert_eq!(
3957 workspace.right_dock().read(cx).active_panel().unwrap().id(),
3958 panel_2.id()
3959 );
3960
3961 (panel_1, panel_2)
3962 });
3963
3964 // Move panel_1 to the right
3965 panel_1.update(cx, |panel_1, cx| {
3966 panel_1.set_position(DockPosition::Right, cx)
3967 });
3968
3969 workspace.update(cx, |workspace, cx| {
3970 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
3971 // Since it was the only panel on the left, the left dock should now be closed.
3972 assert!(!workspace.left_dock().read(cx).is_open());
3973 assert!(workspace.left_dock().read(cx).active_panel().is_none());
3974 let right_dock = workspace.right_dock();
3975 assert_eq!(
3976 right_dock.read(cx).active_panel().unwrap().id(),
3977 panel_1.id()
3978 );
3979 assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
3980
3981 // Now we move panel_2Β to the left
3982 panel_2.set_position(DockPosition::Left, cx);
3983 });
3984
3985 workspace.update(cx, |workspace, cx| {
3986 // Since panel_2 was not visible on the right, we don't open the left dock.
3987 assert!(!workspace.left_dock().read(cx).is_open());
3988 // And the right dock is unaffected in it's displaying of panel_1
3989 assert!(workspace.right_dock().read(cx).is_open());
3990 assert_eq!(
3991 workspace.right_dock().read(cx).active_panel().unwrap().id(),
3992 panel_1.id()
3993 );
3994 });
3995
3996 // Move panel_1 back to the left
3997 panel_1.update(cx, |panel_1, cx| {
3998 panel_1.set_position(DockPosition::Left, cx)
3999 });
4000
4001 workspace.update(cx, |workspace, cx| {
4002 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
4003 let left_dock = workspace.left_dock();
4004 assert!(left_dock.read(cx).is_open());
4005 assert_eq!(
4006 left_dock.read(cx).active_panel().unwrap().id(),
4007 panel_1.id()
4008 );
4009 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
4010 // And right the dock should be closed as it no longer has any panels.
4011 assert!(!workspace.right_dock().read(cx).is_open());
4012
4013 // Now we move panel_1 to the bottom
4014 panel_1.set_position(DockPosition::Bottom, cx);
4015 });
4016
4017 workspace.update(cx, |workspace, cx| {
4018 // Since panel_1 was visible on the left, we close the left dock.
4019 assert!(!workspace.left_dock().read(cx).is_open());
4020 // The bottom dock is sized based on the panel's default size,
4021 // since the panel orientation changed from vertical to horizontal.
4022 let bottom_dock = workspace.bottom_dock();
4023 assert_eq!(
4024 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
4025 panel_1.size(cx),
4026 );
4027 // Close bottom dock and move panel_1 back to the left.
4028 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
4029 panel_1.set_position(DockPosition::Left, cx);
4030 });
4031
4032 // Emit activated event on panel 1
4033 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Activated));
4034
4035 // Now the left dock is open and panel_1 is active and focused.
4036 workspace.read_with(cx, |workspace, cx| {
4037 let left_dock = workspace.left_dock();
4038 assert!(left_dock.read(cx).is_open());
4039 assert_eq!(
4040 left_dock.read(cx).active_panel().unwrap().id(),
4041 panel_1.id()
4042 );
4043 assert!(panel_1.is_focused(cx));
4044 });
4045
4046 // Emit closed event on panel 2, which is not active
4047 panel_2.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
4048
4049 // Wo don't close the left dock, because panel_2 wasn't the active panel
4050 workspace.read_with(cx, |workspace, cx| {
4051 let left_dock = workspace.left_dock();
4052 assert!(left_dock.read(cx).is_open());
4053 assert_eq!(
4054 left_dock.read(cx).active_panel().unwrap().id(),
4055 panel_1.id()
4056 );
4057 });
4058
4059 // Emitting a ZoomIn event shows the panel as zoomed.
4060 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomIn));
4061 workspace.read_with(cx, |workspace, cx| {
4062 assert_eq!(workspace.zoomed(cx), Some(panel_1.clone().into_any()));
4063 });
4064
4065 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
4066 workspace.update(cx, |_, cx| cx.focus_self());
4067 workspace.read_with(cx, |workspace, cx| {
4068 assert_eq!(workspace.zoomed(cx), None);
4069 });
4070
4071 // When focus is transferred back to the panel, it is zoomed again.
4072 panel_1.update(cx, |_, cx| cx.focus_self());
4073 workspace.read_with(cx, |workspace, cx| {
4074 assert_eq!(workspace.zoomed(cx), Some(panel_1.clone().into_any()));
4075 });
4076
4077 // Emitting a ZoomOut event unzooms the panel.
4078 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomOut));
4079 workspace.read_with(cx, |workspace, cx| {
4080 assert_eq!(workspace.zoomed(cx), None);
4081 });
4082
4083 // Emit closed event on panel 1, which is active
4084 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
4085
4086 // Now the left dock is closed, because panel_1 was the active panel
4087 workspace.read_with(cx, |workspace, cx| {
4088 let left_dock = workspace.left_dock();
4089 assert!(!left_dock.read(cx).is_open());
4090 });
4091 }
4092
4093 pub fn init_test(cx: &mut TestAppContext) {
4094 cx.foreground().forbid_parking();
4095 cx.update(|cx| {
4096 cx.set_global(SettingsStore::test(cx));
4097 theme::init((), cx);
4098 language::init(cx);
4099 crate::init_settings(cx);
4100 });
4101 }
4102}