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