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