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