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