1pub mod dock;
2pub mod item;
3mod modal_layer;
4pub mod notifications;
5pub mod pane;
6pub mod pane_group;
7mod persistence;
8pub mod searchable;
9pub mod shared_screen;
10mod status_bar;
11pub mod tasks;
12mod toolbar;
13mod workspace_settings;
14
15use anyhow::{anyhow, Context as _, Result};
16use call::{call_settings::CallSettings, ActiveCall};
17use client::{
18 proto::{self, ErrorCode, PanelId, PeerId},
19 ChannelId, Client, DevServerProjectId, ErrorExt, ProjectId, Status, TypedEnvelope, UserStore,
20};
21use collections::{hash_map, HashMap, HashSet};
22use derive_more::{Deref, DerefMut};
23use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
24use futures::{
25 channel::{
26 mpsc::{self, UnboundedReceiver, UnboundedSender},
27 oneshot,
28 },
29 future::try_join_all,
30 Future, FutureExt, StreamExt,
31};
32use gpui::{
33 action_as, actions, canvas, impl_action_as, impl_actions, point, relative, size,
34 transparent_black, Action, AnyElement, AnyView, AnyWeakView, AppContext, AsyncAppContext,
35 AsyncWindowContext, Bounds, CursorStyle, Decorations, DragMoveEvent, Entity as _, EntityId,
36 EventEmitter, Flatten, FocusHandle, FocusableView, Global, Hsla, KeyContext, Keystroke,
37 ManagedView, Model, ModelContext, MouseButton, PathPromptOptions, Point, PromptLevel, Render,
38 ResizeEdge, Size, Stateful, Subscription, Task, Tiling, View, WeakView, WindowBounds,
39 WindowHandle, WindowId, WindowOptions,
40};
41pub use item::{
42 FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
43 ProjectItem, SerializableItem, SerializableItemHandle, WeakItemHandle,
44};
45use itertools::Itertools;
46use language::{LanguageRegistry, Rope};
47pub use modal_layer::*;
48use node_runtime::NodeRuntime;
49use notifications::{simple_message_notification::MessageNotification, NotificationHandle};
50pub use pane::*;
51pub use pane_group::*;
52pub use persistence::{
53 model::{ItemId, LocalPaths, SerializedDevServerProject, SerializedWorkspaceLocation},
54 WorkspaceDb, DB as WORKSPACE_DB,
55};
56use persistence::{
57 model::{SerializedSshProject, SerializedWorkspace},
58 SerializedWindowBounds, DB,
59};
60use postage::stream::Stream;
61use project::{
62 DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId,
63};
64use remote::{SshConnectionOptions, SshSession};
65use serde::Deserialize;
66use session::AppSession;
67use settings::{InvalidSettingsError, Settings};
68use shared_screen::SharedScreen;
69use sqlez::{
70 bindable::{Bind, Column, StaticColumnCount},
71 statement::Statement,
72};
73use status_bar::StatusBar;
74pub use status_bar::StatusItemView;
75use std::{
76 any::TypeId,
77 borrow::Cow,
78 cell::RefCell,
79 cmp,
80 collections::hash_map::DefaultHasher,
81 env,
82 hash::{Hash, Hasher},
83 path::{Path, PathBuf},
84 rc::Rc,
85 sync::{atomic::AtomicUsize, Arc, LazyLock, Weak},
86 time::Duration,
87};
88use task::SpawnInTerminal;
89use theme::{ActiveTheme, SystemAppearance, ThemeSettings};
90pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
91pub use ui;
92use ui::{
93 div, h_flex, px, BorrowAppContext, Context as _, Div, FluentBuilder, InteractiveElement as _,
94 IntoElement, ParentElement as _, Pixels, SharedString, Styled as _, ViewContext,
95 VisualContext as _, WindowContext,
96};
97use util::{maybe, ResultExt, TryFutureExt};
98use uuid::Uuid;
99pub use workspace_settings::{
100 AutosaveSetting, RestoreOnStartupBehavior, TabBarSettings, WorkspaceSettings,
101};
102
103use crate::notifications::NotificationId;
104use crate::persistence::{
105 model::{DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup},
106 SerializedAxis,
107};
108
109static ZED_WINDOW_SIZE: LazyLock<Option<Size<Pixels>>> = LazyLock::new(|| {
110 env::var("ZED_WINDOW_SIZE")
111 .ok()
112 .as_deref()
113 .and_then(parse_pixel_size_env_var)
114});
115
116static ZED_WINDOW_POSITION: LazyLock<Option<Point<Pixels>>> = LazyLock::new(|| {
117 env::var("ZED_WINDOW_POSITION")
118 .ok()
119 .as_deref()
120 .and_then(parse_pixel_position_env_var)
121});
122
123#[derive(Clone, PartialEq)]
124pub struct RemoveWorktreeFromProject(pub WorktreeId);
125
126actions!(assistant, [ShowConfiguration]);
127
128actions!(
129 workspace,
130 [
131 ActivateNextPane,
132 ActivatePreviousPane,
133 AddFolderToProject,
134 ClearAllNotifications,
135 CloseAllDocks,
136 CloseWindow,
137 CopyPath,
138 CopyRelativePath,
139 Feedback,
140 FollowNextCollaborator,
141 NewCenterTerminal,
142 NewFile,
143 NewFileSplitVertical,
144 NewFileSplitHorizontal,
145 NewSearch,
146 NewTerminal,
147 NewWindow,
148 Open,
149 OpenInTerminal,
150 ReloadActiveItem,
151 SaveAs,
152 SaveWithoutFormat,
153 ToggleBottomDock,
154 ToggleCenteredLayout,
155 ToggleLeftDock,
156 ToggleRightDock,
157 ToggleZoom,
158 Unfollow,
159 Welcome,
160 ]
161);
162
163#[derive(Clone, PartialEq)]
164pub struct OpenPaths {
165 pub paths: Vec<PathBuf>,
166}
167
168#[derive(Clone, Deserialize, PartialEq)]
169pub struct ActivatePane(pub usize);
170
171#[derive(Clone, Deserialize, PartialEq)]
172pub struct ActivatePaneInDirection(pub SplitDirection);
173
174#[derive(Clone, Deserialize, PartialEq)]
175pub struct SwapPaneInDirection(pub SplitDirection);
176
177#[derive(Clone, PartialEq, Debug, Deserialize)]
178#[serde(rename_all = "camelCase")]
179pub struct SaveAll {
180 pub save_intent: Option<SaveIntent>,
181}
182
183#[derive(Clone, PartialEq, Debug, Deserialize)]
184#[serde(rename_all = "camelCase")]
185pub struct Save {
186 pub save_intent: Option<SaveIntent>,
187}
188
189#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
190#[serde(rename_all = "camelCase")]
191pub struct CloseAllItemsAndPanes {
192 pub save_intent: Option<SaveIntent>,
193}
194
195#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
196#[serde(rename_all = "camelCase")]
197pub struct CloseInactiveTabsAndPanes {
198 pub save_intent: Option<SaveIntent>,
199}
200
201#[derive(Clone, Deserialize, PartialEq)]
202pub struct SendKeystrokes(pub String);
203
204#[derive(Clone, Deserialize, PartialEq, Default)]
205pub struct Reload {
206 pub binary_path: Option<PathBuf>,
207}
208
209action_as!(project_symbols, ToggleProjectSymbols as Toggle);
210
211#[derive(Default, PartialEq, Eq, Clone, serde::Deserialize)]
212pub struct ToggleFileFinder {
213 #[serde(default)]
214 pub separate_history: bool,
215}
216
217impl_action_as!(file_finder, ToggleFileFinder as Toggle);
218
219impl_actions!(
220 workspace,
221 [
222 ActivatePane,
223 ActivatePaneInDirection,
224 CloseAllItemsAndPanes,
225 CloseInactiveTabsAndPanes,
226 OpenTerminal,
227 Reload,
228 Save,
229 SaveAll,
230 SwapPaneInDirection,
231 SendKeystrokes,
232 ]
233);
234
235#[derive(PartialEq, Eq, Debug)]
236pub enum CloseIntent {
237 /// Quit the program entirely.
238 Quit,
239 /// Close a window.
240 CloseWindow,
241 /// Replace the workspace in an existing window.
242 ReplaceWindow,
243}
244
245#[derive(Clone)]
246pub struct Toast {
247 id: NotificationId,
248 msg: Cow<'static, str>,
249 autohide: bool,
250 on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut WindowContext)>)>,
251}
252
253impl Toast {
254 pub fn new<I: Into<Cow<'static, str>>>(id: NotificationId, msg: I) -> Self {
255 Toast {
256 id,
257 msg: msg.into(),
258 on_click: None,
259 autohide: false,
260 }
261 }
262
263 pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
264 where
265 M: Into<Cow<'static, str>>,
266 F: Fn(&mut WindowContext) + 'static,
267 {
268 self.on_click = Some((message.into(), Arc::new(on_click)));
269 self
270 }
271
272 pub fn autohide(mut self) -> Self {
273 self.autohide = true;
274 self
275 }
276}
277
278impl PartialEq for Toast {
279 fn eq(&self, other: &Self) -> bool {
280 self.id == other.id
281 && self.msg == other.msg
282 && self.on_click.is_some() == other.on_click.is_some()
283 }
284}
285
286#[derive(Debug, Default, Clone, Deserialize, PartialEq)]
287pub struct OpenTerminal {
288 pub working_directory: PathBuf,
289}
290
291#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
292pub struct WorkspaceId(i64);
293
294impl StaticColumnCount for WorkspaceId {}
295impl Bind for WorkspaceId {
296 fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
297 self.0.bind(statement, start_index)
298 }
299}
300impl Column for WorkspaceId {
301 fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
302 i64::column(statement, start_index)
303 .map(|(i, next_index)| (Self(i), next_index))
304 .with_context(|| format!("Failed to read WorkspaceId at index {start_index}"))
305 }
306}
307impl From<WorkspaceId> for i64 {
308 fn from(val: WorkspaceId) -> Self {
309 val.0
310 }
311}
312
313pub fn init_settings(cx: &mut AppContext) {
314 WorkspaceSettings::register(cx);
315 ItemSettings::register(cx);
316 PreviewTabsSettings::register(cx);
317 TabBarSettings::register(cx);
318}
319
320pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
321 init_settings(cx);
322 notifications::init(cx);
323
324 cx.on_action(Workspace::close_global);
325 cx.on_action(reload);
326
327 cx.on_action({
328 let app_state = Arc::downgrade(&app_state);
329 move |_: &Open, cx: &mut AppContext| {
330 let paths = cx.prompt_for_paths(PathPromptOptions {
331 files: true,
332 directories: true,
333 multiple: true,
334 });
335
336 if let Some(app_state) = app_state.upgrade() {
337 cx.spawn(move |cx| async move {
338 match Flatten::flatten(paths.await.map_err(|e| e.into())) {
339 Ok(Some(paths)) => {
340 cx.update(|cx| {
341 open_paths(&paths, app_state, OpenOptions::default(), cx)
342 .detach_and_log_err(cx)
343 })
344 .ok();
345 }
346 Ok(None) => {}
347 Err(err) => {
348 cx.update(|cx| {
349 if let Some(workspace_window) = cx
350 .active_window()
351 .and_then(|window| window.downcast::<Workspace>())
352 {
353 workspace_window
354 .update(cx, |workspace, cx| {
355 workspace.show_portal_error(err.to_string(), cx);
356 })
357 .ok();
358 }
359 })
360 .ok();
361 }
362 };
363 })
364 .detach();
365 }
366 }
367 });
368}
369
370#[derive(Clone, Default, Deref, DerefMut)]
371struct ProjectItemOpeners(Vec<ProjectItemOpener>);
372
373type ProjectItemOpener = fn(
374 &Model<Project>,
375 &ProjectPath,
376 &mut WindowContext,
377)
378 -> Option<Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>>>;
379
380type WorkspaceItemBuilder = Box<dyn FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>>;
381
382impl Global for ProjectItemOpeners {}
383
384/// Registers a [ProjectItem] for the app. When opening a file, all the registered
385/// items will get a chance to open the file, starting from the project item that
386/// was added last.
387pub fn register_project_item<I: ProjectItem>(cx: &mut AppContext) {
388 let builders = cx.default_global::<ProjectItemOpeners>();
389 builders.push(|project, project_path, cx| {
390 let project_item = <I::Item as project::Item>::try_open(project, project_path, cx)?;
391 let project = project.clone();
392 Some(cx.spawn(|cx| async move {
393 let project_item = project_item.await?;
394 let project_entry_id: Option<ProjectEntryId> =
395 project_item.read_with(&cx, project::Item::entry_id)?;
396 let build_workspace_item = Box::new(|cx: &mut ViewContext<Pane>| {
397 Box::new(cx.new_view(|cx| I::for_project_item(project, project_item, cx)))
398 as Box<dyn ItemHandle>
399 }) as Box<_>;
400 Ok((project_entry_id, build_workspace_item))
401 }))
402 });
403}
404
405#[derive(Default)]
406pub struct FollowableViewRegistry(HashMap<TypeId, FollowableViewDescriptor>);
407
408struct FollowableViewDescriptor {
409 from_state_proto: fn(
410 View<Workspace>,
411 ViewId,
412 &mut Option<proto::view::Variant>,
413 &mut WindowContext,
414 ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>,
415 to_followable_view: fn(&AnyView) -> Box<dyn FollowableItemHandle>,
416}
417
418impl Global for FollowableViewRegistry {}
419
420impl FollowableViewRegistry {
421 pub fn register<I: FollowableItem>(cx: &mut AppContext) {
422 cx.default_global::<Self>().0.insert(
423 TypeId::of::<I>(),
424 FollowableViewDescriptor {
425 from_state_proto: |workspace, id, state, cx| {
426 I::from_state_proto(workspace, id, state, cx).map(|task| {
427 cx.foreground_executor()
428 .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
429 })
430 },
431 to_followable_view: |view| Box::new(view.clone().downcast::<I>().unwrap()),
432 },
433 );
434 }
435
436 pub fn from_state_proto(
437 workspace: View<Workspace>,
438 view_id: ViewId,
439 mut state: Option<proto::view::Variant>,
440 cx: &mut WindowContext,
441 ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>> {
442 cx.update_default_global(|this: &mut Self, cx| {
443 this.0.values().find_map(|descriptor| {
444 (descriptor.from_state_proto)(workspace.clone(), view_id, &mut state, cx)
445 })
446 })
447 }
448
449 pub fn to_followable_view(
450 view: impl Into<AnyView>,
451 cx: &AppContext,
452 ) -> Option<Box<dyn FollowableItemHandle>> {
453 let this = cx.try_global::<Self>()?;
454 let view = view.into();
455 let descriptor = this.0.get(&view.entity_type())?;
456 Some((descriptor.to_followable_view)(&view))
457 }
458}
459
460#[derive(Copy, Clone)]
461struct SerializableItemDescriptor {
462 deserialize: fn(
463 Model<Project>,
464 WeakView<Workspace>,
465 WorkspaceId,
466 ItemId,
467 &mut ViewContext<Pane>,
468 ) -> Task<Result<Box<dyn ItemHandle>>>,
469 cleanup: fn(WorkspaceId, Vec<ItemId>, &mut WindowContext) -> Task<Result<()>>,
470 view_to_serializable_item: fn(AnyView) -> Box<dyn SerializableItemHandle>,
471}
472
473#[derive(Default)]
474struct SerializableItemRegistry {
475 descriptors_by_kind: HashMap<Arc<str>, SerializableItemDescriptor>,
476 descriptors_by_type: HashMap<TypeId, SerializableItemDescriptor>,
477}
478
479impl Global for SerializableItemRegistry {}
480
481impl SerializableItemRegistry {
482 fn deserialize(
483 item_kind: &str,
484 project: Model<Project>,
485 workspace: WeakView<Workspace>,
486 workspace_id: WorkspaceId,
487 item_item: ItemId,
488 cx: &mut ViewContext<Pane>,
489 ) -> Task<Result<Box<dyn ItemHandle>>> {
490 let Some(descriptor) = Self::descriptor(item_kind, cx) else {
491 return Task::ready(Err(anyhow!(
492 "cannot deserialize {}, descriptor not found",
493 item_kind
494 )));
495 };
496
497 (descriptor.deserialize)(project, workspace, workspace_id, item_item, cx)
498 }
499
500 fn cleanup(
501 item_kind: &str,
502 workspace_id: WorkspaceId,
503 loaded_items: Vec<ItemId>,
504 cx: &mut WindowContext,
505 ) -> Task<Result<()>> {
506 let Some(descriptor) = Self::descriptor(item_kind, cx) else {
507 return Task::ready(Err(anyhow!(
508 "cannot cleanup {}, descriptor not found",
509 item_kind
510 )));
511 };
512
513 (descriptor.cleanup)(workspace_id, loaded_items, cx)
514 }
515
516 fn view_to_serializable_item_handle(
517 view: AnyView,
518 cx: &AppContext,
519 ) -> Option<Box<dyn SerializableItemHandle>> {
520 let this = cx.try_global::<Self>()?;
521 let descriptor = this.descriptors_by_type.get(&view.entity_type())?;
522 Some((descriptor.view_to_serializable_item)(view))
523 }
524
525 fn descriptor(item_kind: &str, cx: &AppContext) -> Option<SerializableItemDescriptor> {
526 let this = cx.try_global::<Self>()?;
527 this.descriptors_by_kind.get(item_kind).copied()
528 }
529}
530
531pub fn register_serializable_item<I: SerializableItem>(cx: &mut AppContext) {
532 let serialized_item_kind = I::serialized_item_kind();
533
534 let registry = cx.default_global::<SerializableItemRegistry>();
535 let descriptor = SerializableItemDescriptor {
536 deserialize: |project, workspace, workspace_id, item_id, cx| {
537 let task = I::deserialize(project, workspace, workspace_id, item_id, cx);
538 cx.foreground_executor()
539 .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
540 },
541 cleanup: |workspace_id, loaded_items, cx| I::cleanup(workspace_id, loaded_items, cx),
542 view_to_serializable_item: |view| Box::new(view.downcast::<I>().unwrap()),
543 };
544 registry
545 .descriptors_by_kind
546 .insert(Arc::from(serialized_item_kind), descriptor);
547 registry
548 .descriptors_by_type
549 .insert(TypeId::of::<I>(), descriptor);
550}
551
552pub struct AppState {
553 pub languages: Arc<LanguageRegistry>,
554 pub client: Arc<Client>,
555 pub user_store: Model<UserStore>,
556 pub workspace_store: Model<WorkspaceStore>,
557 pub fs: Arc<dyn fs::Fs>,
558 pub build_window_options: fn(Option<Uuid>, &mut AppContext) -> WindowOptions,
559 pub node_runtime: 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<SshSession>,
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 for path in paths {
5548 project
5549 .update(&mut cx, |project, cx| {
5550 project.find_or_create_worktree(&path, true, cx)
5551 })?
5552 .await?;
5553 }
5554
5555 let serialized_workspace =
5556 persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
5557
5558 let workspace_id = if let Some(workspace_id) =
5559 serialized_workspace.as_ref().map(|workspace| workspace.id)
5560 {
5561 workspace_id
5562 } else {
5563 persistence::DB.next_id().await?
5564 };
5565
5566 cx.update_window(window.into(), |_, cx| {
5567 cx.replace_root_view(|cx| {
5568 let mut workspace =
5569 Workspace::new(Some(workspace_id), project, app_state.clone(), cx);
5570 workspace.set_serialized_ssh_project(serialized_ssh_project);
5571 workspace
5572 });
5573 })?;
5574
5575 window
5576 .update(&mut cx, |_, cx| {
5577 cx.activate_window();
5578
5579 open_items(serialized_workspace, vec![], app_state, cx)
5580 })?
5581 .await?;
5582
5583 Ok(())
5584 })
5585}
5586
5587pub fn join_dev_server_project(
5588 dev_server_project_id: DevServerProjectId,
5589 project_id: ProjectId,
5590 app_state: Arc<AppState>,
5591 window_to_replace: Option<WindowHandle<Workspace>>,
5592 cx: &mut AppContext,
5593) -> Task<Result<WindowHandle<Workspace>>> {
5594 let windows = cx.windows();
5595 cx.spawn(|mut cx| async move {
5596 let existing_workspace = windows.into_iter().find_map(|window| {
5597 window.downcast::<Workspace>().and_then(|window| {
5598 window
5599 .update(&mut cx, |workspace, cx| {
5600 if workspace.project().read(cx).remote_id() == Some(project_id.0) {
5601 Some(window)
5602 } else {
5603 None
5604 }
5605 })
5606 .unwrap_or(None)
5607 })
5608 });
5609
5610 let serialized_workspace: Option<SerializedWorkspace> =
5611 persistence::DB.workspace_for_dev_server_project(dev_server_project_id);
5612
5613 let workspace = if let Some(existing_workspace) = existing_workspace {
5614 existing_workspace
5615 } else {
5616 let project = Project::remote(
5617 project_id.0,
5618 app_state.client.clone(),
5619 app_state.user_store.clone(),
5620 app_state.languages.clone(),
5621 app_state.fs.clone(),
5622 cx.clone(),
5623 )
5624 .await?;
5625
5626 let workspace_id = if let Some(ref serialized_workspace) = serialized_workspace {
5627 serialized_workspace.id
5628 } else {
5629 persistence::DB.next_id().await?
5630 };
5631
5632 if let Some(window_to_replace) = window_to_replace {
5633 cx.update_window(window_to_replace.into(), |_, cx| {
5634 cx.replace_root_view(|cx| {
5635 Workspace::new(Some(workspace_id), project, app_state.clone(), cx)
5636 });
5637 })?;
5638 window_to_replace
5639 } else {
5640 let window_bounds_override = window_bounds_env_override();
5641 cx.update(|cx| {
5642 let mut options = (app_state.build_window_options)(None, cx);
5643 options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
5644 cx.open_window(options, |cx| {
5645 cx.new_view(|cx| {
5646 Workspace::new(Some(workspace_id), project, app_state.clone(), cx)
5647 })
5648 })
5649 })??
5650 }
5651 };
5652
5653 workspace
5654 .update(&mut cx, |_, cx| {
5655 cx.activate(true);
5656 cx.activate_window();
5657 open_items(serialized_workspace, vec![], app_state, cx)
5658 })?
5659 .await?;
5660
5661 anyhow::Ok(workspace)
5662 })
5663}
5664
5665pub fn join_in_room_project(
5666 project_id: u64,
5667 follow_user_id: u64,
5668 app_state: Arc<AppState>,
5669 cx: &mut AppContext,
5670) -> Task<Result<()>> {
5671 let windows = cx.windows();
5672 cx.spawn(|mut cx| async move {
5673 let existing_workspace = windows.into_iter().find_map(|window| {
5674 window.downcast::<Workspace>().and_then(|window| {
5675 window
5676 .update(&mut cx, |workspace, cx| {
5677 if workspace.project().read(cx).remote_id() == Some(project_id) {
5678 Some(window)
5679 } else {
5680 None
5681 }
5682 })
5683 .unwrap_or(None)
5684 })
5685 });
5686
5687 let workspace = if let Some(existing_workspace) = existing_workspace {
5688 existing_workspace
5689 } else {
5690 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
5691 let room = active_call
5692 .read_with(&cx, |call, _| call.room().cloned())?
5693 .ok_or_else(|| anyhow!("not in a call"))?;
5694 let project = room
5695 .update(&mut cx, |room, cx| {
5696 room.join_project(
5697 project_id,
5698 app_state.languages.clone(),
5699 app_state.fs.clone(),
5700 cx,
5701 )
5702 })?
5703 .await?;
5704
5705 let window_bounds_override = window_bounds_env_override();
5706 cx.update(|cx| {
5707 let mut options = (app_state.build_window_options)(None, cx);
5708 options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
5709 cx.open_window(options, |cx| {
5710 cx.new_view(|cx| {
5711 Workspace::new(Default::default(), project, app_state.clone(), cx)
5712 })
5713 })
5714 })??
5715 };
5716
5717 workspace.update(&mut cx, |workspace, cx| {
5718 cx.activate(true);
5719 cx.activate_window();
5720
5721 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
5722 let follow_peer_id = room
5723 .read(cx)
5724 .remote_participants()
5725 .iter()
5726 .find(|(_, participant)| participant.user.id == follow_user_id)
5727 .map(|(_, p)| p.peer_id)
5728 .or_else(|| {
5729 // If we couldn't follow the given user, follow the host instead.
5730 let collaborator = workspace
5731 .project()
5732 .read(cx)
5733 .collaborators()
5734 .values()
5735 .find(|collaborator| collaborator.replica_id == 0)?;
5736 Some(collaborator.peer_id)
5737 });
5738
5739 if let Some(follow_peer_id) = follow_peer_id {
5740 workspace.follow(follow_peer_id, cx);
5741 }
5742 }
5743 })?;
5744
5745 anyhow::Ok(())
5746 })
5747}
5748
5749pub fn reload(reload: &Reload, cx: &mut AppContext) {
5750 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
5751 let mut workspace_windows = cx
5752 .windows()
5753 .into_iter()
5754 .filter_map(|window| window.downcast::<Workspace>())
5755 .collect::<Vec<_>>();
5756
5757 // If multiple windows have unsaved changes, and need a save prompt,
5758 // prompt in the active window before switching to a different window.
5759 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
5760
5761 let mut prompt = None;
5762 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
5763 prompt = window
5764 .update(cx, |_, cx| {
5765 cx.prompt(
5766 PromptLevel::Info,
5767 "Are you sure you want to restart?",
5768 None,
5769 &["Restart", "Cancel"],
5770 )
5771 })
5772 .ok();
5773 }
5774
5775 let binary_path = reload.binary_path.clone();
5776 cx.spawn(|mut cx| async move {
5777 if let Some(prompt) = prompt {
5778 let answer = prompt.await?;
5779 if answer != 0 {
5780 return Ok(());
5781 }
5782 }
5783
5784 // If the user cancels any save prompt, then keep the app open.
5785 for window in workspace_windows {
5786 if let Ok(should_close) = window.update(&mut cx, |workspace, cx| {
5787 workspace.prepare_to_close(CloseIntent::Quit, cx)
5788 }) {
5789 if !should_close.await? {
5790 return Ok(());
5791 }
5792 }
5793 }
5794
5795 cx.update(|cx| cx.restart(binary_path))
5796 })
5797 .detach_and_log_err(cx);
5798}
5799
5800fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
5801 let mut parts = value.split(',');
5802 let x: usize = parts.next()?.parse().ok()?;
5803 let y: usize = parts.next()?.parse().ok()?;
5804 Some(point(px(x as f32), px(y as f32)))
5805}
5806
5807fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
5808 let mut parts = value.split(',');
5809 let width: usize = parts.next()?.parse().ok()?;
5810 let height: usize = parts.next()?.parse().ok()?;
5811 Some(size(px(width as f32), px(height as f32)))
5812}
5813
5814pub fn client_side_decorations(element: impl IntoElement, cx: &mut WindowContext) -> Stateful<Div> {
5815 const BORDER_SIZE: Pixels = px(1.0);
5816 let decorations = cx.window_decorations();
5817
5818 if matches!(decorations, Decorations::Client { .. }) {
5819 cx.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
5820 }
5821
5822 struct GlobalResizeEdge(ResizeEdge);
5823 impl Global for GlobalResizeEdge {}
5824
5825 div()
5826 .id("window-backdrop")
5827 .bg(transparent_black())
5828 .map(|div| match decorations {
5829 Decorations::Server => div,
5830 Decorations::Client { tiling, .. } => div
5831 .when(!(tiling.top || tiling.right), |div| {
5832 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5833 })
5834 .when(!(tiling.top || tiling.left), |div| {
5835 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5836 })
5837 .when(!(tiling.bottom || tiling.right), |div| {
5838 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5839 })
5840 .when(!(tiling.bottom || tiling.left), |div| {
5841 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5842 })
5843 .when(!tiling.top, |div| {
5844 div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
5845 })
5846 .when(!tiling.bottom, |div| {
5847 div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
5848 })
5849 .when(!tiling.left, |div| {
5850 div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
5851 })
5852 .when(!tiling.right, |div| {
5853 div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
5854 })
5855 .on_mouse_move(move |e, cx| {
5856 let size = cx.window_bounds().get_bounds().size;
5857 let pos = e.position;
5858
5859 let new_edge =
5860 resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
5861
5862 let edge = cx.try_global::<GlobalResizeEdge>();
5863 if new_edge != edge.map(|edge| edge.0) {
5864 cx.window_handle()
5865 .update(cx, |workspace, cx| cx.notify(workspace.entity_id()))
5866 .ok();
5867 }
5868 })
5869 .on_mouse_down(MouseButton::Left, move |e, cx| {
5870 let size = cx.window_bounds().get_bounds().size;
5871 let pos = e.position;
5872
5873 let edge = match resize_edge(
5874 pos,
5875 theme::CLIENT_SIDE_DECORATION_SHADOW,
5876 size,
5877 tiling,
5878 ) {
5879 Some(value) => value,
5880 None => return,
5881 };
5882
5883 cx.start_window_resize(edge);
5884 }),
5885 })
5886 .size_full()
5887 .child(
5888 div()
5889 .cursor(CursorStyle::Arrow)
5890 .map(|div| match decorations {
5891 Decorations::Server => div,
5892 Decorations::Client { tiling } => div
5893 .border_color(cx.theme().colors().border)
5894 .when(!(tiling.top || tiling.right), |div| {
5895 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5896 })
5897 .when(!(tiling.top || tiling.left), |div| {
5898 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5899 })
5900 .when(!(tiling.bottom || tiling.right), |div| {
5901 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5902 })
5903 .when(!(tiling.bottom || tiling.left), |div| {
5904 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5905 })
5906 .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
5907 .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
5908 .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
5909 .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
5910 .when(!tiling.is_tiled(), |div| {
5911 div.shadow(smallvec::smallvec![gpui::BoxShadow {
5912 color: Hsla {
5913 h: 0.,
5914 s: 0.,
5915 l: 0.,
5916 a: 0.4,
5917 },
5918 blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
5919 spread_radius: px(0.),
5920 offset: point(px(0.0), px(0.0)),
5921 }])
5922 }),
5923 })
5924 .on_mouse_move(|_e, cx| {
5925 cx.stop_propagation();
5926 })
5927 .size_full()
5928 .child(element),
5929 )
5930 .map(|div| match decorations {
5931 Decorations::Server => div,
5932 Decorations::Client { tiling, .. } => div.child(
5933 canvas(
5934 |_bounds, cx| {
5935 cx.insert_hitbox(
5936 Bounds::new(
5937 point(px(0.0), px(0.0)),
5938 cx.window_bounds().get_bounds().size,
5939 ),
5940 false,
5941 )
5942 },
5943 move |_bounds, hitbox, cx| {
5944 let mouse = cx.mouse_position();
5945 let size = cx.window_bounds().get_bounds().size;
5946 let Some(edge) =
5947 resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
5948 else {
5949 return;
5950 };
5951 cx.set_global(GlobalResizeEdge(edge));
5952 cx.set_cursor_style(
5953 match edge {
5954 ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
5955 ResizeEdge::Left | ResizeEdge::Right => {
5956 CursorStyle::ResizeLeftRight
5957 }
5958 ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
5959 CursorStyle::ResizeUpLeftDownRight
5960 }
5961 ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
5962 CursorStyle::ResizeUpRightDownLeft
5963 }
5964 },
5965 &hitbox,
5966 );
5967 },
5968 )
5969 .size_full()
5970 .absolute(),
5971 ),
5972 })
5973}
5974
5975fn resize_edge(
5976 pos: Point<Pixels>,
5977 shadow_size: Pixels,
5978 window_size: Size<Pixels>,
5979 tiling: Tiling,
5980) -> Option<ResizeEdge> {
5981 let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
5982 if bounds.contains(&pos) {
5983 return None;
5984 }
5985
5986 let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
5987 let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
5988 if !tiling.top && top_left_bounds.contains(&pos) {
5989 return Some(ResizeEdge::TopLeft);
5990 }
5991
5992 let top_right_bounds = Bounds::new(
5993 Point::new(window_size.width - corner_size.width, px(0.)),
5994 corner_size,
5995 );
5996 if !tiling.top && top_right_bounds.contains(&pos) {
5997 return Some(ResizeEdge::TopRight);
5998 }
5999
6000 let bottom_left_bounds = Bounds::new(
6001 Point::new(px(0.), window_size.height - corner_size.height),
6002 corner_size,
6003 );
6004 if !tiling.bottom && bottom_left_bounds.contains(&pos) {
6005 return Some(ResizeEdge::BottomLeft);
6006 }
6007
6008 let bottom_right_bounds = Bounds::new(
6009 Point::new(
6010 window_size.width - corner_size.width,
6011 window_size.height - corner_size.height,
6012 ),
6013 corner_size,
6014 );
6015 if !tiling.bottom && bottom_right_bounds.contains(&pos) {
6016 return Some(ResizeEdge::BottomRight);
6017 }
6018
6019 if !tiling.top && pos.y < shadow_size {
6020 Some(ResizeEdge::Top)
6021 } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
6022 Some(ResizeEdge::Bottom)
6023 } else if !tiling.left && pos.x < shadow_size {
6024 Some(ResizeEdge::Left)
6025 } else if !tiling.right && pos.x > window_size.width - shadow_size {
6026 Some(ResizeEdge::Right)
6027 } else {
6028 None
6029 }
6030}
6031
6032fn join_pane_into_active(active_pane: &View<Pane>, pane: &View<Pane>, cx: &mut WindowContext<'_>) {
6033 if pane == active_pane {
6034 return;
6035 } else if pane.read(cx).items_len() == 0 {
6036 pane.update(cx, |_, cx| {
6037 cx.emit(pane::Event::Remove {
6038 focus_on_pane: None,
6039 });
6040 })
6041 } else {
6042 move_all_items(pane, active_pane, cx);
6043 }
6044}
6045
6046fn move_all_items(from_pane: &View<Pane>, to_pane: &View<Pane>, cx: &mut WindowContext<'_>) {
6047 let destination_is_different = from_pane != to_pane;
6048 let mut moved_items = 0;
6049 for (item_ix, item_handle) in from_pane
6050 .read(cx)
6051 .items()
6052 .enumerate()
6053 .map(|(ix, item)| (ix, item.clone()))
6054 .collect::<Vec<_>>()
6055 {
6056 let ix = item_ix - moved_items;
6057 if destination_is_different {
6058 // Close item from previous pane
6059 from_pane.update(cx, |source, cx| {
6060 source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), cx);
6061 });
6062 moved_items += 1;
6063 }
6064
6065 // This automatically removes duplicate items in the pane
6066 to_pane.update(cx, |destination, cx| {
6067 destination.add_item(item_handle, true, true, None, cx);
6068 destination.focus(cx)
6069 });
6070 }
6071}
6072
6073pub fn move_item(
6074 source: &View<Pane>,
6075 destination: &View<Pane>,
6076 item_id_to_move: EntityId,
6077 destination_index: usize,
6078 cx: &mut WindowContext<'_>,
6079) {
6080 let Some((item_ix, item_handle)) = source
6081 .read(cx)
6082 .items()
6083 .enumerate()
6084 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
6085 .map(|(ix, item)| (ix, item.clone()))
6086 else {
6087 // Tab was closed during drag
6088 return;
6089 };
6090
6091 if source != destination {
6092 // Close item from previous pane
6093 source.update(cx, |source, cx| {
6094 source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), cx);
6095 });
6096 }
6097
6098 // This automatically removes duplicate items in the pane
6099 destination.update(cx, |destination, cx| {
6100 destination.add_item(item_handle, true, true, Some(destination_index), cx);
6101 destination.focus(cx)
6102 });
6103}
6104
6105#[cfg(test)]
6106mod tests {
6107 use std::{cell::RefCell, rc::Rc};
6108
6109 use super::*;
6110 use crate::{
6111 dock::{test::TestPanel, PanelEvent},
6112 item::{
6113 test::{TestItem, TestProjectItem},
6114 ItemEvent,
6115 },
6116 };
6117 use fs::FakeFs;
6118 use gpui::{
6119 px, DismissEvent, Empty, EventEmitter, FocusHandle, FocusableView, Render, TestAppContext,
6120 UpdateGlobal, VisualTestContext,
6121 };
6122 use project::{Project, ProjectEntryId};
6123 use serde_json::json;
6124 use settings::SettingsStore;
6125
6126 #[gpui::test]
6127 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
6128 init_test(cx);
6129
6130 let fs = FakeFs::new(cx.executor());
6131 let project = Project::test(fs, [], cx).await;
6132 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6133
6134 // Adding an item with no ambiguity renders the tab without detail.
6135 let item1 = cx.new_view(|cx| {
6136 let mut item = TestItem::new(cx);
6137 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
6138 item
6139 });
6140 workspace.update(cx, |workspace, cx| {
6141 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6142 });
6143 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
6144
6145 // Adding an item that creates ambiguity increases the level of detail on
6146 // both tabs.
6147 let item2 = cx.new_view(|cx| {
6148 let mut item = TestItem::new(cx);
6149 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6150 item
6151 });
6152 workspace.update(cx, |workspace, cx| {
6153 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6154 });
6155 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6156 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6157
6158 // Adding an item that creates ambiguity increases the level of detail only
6159 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
6160 // we stop at the highest detail available.
6161 let item3 = cx.new_view(|cx| {
6162 let mut item = TestItem::new(cx);
6163 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6164 item
6165 });
6166 workspace.update(cx, |workspace, cx| {
6167 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6168 });
6169 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6170 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6171 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6172 }
6173
6174 #[gpui::test]
6175 async fn test_tracking_active_path(cx: &mut TestAppContext) {
6176 init_test(cx);
6177
6178 let fs = FakeFs::new(cx.executor());
6179 fs.insert_tree(
6180 "/root1",
6181 json!({
6182 "one.txt": "",
6183 "two.txt": "",
6184 }),
6185 )
6186 .await;
6187 fs.insert_tree(
6188 "/root2",
6189 json!({
6190 "three.txt": "",
6191 }),
6192 )
6193 .await;
6194
6195 let project = Project::test(fs, ["root1".as_ref()], cx).await;
6196 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6197 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6198 let worktree_id = project.update(cx, |project, cx| {
6199 project.worktrees(cx).next().unwrap().read(cx).id()
6200 });
6201
6202 let item1 = cx.new_view(|cx| {
6203 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
6204 });
6205 let item2 = cx.new_view(|cx| {
6206 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
6207 });
6208
6209 // Add an item to an empty pane
6210 workspace.update(cx, |workspace, cx| {
6211 workspace.add_item_to_active_pane(Box::new(item1), None, true, cx)
6212 });
6213 project.update(cx, |project, cx| {
6214 assert_eq!(
6215 project.active_entry(),
6216 project
6217 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6218 .map(|e| e.id)
6219 );
6220 });
6221 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
6222
6223 // Add a second item to a non-empty pane
6224 workspace.update(cx, |workspace, cx| {
6225 workspace.add_item_to_active_pane(Box::new(item2), None, true, cx)
6226 });
6227 assert_eq!(cx.window_title().as_deref(), Some("two.txt — root1"));
6228 project.update(cx, |project, cx| {
6229 assert_eq!(
6230 project.active_entry(),
6231 project
6232 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
6233 .map(|e| e.id)
6234 );
6235 });
6236
6237 // Close the active item
6238 pane.update(cx, |pane, cx| {
6239 pane.close_active_item(&Default::default(), cx).unwrap()
6240 })
6241 .await
6242 .unwrap();
6243 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
6244 project.update(cx, |project, cx| {
6245 assert_eq!(
6246 project.active_entry(),
6247 project
6248 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6249 .map(|e| e.id)
6250 );
6251 });
6252
6253 // Add a project folder
6254 project
6255 .update(cx, |project, cx| {
6256 project.find_or_create_worktree("root2", true, cx)
6257 })
6258 .await
6259 .unwrap();
6260 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1, root2"));
6261
6262 // Remove a project folder
6263 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
6264 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root2"));
6265 }
6266
6267 #[gpui::test]
6268 async fn test_close_window(cx: &mut TestAppContext) {
6269 init_test(cx);
6270
6271 let fs = FakeFs::new(cx.executor());
6272 fs.insert_tree("/root", json!({ "one": "" })).await;
6273
6274 let project = Project::test(fs, ["root".as_ref()], cx).await;
6275 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6276
6277 // When there are no dirty items, there's nothing to do.
6278 let item1 = cx.new_view(TestItem::new);
6279 workspace.update(cx, |w, cx| {
6280 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx)
6281 });
6282 let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6283 assert!(task.await.unwrap());
6284
6285 // When there are dirty untitled items, prompt to save each one. If the user
6286 // cancels any prompt, then abort.
6287 let item2 = cx.new_view(|cx| TestItem::new(cx).with_dirty(true));
6288 let item3 = cx.new_view(|cx| {
6289 TestItem::new(cx)
6290 .with_dirty(true)
6291 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6292 });
6293 workspace.update(cx, |w, cx| {
6294 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6295 w.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6296 });
6297 let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6298 cx.executor().run_until_parked();
6299 cx.simulate_prompt_answer(2); // cancel save all
6300 cx.executor().run_until_parked();
6301 cx.simulate_prompt_answer(2); // cancel save all
6302 cx.executor().run_until_parked();
6303 assert!(!cx.has_pending_prompt());
6304 assert!(!task.await.unwrap());
6305 }
6306
6307 #[gpui::test]
6308 async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
6309 init_test(cx);
6310
6311 // Register TestItem as a serializable item
6312 cx.update(|cx| {
6313 register_serializable_item::<TestItem>(cx);
6314 });
6315
6316 let fs = FakeFs::new(cx.executor());
6317 fs.insert_tree("/root", json!({ "one": "" })).await;
6318
6319 let project = Project::test(fs, ["root".as_ref()], cx).await;
6320 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6321
6322 // When there are dirty untitled items, but they can serialize, then there is no prompt.
6323 let item1 = cx.new_view(|cx| {
6324 TestItem::new(cx)
6325 .with_dirty(true)
6326 .with_serialize(|| Some(Task::ready(Ok(()))))
6327 });
6328 let item2 = cx.new_view(|cx| {
6329 TestItem::new(cx)
6330 .with_dirty(true)
6331 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6332 .with_serialize(|| Some(Task::ready(Ok(()))))
6333 });
6334 workspace.update(cx, |w, cx| {
6335 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6336 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6337 });
6338 let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6339 assert!(task.await.unwrap());
6340 }
6341
6342 #[gpui::test]
6343 async fn test_close_pane_items(cx: &mut TestAppContext) {
6344 init_test(cx);
6345
6346 let fs = FakeFs::new(cx.executor());
6347
6348 let project = Project::test(fs, None, cx).await;
6349 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6350
6351 let item1 = cx.new_view(|cx| {
6352 TestItem::new(cx)
6353 .with_dirty(true)
6354 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6355 });
6356 let item2 = cx.new_view(|cx| {
6357 TestItem::new(cx)
6358 .with_dirty(true)
6359 .with_conflict(true)
6360 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
6361 });
6362 let item3 = cx.new_view(|cx| {
6363 TestItem::new(cx)
6364 .with_dirty(true)
6365 .with_conflict(true)
6366 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
6367 });
6368 let item4 = cx.new_view(|cx| {
6369 TestItem::new(cx)
6370 .with_dirty(true)
6371 .with_project_items(&[TestProjectItem::new_untitled(cx)])
6372 });
6373 let pane = workspace.update(cx, |workspace, cx| {
6374 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6375 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6376 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6377 workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, cx);
6378 workspace.active_pane().clone()
6379 });
6380
6381 let close_items = pane.update(cx, |pane, cx| {
6382 pane.activate_item(1, true, true, cx);
6383 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
6384 let item1_id = item1.item_id();
6385 let item3_id = item3.item_id();
6386 let item4_id = item4.item_id();
6387 pane.close_items(cx, SaveIntent::Close, move |id| {
6388 [item1_id, item3_id, item4_id].contains(&id)
6389 })
6390 });
6391 cx.executor().run_until_parked();
6392
6393 assert!(cx.has_pending_prompt());
6394 // Ignore "Save all" prompt
6395 cx.simulate_prompt_answer(2);
6396 cx.executor().run_until_parked();
6397 // There's a prompt to save item 1.
6398 pane.update(cx, |pane, _| {
6399 assert_eq!(pane.items_len(), 4);
6400 assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
6401 });
6402 // Confirm saving item 1.
6403 cx.simulate_prompt_answer(0);
6404 cx.executor().run_until_parked();
6405
6406 // Item 1 is saved. There's a prompt to save item 3.
6407 pane.update(cx, |pane, cx| {
6408 assert_eq!(item1.read(cx).save_count, 1);
6409 assert_eq!(item1.read(cx).save_as_count, 0);
6410 assert_eq!(item1.read(cx).reload_count, 0);
6411 assert_eq!(pane.items_len(), 3);
6412 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
6413 });
6414 assert!(cx.has_pending_prompt());
6415
6416 // Cancel saving item 3.
6417 cx.simulate_prompt_answer(1);
6418 cx.executor().run_until_parked();
6419
6420 // Item 3 is reloaded. There's a prompt to save item 4.
6421 pane.update(cx, |pane, cx| {
6422 assert_eq!(item3.read(cx).save_count, 0);
6423 assert_eq!(item3.read(cx).save_as_count, 0);
6424 assert_eq!(item3.read(cx).reload_count, 1);
6425 assert_eq!(pane.items_len(), 2);
6426 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
6427 });
6428 assert!(cx.has_pending_prompt());
6429
6430 // Confirm saving item 4.
6431 cx.simulate_prompt_answer(0);
6432 cx.executor().run_until_parked();
6433
6434 // There's a prompt for a path for item 4.
6435 cx.simulate_new_path_selection(|_| Some(Default::default()));
6436 close_items.await.unwrap();
6437
6438 // The requested items are closed.
6439 pane.update(cx, |pane, cx| {
6440 assert_eq!(item4.read(cx).save_count, 0);
6441 assert_eq!(item4.read(cx).save_as_count, 1);
6442 assert_eq!(item4.read(cx).reload_count, 0);
6443 assert_eq!(pane.items_len(), 1);
6444 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
6445 });
6446 }
6447
6448 #[gpui::test]
6449 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
6450 init_test(cx);
6451
6452 let fs = FakeFs::new(cx.executor());
6453 let project = Project::test(fs, [], cx).await;
6454 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6455
6456 // Create several workspace items with single project entries, and two
6457 // workspace items with multiple project entries.
6458 let single_entry_items = (0..=4)
6459 .map(|project_entry_id| {
6460 cx.new_view(|cx| {
6461 TestItem::new(cx)
6462 .with_dirty(true)
6463 .with_project_items(&[TestProjectItem::new(
6464 project_entry_id,
6465 &format!("{project_entry_id}.txt"),
6466 cx,
6467 )])
6468 })
6469 })
6470 .collect::<Vec<_>>();
6471 let item_2_3 = cx.new_view(|cx| {
6472 TestItem::new(cx)
6473 .with_dirty(true)
6474 .with_singleton(false)
6475 .with_project_items(&[
6476 single_entry_items[2].read(cx).project_items[0].clone(),
6477 single_entry_items[3].read(cx).project_items[0].clone(),
6478 ])
6479 });
6480 let item_3_4 = cx.new_view(|cx| {
6481 TestItem::new(cx)
6482 .with_dirty(true)
6483 .with_singleton(false)
6484 .with_project_items(&[
6485 single_entry_items[3].read(cx).project_items[0].clone(),
6486 single_entry_items[4].read(cx).project_items[0].clone(),
6487 ])
6488 });
6489
6490 // Create two panes that contain the following project entries:
6491 // left pane:
6492 // multi-entry items: (2, 3)
6493 // single-entry items: 0, 1, 2, 3, 4
6494 // right pane:
6495 // single-entry items: 1
6496 // multi-entry items: (3, 4)
6497 let left_pane = workspace.update(cx, |workspace, cx| {
6498 let left_pane = workspace.active_pane().clone();
6499 workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, cx);
6500 for item in single_entry_items {
6501 workspace.add_item_to_active_pane(Box::new(item), None, true, cx);
6502 }
6503 left_pane.update(cx, |pane, cx| {
6504 pane.activate_item(2, true, true, cx);
6505 });
6506
6507 let right_pane = workspace
6508 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
6509 .unwrap();
6510
6511 right_pane.update(cx, |pane, cx| {
6512 pane.add_item(Box::new(item_3_4.clone()), true, true, None, cx);
6513 });
6514
6515 left_pane
6516 });
6517
6518 cx.focus_view(&left_pane);
6519
6520 // When closing all of the items in the left pane, we should be prompted twice:
6521 // once for project entry 0, and once for project entry 2. Project entries 1,
6522 // 3, and 4 are all still open in the other paten. After those two
6523 // prompts, the task should complete.
6524
6525 let close = left_pane.update(cx, |pane, cx| {
6526 pane.close_all_items(&CloseAllItems::default(), cx).unwrap()
6527 });
6528 cx.executor().run_until_parked();
6529
6530 // Discard "Save all" prompt
6531 cx.simulate_prompt_answer(2);
6532
6533 cx.executor().run_until_parked();
6534 left_pane.update(cx, |pane, cx| {
6535 assert_eq!(
6536 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
6537 &[ProjectEntryId::from_proto(0)]
6538 );
6539 });
6540 cx.simulate_prompt_answer(0);
6541
6542 cx.executor().run_until_parked();
6543 left_pane.update(cx, |pane, cx| {
6544 assert_eq!(
6545 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
6546 &[ProjectEntryId::from_proto(2)]
6547 );
6548 });
6549 cx.simulate_prompt_answer(0);
6550
6551 cx.executor().run_until_parked();
6552 close.await.unwrap();
6553 left_pane.update(cx, |pane, _| {
6554 assert_eq!(pane.items_len(), 0);
6555 });
6556 }
6557
6558 #[gpui::test]
6559 async fn test_autosave(cx: &mut gpui::TestAppContext) {
6560 init_test(cx);
6561
6562 let fs = FakeFs::new(cx.executor());
6563 let project = Project::test(fs, [], cx).await;
6564 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6565 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6566
6567 let item = cx.new_view(|cx| {
6568 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6569 });
6570 let item_id = item.entity_id();
6571 workspace.update(cx, |workspace, cx| {
6572 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6573 });
6574
6575 // Autosave on window change.
6576 item.update(cx, |item, cx| {
6577 SettingsStore::update_global(cx, |settings, cx| {
6578 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6579 settings.autosave = Some(AutosaveSetting::OnWindowChange);
6580 })
6581 });
6582 item.is_dirty = true;
6583 });
6584
6585 // Deactivating the window saves the file.
6586 cx.deactivate_window();
6587 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
6588
6589 // Re-activating the window doesn't save the file.
6590 cx.update(|cx| cx.activate_window());
6591 cx.executor().run_until_parked();
6592 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
6593
6594 // Autosave on focus change.
6595 item.update(cx, |item, cx| {
6596 cx.focus_self();
6597 SettingsStore::update_global(cx, |settings, cx| {
6598 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6599 settings.autosave = Some(AutosaveSetting::OnFocusChange);
6600 })
6601 });
6602 item.is_dirty = true;
6603 });
6604
6605 // Blurring the item saves the file.
6606 item.update(cx, |_, cx| cx.blur());
6607 cx.executor().run_until_parked();
6608 item.update(cx, |item, _| assert_eq!(item.save_count, 2));
6609
6610 // Deactivating the window still saves the file.
6611 item.update(cx, |item, cx| {
6612 cx.focus_self();
6613 item.is_dirty = true;
6614 });
6615 cx.deactivate_window();
6616 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
6617
6618 // Autosave after delay.
6619 item.update(cx, |item, cx| {
6620 SettingsStore::update_global(cx, |settings, cx| {
6621 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6622 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
6623 })
6624 });
6625 item.is_dirty = true;
6626 cx.emit(ItemEvent::Edit);
6627 });
6628
6629 // Delay hasn't fully expired, so the file is still dirty and unsaved.
6630 cx.executor().advance_clock(Duration::from_millis(250));
6631 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
6632
6633 // After delay expires, the file is saved.
6634 cx.executor().advance_clock(Duration::from_millis(250));
6635 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
6636
6637 // Autosave on focus change, ensuring closing the tab counts as such.
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::OnFocusChange);
6642 })
6643 });
6644 item.is_dirty = true;
6645 });
6646
6647 pane.update(cx, |pane, cx| {
6648 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
6649 })
6650 .await
6651 .unwrap();
6652 assert!(!cx.has_pending_prompt());
6653 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6654
6655 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
6656 workspace.update(cx, |workspace, cx| {
6657 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6658 });
6659 item.update(cx, |item, cx| {
6660 item.project_items[0].update(cx, |item, _| {
6661 item.entry_id = None;
6662 });
6663 item.is_dirty = true;
6664 cx.blur();
6665 });
6666 cx.run_until_parked();
6667 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6668
6669 // Ensure autosave is prevented for deleted files also when closing the buffer.
6670 let _close_items = pane.update(cx, |pane, cx| {
6671 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
6672 });
6673 cx.run_until_parked();
6674 assert!(cx.has_pending_prompt());
6675 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6676 }
6677
6678 #[gpui::test]
6679 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
6680 init_test(cx);
6681
6682 let fs = FakeFs::new(cx.executor());
6683
6684 let project = Project::test(fs, [], cx).await;
6685 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6686
6687 let item = cx.new_view(|cx| {
6688 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6689 });
6690 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6691 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
6692 let toolbar_notify_count = Rc::new(RefCell::new(0));
6693
6694 workspace.update(cx, |workspace, cx| {
6695 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6696 let toolbar_notification_count = toolbar_notify_count.clone();
6697 cx.observe(&toolbar, move |_, _, _| {
6698 *toolbar_notification_count.borrow_mut() += 1
6699 })
6700 .detach();
6701 });
6702
6703 pane.update(cx, |pane, _| {
6704 assert!(!pane.can_navigate_backward());
6705 assert!(!pane.can_navigate_forward());
6706 });
6707
6708 item.update(cx, |item, cx| {
6709 item.set_state("one".to_string(), cx);
6710 });
6711
6712 // Toolbar must be notified to re-render the navigation buttons
6713 assert_eq!(*toolbar_notify_count.borrow(), 1);
6714
6715 pane.update(cx, |pane, _| {
6716 assert!(pane.can_navigate_backward());
6717 assert!(!pane.can_navigate_forward());
6718 });
6719
6720 workspace
6721 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
6722 .await
6723 .unwrap();
6724
6725 assert_eq!(*toolbar_notify_count.borrow(), 2);
6726 pane.update(cx, |pane, _| {
6727 assert!(!pane.can_navigate_backward());
6728 assert!(pane.can_navigate_forward());
6729 });
6730 }
6731
6732 #[gpui::test]
6733 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
6734 init_test(cx);
6735 let fs = FakeFs::new(cx.executor());
6736
6737 let project = Project::test(fs, [], cx).await;
6738 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6739
6740 let panel = workspace.update(cx, |workspace, cx| {
6741 let panel = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
6742 workspace.add_panel(panel.clone(), cx);
6743
6744 workspace
6745 .right_dock()
6746 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
6747
6748 panel
6749 });
6750
6751 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6752 pane.update(cx, |pane, cx| {
6753 let item = cx.new_view(TestItem::new);
6754 pane.add_item(Box::new(item), true, true, None, cx);
6755 });
6756
6757 // Transfer focus from center to panel
6758 workspace.update(cx, |workspace, cx| {
6759 workspace.toggle_panel_focus::<TestPanel>(cx);
6760 });
6761
6762 workspace.update(cx, |workspace, cx| {
6763 assert!(workspace.right_dock().read(cx).is_open());
6764 assert!(!panel.is_zoomed(cx));
6765 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6766 });
6767
6768 // Transfer focus from panel to center
6769 workspace.update(cx, |workspace, cx| {
6770 workspace.toggle_panel_focus::<TestPanel>(cx);
6771 });
6772
6773 workspace.update(cx, |workspace, cx| {
6774 assert!(workspace.right_dock().read(cx).is_open());
6775 assert!(!panel.is_zoomed(cx));
6776 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6777 });
6778
6779 // Close the dock
6780 workspace.update(cx, |workspace, cx| {
6781 workspace.toggle_dock(DockPosition::Right, cx);
6782 });
6783
6784 workspace.update(cx, |workspace, cx| {
6785 assert!(!workspace.right_dock().read(cx).is_open());
6786 assert!(!panel.is_zoomed(cx));
6787 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6788 });
6789
6790 // Open the dock
6791 workspace.update(cx, |workspace, cx| {
6792 workspace.toggle_dock(DockPosition::Right, cx);
6793 });
6794
6795 workspace.update(cx, |workspace, cx| {
6796 assert!(workspace.right_dock().read(cx).is_open());
6797 assert!(!panel.is_zoomed(cx));
6798 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6799 });
6800
6801 // Focus and zoom panel
6802 panel.update(cx, |panel, cx| {
6803 cx.focus_self();
6804 panel.set_zoomed(true, cx)
6805 });
6806
6807 workspace.update(cx, |workspace, cx| {
6808 assert!(workspace.right_dock().read(cx).is_open());
6809 assert!(panel.is_zoomed(cx));
6810 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6811 });
6812
6813 // Transfer focus to the center closes the dock
6814 workspace.update(cx, |workspace, cx| {
6815 workspace.toggle_panel_focus::<TestPanel>(cx);
6816 });
6817
6818 workspace.update(cx, |workspace, cx| {
6819 assert!(!workspace.right_dock().read(cx).is_open());
6820 assert!(panel.is_zoomed(cx));
6821 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6822 });
6823
6824 // Transferring focus back to the panel keeps it zoomed
6825 workspace.update(cx, |workspace, cx| {
6826 workspace.toggle_panel_focus::<TestPanel>(cx);
6827 });
6828
6829 workspace.update(cx, |workspace, cx| {
6830 assert!(workspace.right_dock().read(cx).is_open());
6831 assert!(panel.is_zoomed(cx));
6832 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6833 });
6834
6835 // Close the dock while it is zoomed
6836 workspace.update(cx, |workspace, cx| {
6837 workspace.toggle_dock(DockPosition::Right, cx)
6838 });
6839
6840 workspace.update(cx, |workspace, cx| {
6841 assert!(!workspace.right_dock().read(cx).is_open());
6842 assert!(panel.is_zoomed(cx));
6843 assert!(workspace.zoomed.is_none());
6844 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6845 });
6846
6847 // Opening the dock, when it's zoomed, retains focus
6848 workspace.update(cx, |workspace, cx| {
6849 workspace.toggle_dock(DockPosition::Right, cx)
6850 });
6851
6852 workspace.update(cx, |workspace, cx| {
6853 assert!(workspace.right_dock().read(cx).is_open());
6854 assert!(panel.is_zoomed(cx));
6855 assert!(workspace.zoomed.is_some());
6856 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6857 });
6858
6859 // Unzoom and close the panel, zoom the active pane.
6860 panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
6861 workspace.update(cx, |workspace, cx| {
6862 workspace.toggle_dock(DockPosition::Right, cx)
6863 });
6864 pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
6865
6866 // Opening a dock unzooms the pane.
6867 workspace.update(cx, |workspace, cx| {
6868 workspace.toggle_dock(DockPosition::Right, cx)
6869 });
6870 workspace.update(cx, |workspace, cx| {
6871 let pane = pane.read(cx);
6872 assert!(!pane.is_zoomed());
6873 assert!(!pane.focus_handle(cx).is_focused(cx));
6874 assert!(workspace.right_dock().read(cx).is_open());
6875 assert!(workspace.zoomed.is_none());
6876 });
6877 }
6878
6879 #[gpui::test]
6880 async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
6881 init_test(cx);
6882
6883 let fs = FakeFs::new(cx.executor());
6884
6885 let project = Project::test(fs, None, cx).await;
6886 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6887
6888 // Let's arrange the panes like this:
6889 //
6890 // +-----------------------+
6891 // | top |
6892 // +------+--------+-------+
6893 // | left | center | right |
6894 // +------+--------+-------+
6895 // | bottom |
6896 // +-----------------------+
6897
6898 let top_item = cx.new_view(|cx| {
6899 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
6900 });
6901 let bottom_item = cx.new_view(|cx| {
6902 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
6903 });
6904 let left_item = cx.new_view(|cx| {
6905 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
6906 });
6907 let right_item = cx.new_view(|cx| {
6908 TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
6909 });
6910 let center_item = cx.new_view(|cx| {
6911 TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
6912 });
6913
6914 let top_pane_id = workspace.update(cx, |workspace, cx| {
6915 let top_pane_id = workspace.active_pane().entity_id();
6916 workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, cx);
6917 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Down, cx);
6918 top_pane_id
6919 });
6920 let bottom_pane_id = workspace.update(cx, |workspace, cx| {
6921 let bottom_pane_id = workspace.active_pane().entity_id();
6922 workspace.add_item_to_active_pane(Box::new(bottom_item.clone()), None, false, cx);
6923 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Up, cx);
6924 bottom_pane_id
6925 });
6926 let left_pane_id = workspace.update(cx, |workspace, cx| {
6927 let left_pane_id = workspace.active_pane().entity_id();
6928 workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, cx);
6929 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
6930 left_pane_id
6931 });
6932 let right_pane_id = workspace.update(cx, |workspace, cx| {
6933 let right_pane_id = workspace.active_pane().entity_id();
6934 workspace.add_item_to_active_pane(Box::new(right_item.clone()), None, false, cx);
6935 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Left, cx);
6936 right_pane_id
6937 });
6938 let center_pane_id = workspace.update(cx, |workspace, cx| {
6939 let center_pane_id = workspace.active_pane().entity_id();
6940 workspace.add_item_to_active_pane(Box::new(center_item.clone()), None, false, cx);
6941 center_pane_id
6942 });
6943 cx.executor().run_until_parked();
6944
6945 workspace.update(cx, |workspace, cx| {
6946 assert_eq!(center_pane_id, workspace.active_pane().entity_id());
6947
6948 // Join into next from center pane into right
6949 workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
6950 });
6951
6952 workspace.update(cx, |workspace, cx| {
6953 let active_pane = workspace.active_pane();
6954 assert_eq!(right_pane_id, active_pane.entity_id());
6955 assert_eq!(2, active_pane.read(cx).items_len());
6956 let item_ids_in_pane =
6957 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
6958 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
6959 assert!(item_ids_in_pane.contains(&right_item.item_id()));
6960
6961 // Join into next from right pane into bottom
6962 workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
6963 });
6964
6965 workspace.update(cx, |workspace, cx| {
6966 let active_pane = workspace.active_pane();
6967 assert_eq!(bottom_pane_id, active_pane.entity_id());
6968 assert_eq!(3, active_pane.read(cx).items_len());
6969 let item_ids_in_pane =
6970 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
6971 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
6972 assert!(item_ids_in_pane.contains(&right_item.item_id()));
6973 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
6974
6975 // Join into next from bottom pane into left
6976 workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
6977 });
6978
6979 workspace.update(cx, |workspace, cx| {
6980 let active_pane = workspace.active_pane();
6981 assert_eq!(left_pane_id, active_pane.entity_id());
6982 assert_eq!(4, active_pane.read(cx).items_len());
6983 let item_ids_in_pane =
6984 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
6985 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
6986 assert!(item_ids_in_pane.contains(&right_item.item_id()));
6987 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
6988 assert!(item_ids_in_pane.contains(&left_item.item_id()));
6989
6990 // Join into next from left pane into top
6991 workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
6992 });
6993
6994 workspace.update(cx, |workspace, cx| {
6995 let active_pane = workspace.active_pane();
6996 assert_eq!(top_pane_id, active_pane.entity_id());
6997 assert_eq!(5, active_pane.read(cx).items_len());
6998 let item_ids_in_pane =
6999 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7000 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7001 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7002 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7003 assert!(item_ids_in_pane.contains(&left_item.item_id()));
7004 assert!(item_ids_in_pane.contains(&top_item.item_id()));
7005
7006 // Single pane left: no-op
7007 workspace.join_pane_into_next(workspace.active_pane().clone(), cx)
7008 });
7009
7010 workspace.update(cx, |workspace, _cx| {
7011 let active_pane = workspace.active_pane();
7012 assert_eq!(top_pane_id, active_pane.entity_id());
7013 });
7014 }
7015
7016 fn add_an_item_to_active_pane(
7017 cx: &mut VisualTestContext,
7018 workspace: &View<Workspace>,
7019 item_id: u64,
7020 ) -> View<TestItem> {
7021 let item = cx.new_view(|cx| {
7022 TestItem::new(cx).with_project_items(&[TestProjectItem::new(
7023 item_id,
7024 "item{item_id}.txt",
7025 cx,
7026 )])
7027 });
7028 workspace.update(cx, |workspace, cx| {
7029 workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, cx);
7030 });
7031 return item;
7032 }
7033
7034 fn split_pane(cx: &mut VisualTestContext, workspace: &View<Workspace>) -> View<Pane> {
7035 return workspace.update(cx, |workspace, cx| {
7036 let new_pane =
7037 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
7038 new_pane
7039 });
7040 }
7041
7042 #[gpui::test]
7043 async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
7044 init_test(cx);
7045 let fs = FakeFs::new(cx.executor());
7046 let project = Project::test(fs, None, cx).await;
7047 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7048
7049 add_an_item_to_active_pane(cx, &workspace, 1);
7050 split_pane(cx, &workspace);
7051 add_an_item_to_active_pane(cx, &workspace, 2);
7052 split_pane(cx, &workspace); // empty pane
7053 split_pane(cx, &workspace);
7054 let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
7055
7056 cx.executor().run_until_parked();
7057
7058 workspace.update(cx, |workspace, cx| {
7059 let num_panes = workspace.panes().len();
7060 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7061 let active_item = workspace
7062 .active_pane()
7063 .read(cx)
7064 .active_item()
7065 .expect("item is in focus");
7066
7067 assert_eq!(num_panes, 4);
7068 assert_eq!(num_items_in_current_pane, 1);
7069 assert_eq!(active_item.item_id(), last_item.item_id());
7070 });
7071
7072 workspace.update(cx, |workspace, cx| {
7073 workspace.join_all_panes(cx);
7074 });
7075
7076 workspace.update(cx, |workspace, cx| {
7077 let num_panes = workspace.panes().len();
7078 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7079 let active_item = workspace
7080 .active_pane()
7081 .read(cx)
7082 .active_item()
7083 .expect("item is in focus");
7084
7085 assert_eq!(num_panes, 1);
7086 assert_eq!(num_items_in_current_pane, 3);
7087 assert_eq!(active_item.item_id(), last_item.item_id());
7088 });
7089 }
7090 struct TestModal(FocusHandle);
7091
7092 impl TestModal {
7093 fn new(cx: &mut ViewContext<Self>) -> Self {
7094 Self(cx.focus_handle())
7095 }
7096 }
7097
7098 impl EventEmitter<DismissEvent> for TestModal {}
7099
7100 impl FocusableView for TestModal {
7101 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7102 self.0.clone()
7103 }
7104 }
7105
7106 impl ModalView for TestModal {}
7107
7108 impl Render for TestModal {
7109 fn render(&mut self, _cx: &mut ViewContext<TestModal>) -> impl IntoElement {
7110 div().track_focus(&self.0)
7111 }
7112 }
7113
7114 #[gpui::test]
7115 async fn test_panels(cx: &mut gpui::TestAppContext) {
7116 init_test(cx);
7117 let fs = FakeFs::new(cx.executor());
7118
7119 let project = Project::test(fs, [], cx).await;
7120 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7121
7122 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
7123 let panel_1 = cx.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
7124 workspace.add_panel(panel_1.clone(), cx);
7125 workspace
7126 .left_dock()
7127 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
7128 let panel_2 = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
7129 workspace.add_panel(panel_2.clone(), cx);
7130 workspace
7131 .right_dock()
7132 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
7133
7134 let left_dock = workspace.left_dock();
7135 assert_eq!(
7136 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7137 panel_1.panel_id()
7138 );
7139 assert_eq!(
7140 left_dock.read(cx).active_panel_size(cx).unwrap(),
7141 panel_1.size(cx)
7142 );
7143
7144 left_dock.update(cx, |left_dock, cx| {
7145 left_dock.resize_active_panel(Some(px(1337.)), cx)
7146 });
7147 assert_eq!(
7148 workspace
7149 .right_dock()
7150 .read(cx)
7151 .visible_panel()
7152 .unwrap()
7153 .panel_id(),
7154 panel_2.panel_id(),
7155 );
7156
7157 (panel_1, panel_2)
7158 });
7159
7160 // Move panel_1 to the right
7161 panel_1.update(cx, |panel_1, cx| {
7162 panel_1.set_position(DockPosition::Right, cx)
7163 });
7164
7165 workspace.update(cx, |workspace, cx| {
7166 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
7167 // Since it was the only panel on the left, the left dock should now be closed.
7168 assert!(!workspace.left_dock().read(cx).is_open());
7169 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
7170 let right_dock = workspace.right_dock();
7171 assert_eq!(
7172 right_dock.read(cx).visible_panel().unwrap().panel_id(),
7173 panel_1.panel_id()
7174 );
7175 assert_eq!(
7176 right_dock.read(cx).active_panel_size(cx).unwrap(),
7177 px(1337.)
7178 );
7179
7180 // Now we move panel_2 to the left
7181 panel_2.set_position(DockPosition::Left, cx);
7182 });
7183
7184 workspace.update(cx, |workspace, cx| {
7185 // Since panel_2 was not visible on the right, we don't open the left dock.
7186 assert!(!workspace.left_dock().read(cx).is_open());
7187 // And the right dock is unaffected in its displaying of panel_1
7188 assert!(workspace.right_dock().read(cx).is_open());
7189 assert_eq!(
7190 workspace
7191 .right_dock()
7192 .read(cx)
7193 .visible_panel()
7194 .unwrap()
7195 .panel_id(),
7196 panel_1.panel_id(),
7197 );
7198 });
7199
7200 // Move panel_1 back to the left
7201 panel_1.update(cx, |panel_1, cx| {
7202 panel_1.set_position(DockPosition::Left, cx)
7203 });
7204
7205 workspace.update(cx, |workspace, cx| {
7206 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
7207 let left_dock = workspace.left_dock();
7208 assert!(left_dock.read(cx).is_open());
7209 assert_eq!(
7210 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7211 panel_1.panel_id()
7212 );
7213 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), px(1337.));
7214 // And the right dock should be closed as it no longer has any panels.
7215 assert!(!workspace.right_dock().read(cx).is_open());
7216
7217 // Now we move panel_1 to the bottom
7218 panel_1.set_position(DockPosition::Bottom, cx);
7219 });
7220
7221 workspace.update(cx, |workspace, cx| {
7222 // Since panel_1 was visible on the left, we close the left dock.
7223 assert!(!workspace.left_dock().read(cx).is_open());
7224 // The bottom dock is sized based on the panel's default size,
7225 // since the panel orientation changed from vertical to horizontal.
7226 let bottom_dock = workspace.bottom_dock();
7227 assert_eq!(
7228 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
7229 panel_1.size(cx),
7230 );
7231 // Close bottom dock and move panel_1 back to the left.
7232 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
7233 panel_1.set_position(DockPosition::Left, cx);
7234 });
7235
7236 // Emit activated event on panel 1
7237 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
7238
7239 // Now the left dock is open and panel_1 is active and focused.
7240 workspace.update(cx, |workspace, cx| {
7241 let left_dock = workspace.left_dock();
7242 assert!(left_dock.read(cx).is_open());
7243 assert_eq!(
7244 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7245 panel_1.panel_id(),
7246 );
7247 assert!(panel_1.focus_handle(cx).is_focused(cx));
7248 });
7249
7250 // Emit closed event on panel 2, which is not active
7251 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
7252
7253 // Wo don't close the left dock, because panel_2 wasn't the active panel
7254 workspace.update(cx, |workspace, cx| {
7255 let left_dock = workspace.left_dock();
7256 assert!(left_dock.read(cx).is_open());
7257 assert_eq!(
7258 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7259 panel_1.panel_id(),
7260 );
7261 });
7262
7263 // Emitting a ZoomIn event shows the panel as zoomed.
7264 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
7265 workspace.update(cx, |workspace, _| {
7266 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7267 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
7268 });
7269
7270 // Move panel to another dock while it is zoomed
7271 panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
7272 workspace.update(cx, |workspace, _| {
7273 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7274
7275 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7276 });
7277
7278 // This is a helper for getting a:
7279 // - valid focus on an element,
7280 // - that isn't a part of the panes and panels system of the Workspace,
7281 // - and doesn't trigger the 'on_focus_lost' API.
7282 let focus_other_view = {
7283 let workspace = workspace.clone();
7284 move |cx: &mut VisualTestContext| {
7285 workspace.update(cx, |workspace, cx| {
7286 if let Some(_) = workspace.active_modal::<TestModal>(cx) {
7287 workspace.toggle_modal(cx, TestModal::new);
7288 workspace.toggle_modal(cx, TestModal::new);
7289 } else {
7290 workspace.toggle_modal(cx, TestModal::new);
7291 }
7292 })
7293 }
7294 };
7295
7296 // If focus is transferred to another view that's not a panel or another pane, we still show
7297 // the panel as zoomed.
7298 focus_other_view(cx);
7299 workspace.update(cx, |workspace, _| {
7300 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7301 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7302 });
7303
7304 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
7305 workspace.update(cx, |_, cx| cx.focus_self());
7306 workspace.update(cx, |workspace, _| {
7307 assert_eq!(workspace.zoomed, None);
7308 assert_eq!(workspace.zoomed_position, None);
7309 });
7310
7311 // If focus is transferred again to another view that's not a panel or a pane, we won't
7312 // show the panel as zoomed because it wasn't zoomed before.
7313 focus_other_view(cx);
7314 workspace.update(cx, |workspace, _| {
7315 assert_eq!(workspace.zoomed, None);
7316 assert_eq!(workspace.zoomed_position, None);
7317 });
7318
7319 // When the panel is activated, it is zoomed again.
7320 cx.dispatch_action(ToggleRightDock);
7321 workspace.update(cx, |workspace, _| {
7322 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7323 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7324 });
7325
7326 // Emitting a ZoomOut event unzooms the panel.
7327 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
7328 workspace.update(cx, |workspace, _| {
7329 assert_eq!(workspace.zoomed, None);
7330 assert_eq!(workspace.zoomed_position, None);
7331 });
7332
7333 // Emit closed event on panel 1, which is active
7334 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
7335
7336 // Now the left dock is closed, because panel_1 was the active panel
7337 workspace.update(cx, |workspace, cx| {
7338 let right_dock = workspace.right_dock();
7339 assert!(!right_dock.read(cx).is_open());
7340 });
7341 }
7342
7343 mod register_project_item_tests {
7344 use ui::Context as _;
7345
7346 use super::*;
7347
7348 // View
7349 struct TestPngItemView {
7350 focus_handle: FocusHandle,
7351 }
7352 // Model
7353 struct TestPngItem {}
7354
7355 impl project::Item for TestPngItem {
7356 fn try_open(
7357 _project: &Model<Project>,
7358 path: &ProjectPath,
7359 cx: &mut AppContext,
7360 ) -> Option<Task<gpui::Result<Model<Self>>>> {
7361 if path.path.extension().unwrap() == "png" {
7362 Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestPngItem {}) }))
7363 } else {
7364 None
7365 }
7366 }
7367
7368 fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
7369 None
7370 }
7371
7372 fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
7373 None
7374 }
7375 }
7376
7377 impl Item for TestPngItemView {
7378 type Event = ();
7379 }
7380 impl EventEmitter<()> for TestPngItemView {}
7381 impl FocusableView for TestPngItemView {
7382 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7383 self.focus_handle.clone()
7384 }
7385 }
7386
7387 impl Render for TestPngItemView {
7388 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
7389 Empty
7390 }
7391 }
7392
7393 impl ProjectItem for TestPngItemView {
7394 type Item = TestPngItem;
7395
7396 fn for_project_item(
7397 _project: Model<Project>,
7398 _item: Model<Self::Item>,
7399 cx: &mut ViewContext<Self>,
7400 ) -> Self
7401 where
7402 Self: Sized,
7403 {
7404 Self {
7405 focus_handle: cx.focus_handle(),
7406 }
7407 }
7408 }
7409
7410 // View
7411 struct TestIpynbItemView {
7412 focus_handle: FocusHandle,
7413 }
7414 // Model
7415 struct TestIpynbItem {}
7416
7417 impl project::Item for TestIpynbItem {
7418 fn try_open(
7419 _project: &Model<Project>,
7420 path: &ProjectPath,
7421 cx: &mut AppContext,
7422 ) -> Option<Task<gpui::Result<Model<Self>>>> {
7423 if path.path.extension().unwrap() == "ipynb" {
7424 Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestIpynbItem {}) }))
7425 } else {
7426 None
7427 }
7428 }
7429
7430 fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
7431 None
7432 }
7433
7434 fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
7435 None
7436 }
7437 }
7438
7439 impl Item for TestIpynbItemView {
7440 type Event = ();
7441 }
7442 impl EventEmitter<()> for TestIpynbItemView {}
7443 impl FocusableView for TestIpynbItemView {
7444 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7445 self.focus_handle.clone()
7446 }
7447 }
7448
7449 impl Render for TestIpynbItemView {
7450 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
7451 Empty
7452 }
7453 }
7454
7455 impl ProjectItem for TestIpynbItemView {
7456 type Item = TestIpynbItem;
7457
7458 fn for_project_item(
7459 _project: Model<Project>,
7460 _item: Model<Self::Item>,
7461 cx: &mut ViewContext<Self>,
7462 ) -> Self
7463 where
7464 Self: Sized,
7465 {
7466 Self {
7467 focus_handle: cx.focus_handle(),
7468 }
7469 }
7470 }
7471
7472 struct TestAlternatePngItemView {
7473 focus_handle: FocusHandle,
7474 }
7475
7476 impl Item for TestAlternatePngItemView {
7477 type Event = ();
7478 }
7479
7480 impl EventEmitter<()> for TestAlternatePngItemView {}
7481 impl FocusableView for TestAlternatePngItemView {
7482 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7483 self.focus_handle.clone()
7484 }
7485 }
7486
7487 impl Render for TestAlternatePngItemView {
7488 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
7489 Empty
7490 }
7491 }
7492
7493 impl ProjectItem for TestAlternatePngItemView {
7494 type Item = TestPngItem;
7495
7496 fn for_project_item(
7497 _project: Model<Project>,
7498 _item: Model<Self::Item>,
7499 cx: &mut ViewContext<Self>,
7500 ) -> Self
7501 where
7502 Self: Sized,
7503 {
7504 Self {
7505 focus_handle: cx.focus_handle(),
7506 }
7507 }
7508 }
7509
7510 #[gpui::test]
7511 async fn test_register_project_item(cx: &mut TestAppContext) {
7512 init_test(cx);
7513
7514 cx.update(|cx| {
7515 register_project_item::<TestPngItemView>(cx);
7516 register_project_item::<TestIpynbItemView>(cx);
7517 });
7518
7519 let fs = FakeFs::new(cx.executor());
7520 fs.insert_tree(
7521 "/root1",
7522 json!({
7523 "one.png": "BINARYDATAHERE",
7524 "two.ipynb": "{ totally a notebook }",
7525 "three.txt": "editing text, sure why not?"
7526 }),
7527 )
7528 .await;
7529
7530 let project = Project::test(fs, ["root1".as_ref()], cx).await;
7531 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
7532
7533 let worktree_id = project.update(cx, |project, cx| {
7534 project.worktrees(cx).next().unwrap().read(cx).id()
7535 });
7536
7537 let handle = workspace
7538 .update(cx, |workspace, cx| {
7539 let project_path = (worktree_id, "one.png");
7540 workspace.open_path(project_path, None, true, cx)
7541 })
7542 .await
7543 .unwrap();
7544
7545 // Now we can check if the handle we got back errored or not
7546 assert_eq!(
7547 handle.to_any().entity_type(),
7548 TypeId::of::<TestPngItemView>()
7549 );
7550
7551 let handle = workspace
7552 .update(cx, |workspace, cx| {
7553 let project_path = (worktree_id, "two.ipynb");
7554 workspace.open_path(project_path, None, true, cx)
7555 })
7556 .await
7557 .unwrap();
7558
7559 assert_eq!(
7560 handle.to_any().entity_type(),
7561 TypeId::of::<TestIpynbItemView>()
7562 );
7563
7564 let handle = workspace
7565 .update(cx, |workspace, cx| {
7566 let project_path = (worktree_id, "three.txt");
7567 workspace.open_path(project_path, None, true, cx)
7568 })
7569 .await;
7570 assert!(handle.is_err());
7571 }
7572
7573 #[gpui::test]
7574 async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
7575 init_test(cx);
7576
7577 cx.update(|cx| {
7578 register_project_item::<TestPngItemView>(cx);
7579 register_project_item::<TestAlternatePngItemView>(cx);
7580 });
7581
7582 let fs = FakeFs::new(cx.executor());
7583 fs.insert_tree(
7584 "/root1",
7585 json!({
7586 "one.png": "BINARYDATAHERE",
7587 "two.ipynb": "{ totally a notebook }",
7588 "three.txt": "editing text, sure why not?"
7589 }),
7590 )
7591 .await;
7592
7593 let project = Project::test(fs, ["root1".as_ref()], cx).await;
7594 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
7595
7596 let worktree_id = project.update(cx, |project, cx| {
7597 project.worktrees(cx).next().unwrap().read(cx).id()
7598 });
7599
7600 let handle = workspace
7601 .update(cx, |workspace, cx| {
7602 let project_path = (worktree_id, "one.png");
7603 workspace.open_path(project_path, None, true, cx)
7604 })
7605 .await
7606 .unwrap();
7607
7608 // This _must_ be the second item registered
7609 assert_eq!(
7610 handle.to_any().entity_type(),
7611 TypeId::of::<TestAlternatePngItemView>()
7612 );
7613
7614 let handle = workspace
7615 .update(cx, |workspace, cx| {
7616 let project_path = (worktree_id, "three.txt");
7617 workspace.open_path(project_path, None, true, cx)
7618 })
7619 .await;
7620 assert!(handle.is_err());
7621 }
7622 }
7623
7624 pub fn init_test(cx: &mut TestAppContext) {
7625 cx.update(|cx| {
7626 let settings_store = SettingsStore::test(cx);
7627 cx.set_global(settings_store);
7628 theme::init(theme::LoadThemes::JustBase, cx);
7629 language::init(cx);
7630 crate::init_settings(cx);
7631 Project::init_settings(cx);
7632 });
7633 }
7634}