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