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