1pub mod dock;
2pub mod history_manager;
3pub mod item;
4mod modal_layer;
5pub mod notifications;
6pub mod pane;
7pub mod pane_group;
8mod persistence;
9pub mod searchable;
10pub mod shared_screen;
11mod status_bar;
12pub mod tasks;
13mod theme_preview;
14mod toast_layer;
15mod toolbar;
16mod workspace_settings;
17
18pub use toast_layer::{RunAction, ToastAction, ToastLayer, ToastView};
19
20use anyhow::{Context as _, Result, anyhow};
21use call::{ActiveCall, call_settings::CallSettings};
22use client::{
23 ChannelId, Client, ErrorExt, Status, TypedEnvelope, UserStore,
24 proto::{self, ErrorCode, PanelId, PeerId},
25};
26use collections::{HashMap, HashSet, hash_map};
27pub use dock::Panel;
28use dock::{Dock, DockPosition, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE};
29use futures::{
30 Future, FutureExt, StreamExt,
31 channel::{
32 mpsc::{self, UnboundedReceiver, UnboundedSender},
33 oneshot,
34 },
35 future::try_join_all,
36};
37use gpui::{
38 Action, AnyEntity, AnyView, AnyWeakView, App, AsyncApp, AsyncWindowContext, Bounds, Context,
39 CursorStyle, Decorations, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle,
40 Focusable, Global, Hsla, KeyContext, Keystroke, ManagedView, MouseButton, PathPromptOptions,
41 Point, PromptLevel, Render, ResizeEdge, Size, Stateful, Subscription, Task, Tiling, WeakEntity,
42 WindowBounds, WindowHandle, WindowId, WindowOptions, action_as, actions, canvas,
43 impl_action_as, impl_actions, point, relative, size, transparent_black,
44};
45pub use history_manager::*;
46pub use item::{
47 FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
48 ProjectItem, SerializableItem, SerializableItemHandle, WeakItemHandle,
49};
50use itertools::Itertools;
51use language::{Buffer, LanguageRegistry, Rope};
52pub use modal_layer::*;
53use node_runtime::NodeRuntime;
54use notifications::{
55 DetachAndPromptErr, Notifications, dismiss_app_notification,
56 simple_message_notification::MessageNotification,
57};
58pub use pane::*;
59pub use pane_group::*;
60use persistence::{
61 DB, SerializedWindowBounds,
62 model::{SerializedSshProject, SerializedWorkspace},
63};
64pub use persistence::{
65 DB as WORKSPACE_DB, WorkspaceDb, delete_unloaded_items,
66 model::{ItemId, LocalPaths, SerializedWorkspaceLocation},
67};
68use postage::stream::Stream;
69use project::{
70 DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId,
71 debugger::breakpoint_store::BreakpointStoreEvent,
72};
73use remote::{SshClientDelegate, SshConnectionOptions, ssh_session::ConnectionIdentifier};
74use schemars::JsonSchema;
75use serde::Deserialize;
76use session::AppSession;
77use settings::Settings;
78use shared_screen::SharedScreen;
79use sqlez::{
80 bindable::{Bind, Column, StaticColumnCount},
81 statement::Statement,
82};
83use status_bar::StatusBar;
84pub use status_bar::StatusItemView;
85use std::{
86 any::TypeId,
87 borrow::Cow,
88 cell::RefCell,
89 cmp,
90 collections::hash_map::DefaultHasher,
91 env,
92 hash::{Hash, Hasher},
93 path::{Path, PathBuf},
94 process::ExitStatus,
95 rc::Rc,
96 sync::{Arc, LazyLock, Weak, atomic::AtomicUsize},
97 time::Duration,
98};
99use task::{DebugScenario, SpawnInTerminal, TaskContext};
100use theme::{ActiveTheme, SystemAppearance, ThemeSettings};
101pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
102pub use ui;
103use ui::prelude::*;
104use util::{ResultExt, TryFutureExt, paths::SanitizedPath, serde::default_true};
105use uuid::Uuid;
106pub use workspace_settings::{
107 AutosaveSetting, BottomDockLayout, RestoreOnStartupBehavior, TabBarSettings, WorkspaceSettings,
108};
109use zed_actions::feedback::FileBugReport;
110
111use crate::notifications::NotificationId;
112use crate::persistence::{
113 SerializedAxis,
114 model::{DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup},
115};
116
117pub const SERIALIZATION_THROTTLE_TIME: Duration = Duration::from_millis(200);
118
119static ZED_WINDOW_SIZE: LazyLock<Option<Size<Pixels>>> = LazyLock::new(|| {
120 env::var("ZED_WINDOW_SIZE")
121 .ok()
122 .as_deref()
123 .and_then(parse_pixel_size_env_var)
124});
125
126static ZED_WINDOW_POSITION: LazyLock<Option<Point<Pixels>>> = LazyLock::new(|| {
127 env::var("ZED_WINDOW_POSITION")
128 .ok()
129 .as_deref()
130 .and_then(parse_pixel_position_env_var)
131});
132
133pub trait TerminalProvider {
134 fn spawn(
135 &self,
136 task: SpawnInTerminal,
137 window: &mut Window,
138 cx: &mut App,
139 ) -> Task<Option<Result<ExitStatus>>>;
140}
141
142pub trait DebuggerProvider {
143 // `active_buffer` is used to resolve build task's name against language-specific tasks.
144 fn start_session(
145 &self,
146 definition: DebugScenario,
147 task_context: TaskContext,
148 active_buffer: Option<Entity<Buffer>>,
149 window: &mut Window,
150 cx: &mut App,
151 );
152}
153
154actions!(
155 workspace,
156 [
157 ActivateNextPane,
158 ActivatePreviousPane,
159 ActivateNextWindow,
160 ActivatePreviousWindow,
161 AddFolderToProject,
162 ClearAllNotifications,
163 CloseAllDocks,
164 CloseWindow,
165 Feedback,
166 FollowNextCollaborator,
167 MoveFocusedPanelToNextPosition,
168 NewCenterTerminal,
169 NewFile,
170 NewFileSplitVertical,
171 NewFileSplitHorizontal,
172 NewSearch,
173 NewTerminal,
174 NewWindow,
175 Open,
176 OpenFiles,
177 OpenInTerminal,
178 OpenComponentPreview,
179 ReloadActiveItem,
180 SaveAs,
181 SaveWithoutFormat,
182 ShutdownDebugAdapters,
183 SuppressNotification,
184 ToggleBottomDock,
185 ToggleCenteredLayout,
186 ToggleLeftDock,
187 ToggleRightDock,
188 ToggleZoom,
189 Unfollow,
190 Welcome,
191 RestoreBanner,
192 ]
193);
194
195#[derive(Clone, PartialEq)]
196pub struct OpenPaths {
197 pub paths: Vec<PathBuf>,
198}
199
200#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
201pub struct ActivatePane(pub usize);
202
203#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
204#[serde(deny_unknown_fields)]
205pub struct MoveItemToPane {
206 pub destination: usize,
207 #[serde(default = "default_true")]
208 pub focus: bool,
209}
210
211#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
212#[serde(deny_unknown_fields)]
213pub struct MoveItemToPaneInDirection {
214 pub direction: SplitDirection,
215 #[serde(default = "default_true")]
216 pub focus: bool,
217}
218
219#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema)]
220#[serde(deny_unknown_fields)]
221pub struct SaveAll {
222 pub save_intent: Option<SaveIntent>,
223}
224
225#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema)]
226#[serde(deny_unknown_fields)]
227pub struct Save {
228 pub save_intent: Option<SaveIntent>,
229}
230
231#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema)]
232#[serde(deny_unknown_fields)]
233pub struct CloseAllItemsAndPanes {
234 pub save_intent: Option<SaveIntent>,
235}
236
237#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema)]
238#[serde(deny_unknown_fields)]
239pub struct CloseInactiveTabsAndPanes {
240 pub save_intent: Option<SaveIntent>,
241}
242
243#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
244pub struct SendKeystrokes(pub String);
245
246#[derive(Clone, Deserialize, PartialEq, Default, JsonSchema)]
247#[serde(deny_unknown_fields)]
248pub struct Reload {
249 pub binary_path: Option<PathBuf>,
250}
251
252action_as!(project_symbols, ToggleProjectSymbols as Toggle);
253
254#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema)]
255pub struct ToggleFileFinder {
256 #[serde(default)]
257 pub separate_history: bool,
258}
259
260impl_action_as!(file_finder, ToggleFileFinder as Toggle);
261
262impl_actions!(
263 workspace,
264 [
265 ActivatePane,
266 CloseAllItemsAndPanes,
267 CloseInactiveTabsAndPanes,
268 MoveItemToPane,
269 MoveItemToPaneInDirection,
270 OpenTerminal,
271 Reload,
272 Save,
273 SaveAll,
274 SendKeystrokes,
275 ]
276);
277
278actions!(
279 workspace,
280 [
281 ActivatePaneLeft,
282 ActivatePaneRight,
283 ActivatePaneUp,
284 ActivatePaneDown,
285 SwapPaneLeft,
286 SwapPaneRight,
287 SwapPaneUp,
288 SwapPaneDown,
289 ]
290);
291
292#[derive(PartialEq, Eq, Debug)]
293pub enum CloseIntent {
294 /// Quit the program entirely.
295 Quit,
296 /// Close a window.
297 CloseWindow,
298 /// Replace the workspace in an existing window.
299 ReplaceWindow,
300}
301
302#[derive(Clone)]
303pub struct Toast {
304 id: NotificationId,
305 msg: Cow<'static, str>,
306 autohide: bool,
307 on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut Window, &mut App)>)>,
308}
309
310impl Toast {
311 pub fn new<I: Into<Cow<'static, str>>>(id: NotificationId, msg: I) -> Self {
312 Toast {
313 id,
314 msg: msg.into(),
315 on_click: None,
316 autohide: false,
317 }
318 }
319
320 pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
321 where
322 M: Into<Cow<'static, str>>,
323 F: Fn(&mut Window, &mut App) + 'static,
324 {
325 self.on_click = Some((message.into(), Arc::new(on_click)));
326 self
327 }
328
329 pub fn autohide(mut self) -> Self {
330 self.autohide = true;
331 self
332 }
333}
334
335impl PartialEq for Toast {
336 fn eq(&self, other: &Self) -> bool {
337 self.id == other.id
338 && self.msg == other.msg
339 && self.on_click.is_some() == other.on_click.is_some()
340 }
341}
342
343#[derive(Debug, Default, Clone, Deserialize, PartialEq, JsonSchema)]
344#[serde(deny_unknown_fields)]
345pub struct OpenTerminal {
346 pub working_directory: PathBuf,
347}
348
349#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
350pub struct WorkspaceId(i64);
351
352impl StaticColumnCount for WorkspaceId {}
353impl Bind for WorkspaceId {
354 fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
355 self.0.bind(statement, start_index)
356 }
357}
358impl Column for WorkspaceId {
359 fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
360 i64::column(statement, start_index)
361 .map(|(i, next_index)| (Self(i), next_index))
362 .with_context(|| format!("Failed to read WorkspaceId at index {start_index}"))
363 }
364}
365impl From<WorkspaceId> for i64 {
366 fn from(val: WorkspaceId) -> Self {
367 val.0
368 }
369}
370
371pub fn init_settings(cx: &mut App) {
372 WorkspaceSettings::register(cx);
373 ItemSettings::register(cx);
374 PreviewTabsSettings::register(cx);
375 TabBarSettings::register(cx);
376}
377
378fn prompt_and_open_paths(app_state: Arc<AppState>, options: PathPromptOptions, cx: &mut App) {
379 let paths = cx.prompt_for_paths(options);
380 cx.spawn(
381 async move |cx| match paths.await.anyhow().and_then(|res| res) {
382 Ok(Some(paths)) => {
383 cx.update(|cx| {
384 open_paths(&paths, app_state, OpenOptions::default(), cx).detach_and_log_err(cx)
385 })
386 .ok();
387 }
388 Ok(None) => {}
389 Err(err) => {
390 util::log_err(&err);
391 cx.update(|cx| {
392 if let Some(workspace_window) = cx
393 .active_window()
394 .and_then(|window| window.downcast::<Workspace>())
395 {
396 workspace_window
397 .update(cx, |workspace, _, cx| {
398 workspace.show_portal_error(err.to_string(), cx);
399 })
400 .ok();
401 }
402 })
403 .ok();
404 }
405 },
406 )
407 .detach();
408}
409
410pub fn init(app_state: Arc<AppState>, cx: &mut App) {
411 init_settings(cx);
412 component::init();
413 theme_preview::init(cx);
414 toast_layer::init(cx);
415 history_manager::init(cx);
416
417 cx.on_action(Workspace::close_global);
418 cx.on_action(reload);
419
420 cx.on_action({
421 let app_state = Arc::downgrade(&app_state);
422 move |_: &Open, cx: &mut App| {
423 if let Some(app_state) = app_state.upgrade() {
424 prompt_and_open_paths(
425 app_state,
426 PathPromptOptions {
427 files: true,
428 directories: true,
429 multiple: true,
430 },
431 cx,
432 );
433 }
434 }
435 });
436 cx.on_action({
437 let app_state = Arc::downgrade(&app_state);
438 move |_: &OpenFiles, cx: &mut App| {
439 let directories = cx.can_select_mixed_files_and_dirs();
440 if let Some(app_state) = app_state.upgrade() {
441 prompt_and_open_paths(
442 app_state,
443 PathPromptOptions {
444 files: true,
445 directories,
446 multiple: true,
447 },
448 cx,
449 );
450 }
451 }
452 });
453}
454
455type BuildProjectItemFn =
456 fn(AnyEntity, Entity<Project>, Option<&Pane>, &mut Window, &mut App) -> Box<dyn ItemHandle>;
457
458type BuildProjectItemForPathFn =
459 fn(
460 &Entity<Project>,
461 &ProjectPath,
462 &mut Window,
463 &mut App,
464 ) -> Option<Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>>>;
465
466#[derive(Clone, Default)]
467struct ProjectItemRegistry {
468 build_project_item_fns_by_type: HashMap<TypeId, BuildProjectItemFn>,
469 build_project_item_for_path_fns: Vec<BuildProjectItemForPathFn>,
470}
471
472impl ProjectItemRegistry {
473 fn register<T: ProjectItem>(&mut self) {
474 self.build_project_item_fns_by_type.insert(
475 TypeId::of::<T::Item>(),
476 |item, project, pane, window, cx| {
477 let item = item.downcast().unwrap();
478 Box::new(cx.new(|cx| T::for_project_item(project, pane, item, window, cx)))
479 as Box<dyn ItemHandle>
480 },
481 );
482 self.build_project_item_for_path_fns
483 .push(|project, project_path, window, cx| {
484 let project_item =
485 <T::Item as project::ProjectItem>::try_open(project, project_path, cx)?;
486 let project = project.clone();
487 Some(window.spawn(cx, async move |cx| {
488 let project_item = project_item.await?;
489 let project_entry_id: Option<ProjectEntryId> =
490 project_item.read_with(cx, project::ProjectItem::entry_id)?;
491 let build_workspace_item = Box::new(
492 |pane: &mut Pane, window: &mut Window, cx: &mut Context<Pane>| {
493 Box::new(cx.new(|cx| {
494 T::for_project_item(project, Some(pane), project_item, window, cx)
495 })) as Box<dyn ItemHandle>
496 },
497 ) as Box<_>;
498 Ok((project_entry_id, build_workspace_item))
499 }))
500 });
501 }
502
503 fn open_path(
504 &self,
505 project: &Entity<Project>,
506 path: &ProjectPath,
507 window: &mut Window,
508 cx: &mut App,
509 ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
510 let Some(open_project_item) = self
511 .build_project_item_for_path_fns
512 .iter()
513 .rev()
514 .find_map(|open_project_item| open_project_item(&project, &path, window, cx))
515 else {
516 return Task::ready(Err(anyhow!("cannot open file {:?}", path.path)));
517 };
518 open_project_item
519 }
520
521 fn build_item<T: project::ProjectItem>(
522 &self,
523 item: Entity<T>,
524 project: Entity<Project>,
525 pane: Option<&Pane>,
526 window: &mut Window,
527 cx: &mut App,
528 ) -> Option<Box<dyn ItemHandle>> {
529 let build = self
530 .build_project_item_fns_by_type
531 .get(&TypeId::of::<T>())?;
532 Some(build(item.into_any(), project, pane, window, cx))
533 }
534}
535
536type WorkspaceItemBuilder =
537 Box<dyn FnOnce(&mut Pane, &mut Window, &mut Context<Pane>) -> Box<dyn ItemHandle>>;
538
539impl Global for ProjectItemRegistry {}
540
541/// Registers a [ProjectItem] for the app. When opening a file, all the registered
542/// items will get a chance to open the file, starting from the project item that
543/// was added last.
544pub fn register_project_item<I: ProjectItem>(cx: &mut App) {
545 cx.default_global::<ProjectItemRegistry>().register::<I>();
546}
547
548#[derive(Default)]
549pub struct FollowableViewRegistry(HashMap<TypeId, FollowableViewDescriptor>);
550
551struct FollowableViewDescriptor {
552 from_state_proto: fn(
553 Entity<Workspace>,
554 ViewId,
555 &mut Option<proto::view::Variant>,
556 &mut Window,
557 &mut App,
558 ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>,
559 to_followable_view: fn(&AnyView) -> Box<dyn FollowableItemHandle>,
560}
561
562impl Global for FollowableViewRegistry {}
563
564impl FollowableViewRegistry {
565 pub fn register<I: FollowableItem>(cx: &mut App) {
566 cx.default_global::<Self>().0.insert(
567 TypeId::of::<I>(),
568 FollowableViewDescriptor {
569 from_state_proto: |workspace, id, state, window, cx| {
570 I::from_state_proto(workspace, id, state, window, cx).map(|task| {
571 cx.foreground_executor()
572 .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
573 })
574 },
575 to_followable_view: |view| Box::new(view.clone().downcast::<I>().unwrap()),
576 },
577 );
578 }
579
580 pub fn from_state_proto(
581 workspace: Entity<Workspace>,
582 view_id: ViewId,
583 mut state: Option<proto::view::Variant>,
584 window: &mut Window,
585 cx: &mut App,
586 ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>> {
587 cx.update_default_global(|this: &mut Self, cx| {
588 this.0.values().find_map(|descriptor| {
589 (descriptor.from_state_proto)(workspace.clone(), view_id, &mut state, window, cx)
590 })
591 })
592 }
593
594 pub fn to_followable_view(
595 view: impl Into<AnyView>,
596 cx: &App,
597 ) -> Option<Box<dyn FollowableItemHandle>> {
598 let this = cx.try_global::<Self>()?;
599 let view = view.into();
600 let descriptor = this.0.get(&view.entity_type())?;
601 Some((descriptor.to_followable_view)(&view))
602 }
603}
604
605#[derive(Copy, Clone)]
606struct SerializableItemDescriptor {
607 deserialize: fn(
608 Entity<Project>,
609 WeakEntity<Workspace>,
610 WorkspaceId,
611 ItemId,
612 &mut Window,
613 &mut Context<Pane>,
614 ) -> Task<Result<Box<dyn ItemHandle>>>,
615 cleanup: fn(WorkspaceId, Vec<ItemId>, &mut Window, &mut App) -> Task<Result<()>>,
616 view_to_serializable_item: fn(AnyView) -> Box<dyn SerializableItemHandle>,
617}
618
619#[derive(Default)]
620struct SerializableItemRegistry {
621 descriptors_by_kind: HashMap<Arc<str>, SerializableItemDescriptor>,
622 descriptors_by_type: HashMap<TypeId, SerializableItemDescriptor>,
623}
624
625impl Global for SerializableItemRegistry {}
626
627impl SerializableItemRegistry {
628 fn deserialize(
629 item_kind: &str,
630 project: Entity<Project>,
631 workspace: WeakEntity<Workspace>,
632 workspace_id: WorkspaceId,
633 item_item: ItemId,
634 window: &mut Window,
635 cx: &mut Context<Pane>,
636 ) -> Task<Result<Box<dyn ItemHandle>>> {
637 let Some(descriptor) = Self::descriptor(item_kind, cx) else {
638 return Task::ready(Err(anyhow!(
639 "cannot deserialize {}, descriptor not found",
640 item_kind
641 )));
642 };
643
644 (descriptor.deserialize)(project, workspace, workspace_id, item_item, window, cx)
645 }
646
647 fn cleanup(
648 item_kind: &str,
649 workspace_id: WorkspaceId,
650 loaded_items: Vec<ItemId>,
651 window: &mut Window,
652 cx: &mut App,
653 ) -> Task<Result<()>> {
654 let Some(descriptor) = Self::descriptor(item_kind, cx) else {
655 return Task::ready(Err(anyhow!(
656 "cannot cleanup {}, descriptor not found",
657 item_kind
658 )));
659 };
660
661 (descriptor.cleanup)(workspace_id, loaded_items, window, cx)
662 }
663
664 fn view_to_serializable_item_handle(
665 view: AnyView,
666 cx: &App,
667 ) -> Option<Box<dyn SerializableItemHandle>> {
668 let this = cx.try_global::<Self>()?;
669 let descriptor = this.descriptors_by_type.get(&view.entity_type())?;
670 Some((descriptor.view_to_serializable_item)(view))
671 }
672
673 fn descriptor(item_kind: &str, cx: &App) -> Option<SerializableItemDescriptor> {
674 let this = cx.try_global::<Self>()?;
675 this.descriptors_by_kind.get(item_kind).copied()
676 }
677}
678
679pub fn register_serializable_item<I: SerializableItem>(cx: &mut App) {
680 let serialized_item_kind = I::serialized_item_kind();
681
682 let registry = cx.default_global::<SerializableItemRegistry>();
683 let descriptor = SerializableItemDescriptor {
684 deserialize: |project, workspace, workspace_id, item_id, window, cx| {
685 let task = I::deserialize(project, workspace, workspace_id, item_id, window, cx);
686 cx.foreground_executor()
687 .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
688 },
689 cleanup: |workspace_id, loaded_items, window, cx| {
690 I::cleanup(workspace_id, loaded_items, window, cx)
691 },
692 view_to_serializable_item: |view| Box::new(view.downcast::<I>().unwrap()),
693 };
694 registry
695 .descriptors_by_kind
696 .insert(Arc::from(serialized_item_kind), descriptor);
697 registry
698 .descriptors_by_type
699 .insert(TypeId::of::<I>(), descriptor);
700}
701
702pub struct AppState {
703 pub languages: Arc<LanguageRegistry>,
704 pub client: Arc<Client>,
705 pub user_store: Entity<UserStore>,
706 pub workspace_store: Entity<WorkspaceStore>,
707 pub fs: Arc<dyn fs::Fs>,
708 pub build_window_options: fn(Option<Uuid>, &mut App) -> WindowOptions,
709 pub node_runtime: NodeRuntime,
710 pub session: Entity<AppSession>,
711}
712
713struct GlobalAppState(Weak<AppState>);
714
715impl Global for GlobalAppState {}
716
717pub struct WorkspaceStore {
718 workspaces: HashSet<WindowHandle<Workspace>>,
719 client: Arc<Client>,
720 _subscriptions: Vec<client::Subscription>,
721}
722
723#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
724pub enum CollaboratorId {
725 PeerId(PeerId),
726 Agent,
727}
728
729impl From<PeerId> for CollaboratorId {
730 fn from(peer_id: PeerId) -> Self {
731 CollaboratorId::PeerId(peer_id)
732 }
733}
734
735impl From<&PeerId> for CollaboratorId {
736 fn from(peer_id: &PeerId) -> Self {
737 CollaboratorId::PeerId(*peer_id)
738 }
739}
740
741#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
742struct Follower {
743 project_id: Option<u64>,
744 peer_id: PeerId,
745}
746
747impl AppState {
748 #[track_caller]
749 pub fn global(cx: &App) -> Weak<Self> {
750 cx.global::<GlobalAppState>().0.clone()
751 }
752 pub fn try_global(cx: &App) -> Option<Weak<Self>> {
753 cx.try_global::<GlobalAppState>()
754 .map(|state| state.0.clone())
755 }
756 pub fn set_global(state: Weak<AppState>, cx: &mut App) {
757 cx.set_global(GlobalAppState(state));
758 }
759
760 #[cfg(any(test, feature = "test-support"))]
761 pub fn test(cx: &mut App) -> Arc<Self> {
762 use node_runtime::NodeRuntime;
763 use session::Session;
764 use settings::SettingsStore;
765
766 if !cx.has_global::<SettingsStore>() {
767 let settings_store = SettingsStore::test(cx);
768 cx.set_global(settings_store);
769 }
770
771 let fs = fs::FakeFs::new(cx.background_executor().clone());
772 let languages = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
773 let clock = Arc::new(clock::FakeSystemClock::new());
774 let http_client = http_client::FakeHttpClient::with_404_response();
775 let client = Client::new(clock, http_client.clone(), cx);
776 let session = cx.new(|cx| AppSession::new(Session::test(), cx));
777 let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
778 let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
779
780 theme::init(theme::LoadThemes::JustBase, cx);
781 client::init(&client, cx);
782 crate::init_settings(cx);
783
784 Arc::new(Self {
785 client,
786 fs,
787 languages,
788 user_store,
789 workspace_store,
790 node_runtime: NodeRuntime::unavailable(),
791 build_window_options: |_, _| Default::default(),
792 session,
793 })
794 }
795}
796
797struct DelayedDebouncedEditAction {
798 task: Option<Task<()>>,
799 cancel_channel: Option<oneshot::Sender<()>>,
800}
801
802impl DelayedDebouncedEditAction {
803 fn new() -> DelayedDebouncedEditAction {
804 DelayedDebouncedEditAction {
805 task: None,
806 cancel_channel: None,
807 }
808 }
809
810 fn fire_new<F>(
811 &mut self,
812 delay: Duration,
813 window: &mut Window,
814 cx: &mut Context<Workspace>,
815 func: F,
816 ) where
817 F: 'static
818 + Send
819 + FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> Task<Result<()>>,
820 {
821 if let Some(channel) = self.cancel_channel.take() {
822 _ = channel.send(());
823 }
824
825 let (sender, mut receiver) = oneshot::channel::<()>();
826 self.cancel_channel = Some(sender);
827
828 let previous_task = self.task.take();
829 self.task = Some(cx.spawn_in(window, async move |workspace, cx| {
830 let mut timer = cx.background_executor().timer(delay).fuse();
831 if let Some(previous_task) = previous_task {
832 previous_task.await;
833 }
834
835 futures::select_biased! {
836 _ = receiver => return,
837 _ = timer => {}
838 }
839
840 if let Some(result) = workspace
841 .update_in(cx, |workspace, window, cx| (func)(workspace, window, cx))
842 .log_err()
843 {
844 result.await.log_err();
845 }
846 }));
847 }
848}
849
850pub enum Event {
851 PaneAdded(Entity<Pane>),
852 PaneRemoved,
853 ItemAdded {
854 item: Box<dyn ItemHandle>,
855 },
856 ItemRemoved,
857 ActiveItemChanged,
858 UserSavedItem {
859 pane: WeakEntity<Pane>,
860 item: Box<dyn WeakItemHandle>,
861 save_intent: SaveIntent,
862 },
863 ContactRequestedJoin(u64),
864 WorkspaceCreated(WeakEntity<Workspace>),
865 OpenBundledFile {
866 text: Cow<'static, str>,
867 title: &'static str,
868 language: &'static str,
869 },
870 ZoomChanged,
871 ModalOpened,
872}
873
874#[derive(Debug)]
875pub enum OpenVisible {
876 All,
877 None,
878 OnlyFiles,
879 OnlyDirectories,
880}
881
882type PromptForNewPath = Box<
883 dyn Fn(
884 &mut Workspace,
885 &mut Window,
886 &mut Context<Workspace>,
887 ) -> oneshot::Receiver<Option<ProjectPath>>,
888>;
889
890type PromptForOpenPath = Box<
891 dyn Fn(
892 &mut Workspace,
893 DirectoryLister,
894 &mut Window,
895 &mut Context<Workspace>,
896 ) -> oneshot::Receiver<Option<Vec<PathBuf>>>,
897>;
898
899/// Collects everything project-related for a certain window opened.
900/// In some way, is a counterpart of a window, as the [`WindowHandle`] could be downcast into `Workspace`.
901///
902/// A `Workspace` usually consists of 1 or more projects, a central pane group, 3 docks and a status bar.
903/// The `Workspace` owns everybody's state and serves as a default, "global context",
904/// that can be used to register a global action to be triggered from any place in the window.
905pub struct Workspace {
906 weak_self: WeakEntity<Self>,
907 workspace_actions: Vec<Box<dyn Fn(Div, &mut Window, &mut Context<Self>) -> Div>>,
908 zoomed: Option<AnyWeakView>,
909 previous_dock_drag_coordinates: Option<Point<Pixels>>,
910 zoomed_position: Option<DockPosition>,
911 center: PaneGroup,
912 left_dock: Entity<Dock>,
913 bottom_dock: Entity<Dock>,
914 bottom_dock_layout: BottomDockLayout,
915 right_dock: Entity<Dock>,
916 panes: Vec<Entity<Pane>>,
917 panes_by_item: HashMap<EntityId, WeakEntity<Pane>>,
918 active_pane: Entity<Pane>,
919 last_active_center_pane: Option<WeakEntity<Pane>>,
920 last_active_view_id: Option<proto::ViewId>,
921 status_bar: Entity<StatusBar>,
922 modal_layer: Entity<ModalLayer>,
923 toast_layer: Entity<ToastLayer>,
924 titlebar_item: Option<AnyView>,
925 notifications: Notifications,
926 suppressed_notifications: HashSet<NotificationId>,
927 project: Entity<Project>,
928 follower_states: HashMap<CollaboratorId, FollowerState>,
929 last_leaders_by_pane: HashMap<WeakEntity<Pane>, CollaboratorId>,
930 window_edited: bool,
931 dirty_items: HashMap<EntityId, Subscription>,
932 active_call: Option<(Entity<ActiveCall>, Vec<Subscription>)>,
933 leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
934 database_id: Option<WorkspaceId>,
935 app_state: Arc<AppState>,
936 dispatching_keystrokes: Rc<RefCell<(HashSet<String>, Vec<Keystroke>)>>,
937 _subscriptions: Vec<Subscription>,
938 _apply_leader_updates: Task<Result<()>>,
939 _observe_current_user: Task<Result<()>>,
940 _schedule_serialize: Option<Task<()>>,
941 pane_history_timestamp: Arc<AtomicUsize>,
942 bounds: Bounds<Pixels>,
943 pub centered_layout: bool,
944 bounds_save_task_queued: Option<Task<()>>,
945 on_prompt_for_new_path: Option<PromptForNewPath>,
946 on_prompt_for_open_path: Option<PromptForOpenPath>,
947 terminal_provider: Option<Box<dyn TerminalProvider>>,
948 debugger_provider: Option<Box<dyn DebuggerProvider>>,
949 serializable_items_tx: UnboundedSender<Box<dyn SerializableItemHandle>>,
950 serialized_ssh_project: Option<SerializedSshProject>,
951 _items_serializer: Task<Result<()>>,
952 session_id: Option<String>,
953}
954
955impl EventEmitter<Event> for Workspace {}
956
957#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
958pub struct ViewId {
959 pub creator: CollaboratorId,
960 pub id: u64,
961}
962
963pub struct FollowerState {
964 center_pane: Entity<Pane>,
965 dock_pane: Option<Entity<Pane>>,
966 active_view_id: Option<ViewId>,
967 items_by_leader_view_id: HashMap<ViewId, FollowerView>,
968}
969
970struct FollowerView {
971 view: Box<dyn FollowableItemHandle>,
972 location: Option<proto::PanelId>,
973}
974
975impl Workspace {
976 const DEFAULT_PADDING: f32 = 0.2;
977 const MAX_PADDING: f32 = 0.4;
978
979 pub fn new(
980 workspace_id: Option<WorkspaceId>,
981 project: Entity<Project>,
982 app_state: Arc<AppState>,
983 window: &mut Window,
984 cx: &mut Context<Self>,
985 ) -> Self {
986 cx.subscribe_in(&project, window, move |this, _, event, window, cx| {
987 match event {
988 project::Event::RemoteIdChanged(_) => {
989 this.update_window_title(window, cx);
990 }
991
992 project::Event::CollaboratorLeft(peer_id) => {
993 this.collaborator_left(*peer_id, window, cx);
994 }
995
996 project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded(_) => {
997 this.update_window_title(window, cx);
998 this.serialize_workspace(window, cx);
999 // This event could be triggered by `AddFolderToProject` or `RemoveFromProject`.
1000 // So we need to update the history.
1001 this.update_history(cx);
1002 }
1003
1004 project::Event::DisconnectedFromHost => {
1005 this.update_window_edited(window, cx);
1006 let leaders_to_unfollow =
1007 this.follower_states.keys().copied().collect::<Vec<_>>();
1008 for leader_id in leaders_to_unfollow {
1009 this.unfollow(leader_id, window, cx);
1010 }
1011 }
1012
1013 project::Event::DisconnectedFromSshRemote => {
1014 this.update_window_edited(window, cx);
1015 }
1016
1017 project::Event::Closed => {
1018 window.remove_window();
1019 }
1020
1021 project::Event::DeletedEntry(_, entry_id) => {
1022 for pane in this.panes.iter() {
1023 pane.update(cx, |pane, cx| {
1024 pane.handle_deleted_project_item(*entry_id, window, cx)
1025 });
1026 }
1027 }
1028
1029 project::Event::Toast {
1030 notification_id,
1031 message,
1032 } => this.show_notification(
1033 NotificationId::named(notification_id.clone()),
1034 cx,
1035 |cx| cx.new(|cx| MessageNotification::new(message.clone(), cx)),
1036 ),
1037
1038 project::Event::HideToast { notification_id } => {
1039 this.dismiss_notification(&NotificationId::named(notification_id.clone()), cx)
1040 }
1041
1042 project::Event::LanguageServerPrompt(request) => {
1043 struct LanguageServerPrompt;
1044
1045 let mut hasher = DefaultHasher::new();
1046 request.lsp_name.as_str().hash(&mut hasher);
1047 let id = hasher.finish();
1048
1049 this.show_notification(
1050 NotificationId::composite::<LanguageServerPrompt>(id as usize),
1051 cx,
1052 |cx| {
1053 cx.new(|cx| {
1054 notifications::LanguageServerPrompt::new(request.clone(), cx)
1055 })
1056 },
1057 );
1058 }
1059
1060 project::Event::AgentLocationChanged => {
1061 this.handle_agent_location_changed(window, cx)
1062 }
1063
1064 _ => {}
1065 }
1066 cx.notify()
1067 })
1068 .detach();
1069
1070 cx.subscribe_in(
1071 &project.read(cx).breakpoint_store(),
1072 window,
1073 |workspace, _, event, window, cx| match event {
1074 BreakpointStoreEvent::BreakpointsUpdated(_, _)
1075 | BreakpointStoreEvent::BreakpointsCleared(_) => {
1076 workspace.serialize_workspace(window, cx);
1077 }
1078 BreakpointStoreEvent::SetDebugLine | BreakpointStoreEvent::ClearDebugLines => {}
1079 },
1080 )
1081 .detach();
1082
1083 cx.on_focus_lost(window, |this, window, cx| {
1084 let focus_handle = this.focus_handle(cx);
1085 window.focus(&focus_handle);
1086 })
1087 .detach();
1088
1089 let weak_handle = cx.entity().downgrade();
1090 let pane_history_timestamp = Arc::new(AtomicUsize::new(0));
1091
1092 let center_pane = cx.new(|cx| {
1093 let mut center_pane = Pane::new(
1094 weak_handle.clone(),
1095 project.clone(),
1096 pane_history_timestamp.clone(),
1097 None,
1098 NewFile.boxed_clone(),
1099 window,
1100 cx,
1101 );
1102 center_pane.set_can_split(Some(Arc::new(|_, _, _, _| true)));
1103 center_pane
1104 });
1105 cx.subscribe_in(¢er_pane, window, Self::handle_pane_event)
1106 .detach();
1107
1108 window.focus(¢er_pane.focus_handle(cx));
1109
1110 cx.emit(Event::PaneAdded(center_pane.clone()));
1111
1112 let window_handle = window.window_handle().downcast::<Workspace>().unwrap();
1113 app_state.workspace_store.update(cx, |store, _| {
1114 store.workspaces.insert(window_handle);
1115 });
1116
1117 let mut current_user = app_state.user_store.read(cx).watch_current_user();
1118 let mut connection_status = app_state.client.status();
1119 let _observe_current_user = cx.spawn_in(window, async move |this, cx| {
1120 current_user.next().await;
1121 connection_status.next().await;
1122 let mut stream =
1123 Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
1124
1125 while stream.recv().await.is_some() {
1126 this.update(cx, |_, cx| cx.notify())?;
1127 }
1128 anyhow::Ok(())
1129 });
1130
1131 // All leader updates are enqueued and then processed in a single task, so
1132 // that each asynchronous operation can be run in order.
1133 let (leader_updates_tx, mut leader_updates_rx) =
1134 mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
1135 let _apply_leader_updates = cx.spawn_in(window, async move |this, cx| {
1136 while let Some((leader_id, update)) = leader_updates_rx.next().await {
1137 Self::process_leader_update(&this, leader_id, update, cx)
1138 .await
1139 .log_err();
1140 }
1141
1142 Ok(())
1143 });
1144
1145 cx.emit(Event::WorkspaceCreated(weak_handle.clone()));
1146 let modal_layer = cx.new(|_| ModalLayer::new());
1147 let toast_layer = cx.new(|_| ToastLayer::new());
1148 cx.subscribe(
1149 &modal_layer,
1150 |_, _, _: &modal_layer::ModalOpenedEvent, cx| {
1151 cx.emit(Event::ModalOpened);
1152 },
1153 )
1154 .detach();
1155
1156 let bottom_dock_layout = WorkspaceSettings::get_global(cx).bottom_dock_layout;
1157 let left_dock = Dock::new(DockPosition::Left, modal_layer.clone(), window, cx);
1158 let bottom_dock = Dock::new(DockPosition::Bottom, modal_layer.clone(), window, cx);
1159 let right_dock = Dock::new(DockPosition::Right, modal_layer.clone(), window, cx);
1160 let left_dock_buttons = cx.new(|cx| PanelButtons::new(left_dock.clone(), cx));
1161 let bottom_dock_buttons = cx.new(|cx| PanelButtons::new(bottom_dock.clone(), cx));
1162 let right_dock_buttons = cx.new(|cx| PanelButtons::new(right_dock.clone(), cx));
1163 let status_bar = cx.new(|cx| {
1164 let mut status_bar = StatusBar::new(¢er_pane.clone(), window, cx);
1165 status_bar.add_left_item(left_dock_buttons, window, cx);
1166 status_bar.add_right_item(right_dock_buttons, window, cx);
1167 status_bar.add_right_item(bottom_dock_buttons, window, cx);
1168 status_bar
1169 });
1170
1171 let session_id = app_state.session.read(cx).id().to_owned();
1172
1173 let mut active_call = None;
1174 if let Some(call) = ActiveCall::try_global(cx) {
1175 let call = call.clone();
1176 let subscriptions = vec![cx.subscribe_in(&call, window, Self::on_active_call_event)];
1177 active_call = Some((call, subscriptions));
1178 }
1179
1180 let (serializable_items_tx, serializable_items_rx) =
1181 mpsc::unbounded::<Box<dyn SerializableItemHandle>>();
1182 let _items_serializer = cx.spawn_in(window, async move |this, cx| {
1183 Self::serialize_items(&this, serializable_items_rx, cx).await
1184 });
1185
1186 let subscriptions = vec![
1187 cx.observe_window_activation(window, Self::on_window_activation_changed),
1188 cx.observe_window_bounds(window, move |this, window, cx| {
1189 if this.bounds_save_task_queued.is_some() {
1190 return;
1191 }
1192 this.bounds_save_task_queued = Some(cx.spawn_in(window, async move |this, cx| {
1193 cx.background_executor()
1194 .timer(Duration::from_millis(100))
1195 .await;
1196 this.update_in(cx, |this, window, cx| {
1197 if let Some(display) = window.display(cx) {
1198 if let Ok(display_uuid) = display.uuid() {
1199 let window_bounds = window.inner_window_bounds();
1200 if let Some(database_id) = workspace_id {
1201 cx.background_executor()
1202 .spawn(DB.set_window_open_status(
1203 database_id,
1204 SerializedWindowBounds(window_bounds),
1205 display_uuid,
1206 ))
1207 .detach_and_log_err(cx);
1208 }
1209 }
1210 }
1211 this.bounds_save_task_queued.take();
1212 })
1213 .ok();
1214 }));
1215 cx.notify();
1216 }),
1217 cx.observe_window_appearance(window, |_, window, cx| {
1218 let window_appearance = window.appearance();
1219
1220 *SystemAppearance::global_mut(cx) = SystemAppearance(window_appearance.into());
1221
1222 ThemeSettings::reload_current_theme(cx);
1223 ThemeSettings::reload_current_icon_theme(cx);
1224 }),
1225 cx.on_release(move |this, cx| {
1226 this.app_state.workspace_store.update(cx, move |store, _| {
1227 store.workspaces.remove(&window_handle.clone());
1228 })
1229 }),
1230 ];
1231
1232 cx.defer_in(window, |this, window, cx| {
1233 this.update_window_title(window, cx);
1234 this.show_initial_notifications(cx);
1235 });
1236 Workspace {
1237 weak_self: weak_handle.clone(),
1238 zoomed: None,
1239 zoomed_position: None,
1240 previous_dock_drag_coordinates: None,
1241 center: PaneGroup::new(center_pane.clone()),
1242 panes: vec![center_pane.clone()],
1243 panes_by_item: Default::default(),
1244 active_pane: center_pane.clone(),
1245 last_active_center_pane: Some(center_pane.downgrade()),
1246 last_active_view_id: None,
1247 status_bar,
1248 modal_layer,
1249 toast_layer,
1250 titlebar_item: None,
1251 notifications: Notifications::default(),
1252 suppressed_notifications: HashSet::default(),
1253 left_dock,
1254 bottom_dock,
1255 bottom_dock_layout,
1256 right_dock,
1257 project: project.clone(),
1258 follower_states: Default::default(),
1259 last_leaders_by_pane: Default::default(),
1260 dispatching_keystrokes: Default::default(),
1261 window_edited: false,
1262 dirty_items: Default::default(),
1263 active_call,
1264 database_id: workspace_id,
1265 app_state,
1266 _observe_current_user,
1267 _apply_leader_updates,
1268 _schedule_serialize: None,
1269 leader_updates_tx,
1270 _subscriptions: subscriptions,
1271 pane_history_timestamp,
1272 workspace_actions: Default::default(),
1273 // This data will be incorrect, but it will be overwritten by the time it needs to be used.
1274 bounds: Default::default(),
1275 centered_layout: false,
1276 bounds_save_task_queued: None,
1277 on_prompt_for_new_path: None,
1278 on_prompt_for_open_path: None,
1279 terminal_provider: None,
1280 debugger_provider: None,
1281 serializable_items_tx,
1282 _items_serializer,
1283 session_id: Some(session_id),
1284 serialized_ssh_project: None,
1285 }
1286 }
1287
1288 pub fn new_local(
1289 abs_paths: Vec<PathBuf>,
1290 app_state: Arc<AppState>,
1291 requesting_window: Option<WindowHandle<Workspace>>,
1292 env: Option<HashMap<String, String>>,
1293 cx: &mut App,
1294 ) -> Task<
1295 anyhow::Result<(
1296 WindowHandle<Workspace>,
1297 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
1298 )>,
1299 > {
1300 let project_handle = Project::local(
1301 app_state.client.clone(),
1302 app_state.node_runtime.clone(),
1303 app_state.user_store.clone(),
1304 app_state.languages.clone(),
1305 app_state.fs.clone(),
1306 env,
1307 cx,
1308 );
1309
1310 cx.spawn(async move |cx| {
1311 let mut paths_to_open = Vec::with_capacity(abs_paths.len());
1312 for path in abs_paths.into_iter() {
1313 if let Some(canonical) = app_state.fs.canonicalize(&path).await.ok() {
1314 paths_to_open.push(canonical)
1315 } else {
1316 paths_to_open.push(path)
1317 }
1318 }
1319
1320 let serialized_workspace =
1321 persistence::DB.workspace_for_roots(paths_to_open.as_slice());
1322
1323 let workspace_location = serialized_workspace
1324 .as_ref()
1325 .map(|ws| &ws.location)
1326 .and_then(|loc| match loc {
1327 SerializedWorkspaceLocation::Local(_, order) => {
1328 Some((loc.sorted_paths(), order.order()))
1329 }
1330 _ => None,
1331 });
1332
1333 if let Some((paths, order)) = workspace_location {
1334 paths_to_open = paths.iter().cloned().collect();
1335
1336 if order.iter().enumerate().any(|(i, &j)| i != j) {
1337 project_handle
1338 .update(cx, |project, cx| {
1339 project.set_worktrees_reordered(true, cx);
1340 })
1341 .log_err();
1342 }
1343 }
1344
1345 // Get project paths for all of the abs_paths
1346 let mut project_paths: Vec<(PathBuf, Option<ProjectPath>)> =
1347 Vec::with_capacity(paths_to_open.len());
1348
1349 for path in paths_to_open.into_iter() {
1350 if let Some((_, project_entry)) = cx
1351 .update(|cx| {
1352 Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
1353 })?
1354 .await
1355 .log_err()
1356 {
1357 project_paths.push((path, Some(project_entry)));
1358 } else {
1359 project_paths.push((path, None));
1360 }
1361 }
1362
1363 let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
1364 serialized_workspace.id
1365 } else {
1366 DB.next_id().await.unwrap_or_else(|_| Default::default())
1367 };
1368
1369 let toolchains = DB.toolchains(workspace_id).await?;
1370 for (toolchain, worktree_id, path) in toolchains {
1371 project_handle
1372 .update(cx, |this, cx| {
1373 this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx)
1374 })?
1375 .await;
1376 }
1377 let window = if let Some(window) = requesting_window {
1378 cx.update_window(window.into(), |_, window, cx| {
1379 window.replace_root(cx, |window, cx| {
1380 Workspace::new(
1381 Some(workspace_id),
1382 project_handle.clone(),
1383 app_state.clone(),
1384 window,
1385 cx,
1386 )
1387 });
1388 })?;
1389 window
1390 } else {
1391 let window_bounds_override = window_bounds_env_override();
1392
1393 let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
1394 (Some(WindowBounds::Windowed(bounds)), None)
1395 } else {
1396 let restorable_bounds = serialized_workspace
1397 .as_ref()
1398 .and_then(|workspace| Some((workspace.display?, workspace.window_bounds?)))
1399 .or_else(|| {
1400 let (display, window_bounds) = DB.last_window().log_err()?;
1401 Some((display?, window_bounds?))
1402 });
1403
1404 if let Some((serialized_display, serialized_status)) = restorable_bounds {
1405 (Some(serialized_status.0), Some(serialized_display))
1406 } else {
1407 (None, None)
1408 }
1409 };
1410
1411 // Use the serialized workspace to construct the new window
1412 let mut options = cx.update(|cx| (app_state.build_window_options)(display, cx))?;
1413 options.window_bounds = window_bounds;
1414 let centered_layout = serialized_workspace
1415 .as_ref()
1416 .map(|w| w.centered_layout)
1417 .unwrap_or(false);
1418 cx.open_window(options, {
1419 let app_state = app_state.clone();
1420 let project_handle = project_handle.clone();
1421 move |window, cx| {
1422 cx.new(|cx| {
1423 let mut workspace = Workspace::new(
1424 Some(workspace_id),
1425 project_handle,
1426 app_state,
1427 window,
1428 cx,
1429 );
1430 workspace.centered_layout = centered_layout;
1431 workspace
1432 })
1433 }
1434 })?
1435 };
1436
1437 notify_if_database_failed(window, cx);
1438 let opened_items = window
1439 .update(cx, |_workspace, window, cx| {
1440 open_items(serialized_workspace, project_paths, window, cx)
1441 })?
1442 .await
1443 .unwrap_or_default();
1444
1445 window
1446 .update(cx, |workspace, window, cx| {
1447 window.activate_window();
1448 workspace.update_history(cx);
1449 })
1450 .log_err();
1451 Ok((window, opened_items))
1452 })
1453 }
1454
1455 pub fn weak_handle(&self) -> WeakEntity<Self> {
1456 self.weak_self.clone()
1457 }
1458
1459 pub fn left_dock(&self) -> &Entity<Dock> {
1460 &self.left_dock
1461 }
1462
1463 pub fn bottom_dock(&self) -> &Entity<Dock> {
1464 &self.bottom_dock
1465 }
1466
1467 pub fn bottom_dock_layout(&self) -> BottomDockLayout {
1468 self.bottom_dock_layout
1469 }
1470
1471 pub fn set_bottom_dock_layout(
1472 &mut self,
1473 layout: BottomDockLayout,
1474 window: &mut Window,
1475 cx: &mut Context<Self>,
1476 ) {
1477 let fs = self.project().read(cx).fs();
1478 settings::update_settings_file::<WorkspaceSettings>(fs.clone(), cx, move |content, _cx| {
1479 content.bottom_dock_layout = Some(layout);
1480 });
1481
1482 self.bottom_dock_layout = layout;
1483 cx.notify();
1484 self.serialize_workspace(window, cx);
1485 }
1486
1487 pub fn right_dock(&self) -> &Entity<Dock> {
1488 &self.right_dock
1489 }
1490
1491 pub fn all_docks(&self) -> [&Entity<Dock>; 3] {
1492 [&self.left_dock, &self.bottom_dock, &self.right_dock]
1493 }
1494
1495 pub fn dock_at_position(&self, position: DockPosition) -> &Entity<Dock> {
1496 match position {
1497 DockPosition::Left => &self.left_dock,
1498 DockPosition::Bottom => &self.bottom_dock,
1499 DockPosition::Right => &self.right_dock,
1500 }
1501 }
1502
1503 pub fn is_edited(&self) -> bool {
1504 self.window_edited
1505 }
1506
1507 pub fn add_panel<T: Panel>(
1508 &mut self,
1509 panel: Entity<T>,
1510 window: &mut Window,
1511 cx: &mut Context<Self>,
1512 ) {
1513 let focus_handle = panel.panel_focus_handle(cx);
1514 cx.on_focus_in(&focus_handle, window, Self::handle_panel_focused)
1515 .detach();
1516
1517 let dock_position = panel.position(window, cx);
1518 let dock = self.dock_at_position(dock_position);
1519
1520 dock.update(cx, |dock, cx| {
1521 dock.add_panel(panel, self.weak_self.clone(), window, cx)
1522 });
1523 }
1524
1525 pub fn status_bar(&self) -> &Entity<StatusBar> {
1526 &self.status_bar
1527 }
1528
1529 pub fn app_state(&self) -> &Arc<AppState> {
1530 &self.app_state
1531 }
1532
1533 pub fn user_store(&self) -> &Entity<UserStore> {
1534 &self.app_state.user_store
1535 }
1536
1537 pub fn project(&self) -> &Entity<Project> {
1538 &self.project
1539 }
1540
1541 pub fn recently_activated_items(&self, cx: &App) -> HashMap<EntityId, usize> {
1542 let mut history: HashMap<EntityId, usize> = HashMap::default();
1543
1544 for pane_handle in &self.panes {
1545 let pane = pane_handle.read(cx);
1546
1547 for entry in pane.activation_history() {
1548 history.insert(
1549 entry.entity_id,
1550 history
1551 .get(&entry.entity_id)
1552 .cloned()
1553 .unwrap_or(0)
1554 .max(entry.timestamp),
1555 );
1556 }
1557 }
1558
1559 history
1560 }
1561
1562 pub fn recent_navigation_history_iter(
1563 &self,
1564 cx: &App,
1565 ) -> impl Iterator<Item = (ProjectPath, Option<PathBuf>)> {
1566 let mut abs_paths_opened: HashMap<PathBuf, HashSet<ProjectPath>> = HashMap::default();
1567 let mut history: HashMap<ProjectPath, (Option<PathBuf>, usize)> = HashMap::default();
1568
1569 for pane in &self.panes {
1570 let pane = pane.read(cx);
1571
1572 pane.nav_history()
1573 .for_each_entry(cx, |entry, (project_path, fs_path)| {
1574 if let Some(fs_path) = &fs_path {
1575 abs_paths_opened
1576 .entry(fs_path.clone())
1577 .or_default()
1578 .insert(project_path.clone());
1579 }
1580 let timestamp = entry.timestamp;
1581 match history.entry(project_path) {
1582 hash_map::Entry::Occupied(mut entry) => {
1583 let (_, old_timestamp) = entry.get();
1584 if ×tamp > old_timestamp {
1585 entry.insert((fs_path, timestamp));
1586 }
1587 }
1588 hash_map::Entry::Vacant(entry) => {
1589 entry.insert((fs_path, timestamp));
1590 }
1591 }
1592 });
1593
1594 if let Some(item) = pane.active_item() {
1595 if let Some(project_path) = item.project_path(cx) {
1596 let fs_path = self.project.read(cx).absolute_path(&project_path, cx);
1597
1598 if let Some(fs_path) = &fs_path {
1599 abs_paths_opened
1600 .entry(fs_path.clone())
1601 .or_default()
1602 .insert(project_path.clone());
1603 }
1604
1605 history.insert(project_path, (fs_path, std::usize::MAX));
1606 }
1607 }
1608 }
1609
1610 history
1611 .into_iter()
1612 .sorted_by_key(|(_, (_, order))| *order)
1613 .map(|(project_path, (fs_path, _))| (project_path, fs_path))
1614 .rev()
1615 .filter(move |(history_path, abs_path)| {
1616 let latest_project_path_opened = abs_path
1617 .as_ref()
1618 .and_then(|abs_path| abs_paths_opened.get(abs_path))
1619 .and_then(|project_paths| {
1620 project_paths
1621 .iter()
1622 .max_by(|b1, b2| b1.worktree_id.cmp(&b2.worktree_id))
1623 });
1624
1625 match latest_project_path_opened {
1626 Some(latest_project_path_opened) => latest_project_path_opened == history_path,
1627 None => true,
1628 }
1629 })
1630 }
1631
1632 pub fn recent_navigation_history(
1633 &self,
1634 limit: Option<usize>,
1635 cx: &App,
1636 ) -> Vec<(ProjectPath, Option<PathBuf>)> {
1637 self.recent_navigation_history_iter(cx)
1638 .take(limit.unwrap_or(usize::MAX))
1639 .collect()
1640 }
1641
1642 fn navigate_history(
1643 &mut self,
1644 pane: WeakEntity<Pane>,
1645 mode: NavigationMode,
1646 window: &mut Window,
1647 cx: &mut Context<Workspace>,
1648 ) -> Task<Result<()>> {
1649 let to_load = if let Some(pane) = pane.upgrade() {
1650 pane.update(cx, |pane, cx| {
1651 window.focus(&pane.focus_handle(cx));
1652 loop {
1653 // Retrieve the weak item handle from the history.
1654 let entry = pane.nav_history_mut().pop(mode, cx)?;
1655
1656 // If the item is still present in this pane, then activate it.
1657 if let Some(index) = entry
1658 .item
1659 .upgrade()
1660 .and_then(|v| pane.index_for_item(v.as_ref()))
1661 {
1662 let prev_active_item_index = pane.active_item_index();
1663 pane.nav_history_mut().set_mode(mode);
1664 pane.activate_item(index, true, true, window, cx);
1665 pane.nav_history_mut().set_mode(NavigationMode::Normal);
1666
1667 let mut navigated = prev_active_item_index != pane.active_item_index();
1668 if let Some(data) = entry.data {
1669 navigated |= pane.active_item()?.navigate(data, window, cx);
1670 }
1671
1672 if navigated {
1673 break None;
1674 }
1675 } else {
1676 // If the item is no longer present in this pane, then retrieve its
1677 // path info in order to reopen it.
1678 break pane
1679 .nav_history()
1680 .path_for_item(entry.item.id())
1681 .map(|(project_path, abs_path)| (project_path, abs_path, entry));
1682 }
1683 }
1684 })
1685 } else {
1686 None
1687 };
1688
1689 if let Some((project_path, abs_path, entry)) = to_load {
1690 // If the item was no longer present, then load it again from its previous path, first try the local path
1691 let open_by_project_path = self.load_path(project_path.clone(), window, cx);
1692
1693 cx.spawn_in(window, async move |workspace, cx| {
1694 let open_by_project_path = open_by_project_path.await;
1695 let mut navigated = false;
1696 match open_by_project_path
1697 .with_context(|| format!("Navigating to {project_path:?}"))
1698 {
1699 Ok((project_entry_id, build_item)) => {
1700 let prev_active_item_id = pane.update(cx, |pane, _| {
1701 pane.nav_history_mut().set_mode(mode);
1702 pane.active_item().map(|p| p.item_id())
1703 })?;
1704
1705 pane.update_in(cx, |pane, window, cx| {
1706 let item = pane.open_item(
1707 project_entry_id,
1708 true,
1709 entry.is_preview,
1710 true,
1711 None,
1712 window, cx,
1713 build_item,
1714 );
1715 navigated |= Some(item.item_id()) != prev_active_item_id;
1716 pane.nav_history_mut().set_mode(NavigationMode::Normal);
1717 if let Some(data) = entry.data {
1718 navigated |= item.navigate(data, window, cx);
1719 }
1720 })?;
1721 }
1722 Err(open_by_project_path_e) => {
1723 // Fall back to opening by abs path, in case an external file was opened and closed,
1724 // and its worktree is now dropped
1725 if let Some(abs_path) = abs_path {
1726 let prev_active_item_id = pane.update(cx, |pane, _| {
1727 pane.nav_history_mut().set_mode(mode);
1728 pane.active_item().map(|p| p.item_id())
1729 })?;
1730 let open_by_abs_path = workspace.update_in(cx, |workspace, window, cx| {
1731 workspace.open_abs_path(abs_path.clone(), OpenOptions { visible: Some(OpenVisible::None), ..Default::default() }, window, cx)
1732 })?;
1733 match open_by_abs_path
1734 .await
1735 .with_context(|| format!("Navigating to {abs_path:?}"))
1736 {
1737 Ok(item) => {
1738 pane.update_in(cx, |pane, window, cx| {
1739 navigated |= Some(item.item_id()) != prev_active_item_id;
1740 pane.nav_history_mut().set_mode(NavigationMode::Normal);
1741 if let Some(data) = entry.data {
1742 navigated |= item.navigate(data, window, cx);
1743 }
1744 })?;
1745 }
1746 Err(open_by_abs_path_e) => {
1747 log::error!("Failed to navigate history: {open_by_project_path_e:#} and {open_by_abs_path_e:#}");
1748 }
1749 }
1750 }
1751 }
1752 }
1753
1754 if !navigated {
1755 workspace
1756 .update_in(cx, |workspace, window, cx| {
1757 Self::navigate_history(workspace, pane, mode, window, cx)
1758 })?
1759 .await?;
1760 }
1761
1762 Ok(())
1763 })
1764 } else {
1765 Task::ready(Ok(()))
1766 }
1767 }
1768
1769 pub fn go_back(
1770 &mut self,
1771 pane: WeakEntity<Pane>,
1772 window: &mut Window,
1773 cx: &mut Context<Workspace>,
1774 ) -> Task<Result<()>> {
1775 self.navigate_history(pane, NavigationMode::GoingBack, window, cx)
1776 }
1777
1778 pub fn go_forward(
1779 &mut self,
1780 pane: WeakEntity<Pane>,
1781 window: &mut Window,
1782 cx: &mut Context<Workspace>,
1783 ) -> Task<Result<()>> {
1784 self.navigate_history(pane, NavigationMode::GoingForward, window, cx)
1785 }
1786
1787 pub fn reopen_closed_item(
1788 &mut self,
1789 window: &mut Window,
1790 cx: &mut Context<Workspace>,
1791 ) -> Task<Result<()>> {
1792 self.navigate_history(
1793 self.active_pane().downgrade(),
1794 NavigationMode::ReopeningClosedItem,
1795 window,
1796 cx,
1797 )
1798 }
1799
1800 pub fn client(&self) -> &Arc<Client> {
1801 &self.app_state.client
1802 }
1803
1804 pub fn set_titlebar_item(&mut self, item: AnyView, _: &mut Window, cx: &mut Context<Self>) {
1805 self.titlebar_item = Some(item);
1806 cx.notify();
1807 }
1808
1809 pub fn set_prompt_for_new_path(&mut self, prompt: PromptForNewPath) {
1810 self.on_prompt_for_new_path = Some(prompt)
1811 }
1812
1813 pub fn set_prompt_for_open_path(&mut self, prompt: PromptForOpenPath) {
1814 self.on_prompt_for_open_path = Some(prompt)
1815 }
1816
1817 pub fn set_terminal_provider(&mut self, provider: impl TerminalProvider + 'static) {
1818 self.terminal_provider = Some(Box::new(provider));
1819 }
1820
1821 pub fn set_debugger_provider(&mut self, provider: impl DebuggerProvider + 'static) {
1822 self.debugger_provider = Some(Box::new(provider));
1823 }
1824
1825 pub fn serialized_ssh_project(&self) -> Option<SerializedSshProject> {
1826 self.serialized_ssh_project.clone()
1827 }
1828
1829 pub fn set_serialized_ssh_project(&mut self, serialized_ssh_project: SerializedSshProject) {
1830 self.serialized_ssh_project = Some(serialized_ssh_project);
1831 }
1832
1833 pub fn prompt_for_open_path(
1834 &mut self,
1835 path_prompt_options: PathPromptOptions,
1836 lister: DirectoryLister,
1837 window: &mut Window,
1838 cx: &mut Context<Self>,
1839 ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
1840 if !lister.is_local(cx) || !WorkspaceSettings::get_global(cx).use_system_path_prompts {
1841 let prompt = self.on_prompt_for_open_path.take().unwrap();
1842 let rx = prompt(self, lister, window, cx);
1843 self.on_prompt_for_open_path = Some(prompt);
1844 rx
1845 } else {
1846 let (tx, rx) = oneshot::channel();
1847 let abs_path = cx.prompt_for_paths(path_prompt_options);
1848
1849 cx.spawn_in(window, async move |this, cx| {
1850 let Ok(result) = abs_path.await else {
1851 return Ok(());
1852 };
1853
1854 match result {
1855 Ok(result) => {
1856 tx.send(result).log_err();
1857 }
1858 Err(err) => {
1859 let rx = this.update_in(cx, |this, window, cx| {
1860 this.show_portal_error(err.to_string(), cx);
1861 let prompt = this.on_prompt_for_open_path.take().unwrap();
1862 let rx = prompt(this, lister, window, cx);
1863 this.on_prompt_for_open_path = Some(prompt);
1864 rx
1865 })?;
1866 if let Ok(path) = rx.await {
1867 tx.send(path).log_err();
1868 }
1869 }
1870 };
1871 anyhow::Ok(())
1872 })
1873 .detach();
1874
1875 rx
1876 }
1877 }
1878
1879 pub fn prompt_for_new_path(
1880 &mut self,
1881 window: &mut Window,
1882 cx: &mut Context<Self>,
1883 ) -> oneshot::Receiver<Option<ProjectPath>> {
1884 if (self.project.read(cx).is_via_collab() || self.project.read(cx).is_via_ssh())
1885 || !WorkspaceSettings::get_global(cx).use_system_path_prompts
1886 {
1887 let prompt = self.on_prompt_for_new_path.take().unwrap();
1888 let rx = prompt(self, window, cx);
1889 self.on_prompt_for_new_path = Some(prompt);
1890 return rx;
1891 }
1892
1893 let (tx, rx) = oneshot::channel();
1894 cx.spawn_in(window, async move |this, cx| {
1895 let abs_path = this.update(cx, |this, cx| {
1896 let mut relative_to = this
1897 .most_recent_active_path(cx)
1898 .and_then(|p| p.parent().map(|p| p.to_path_buf()));
1899 if relative_to.is_none() {
1900 let project = this.project.read(cx);
1901 relative_to = project
1902 .visible_worktrees(cx)
1903 .filter_map(|worktree| {
1904 Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
1905 })
1906 .next()
1907 };
1908
1909 cx.prompt_for_new_path(&relative_to.unwrap_or_else(|| PathBuf::from("")))
1910 })?;
1911 let abs_path = match abs_path.await? {
1912 Ok(path) => path,
1913 Err(err) => {
1914 let rx = this.update_in(cx, |this, window, cx| {
1915 this.show_portal_error(err.to_string(), cx);
1916
1917 let prompt = this.on_prompt_for_new_path.take().unwrap();
1918 let rx = prompt(this, window, cx);
1919 this.on_prompt_for_new_path = Some(prompt);
1920 rx
1921 })?;
1922 if let Ok(path) = rx.await {
1923 tx.send(path).log_err();
1924 }
1925 return anyhow::Ok(());
1926 }
1927 };
1928
1929 let project_path = abs_path.and_then(|abs_path| {
1930 this.update(cx, |this, cx| {
1931 this.project.update(cx, |project, cx| {
1932 project.find_or_create_worktree(abs_path, true, cx)
1933 })
1934 })
1935 .ok()
1936 });
1937
1938 if let Some(project_path) = project_path {
1939 let (worktree, path) = project_path.await?;
1940 let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id())?;
1941 tx.send(Some(ProjectPath {
1942 worktree_id,
1943 path: path.into(),
1944 }))
1945 .ok();
1946 } else {
1947 tx.send(None).ok();
1948 }
1949 anyhow::Ok(())
1950 })
1951 .detach_and_log_err(cx);
1952
1953 rx
1954 }
1955
1956 pub fn titlebar_item(&self) -> Option<AnyView> {
1957 self.titlebar_item.clone()
1958 }
1959
1960 /// Call the given callback with a workspace whose project is local.
1961 ///
1962 /// If the given workspace has a local project, then it will be passed
1963 /// to the callback. Otherwise, a new empty window will be created.
1964 pub fn with_local_workspace<T, F>(
1965 &mut self,
1966 window: &mut Window,
1967 cx: &mut Context<Self>,
1968 callback: F,
1969 ) -> Task<Result<T>>
1970 where
1971 T: 'static,
1972 F: 'static + FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> T,
1973 {
1974 if self.project.read(cx).is_local() {
1975 Task::ready(Ok(callback(self, window, cx)))
1976 } else {
1977 let env = self.project.read(cx).cli_environment(cx);
1978 let task = Self::new_local(Vec::new(), self.app_state.clone(), None, env, cx);
1979 cx.spawn_in(window, async move |_vh, cx| {
1980 let (workspace, _) = task.await?;
1981 workspace.update(cx, callback)
1982 })
1983 }
1984 }
1985
1986 pub fn worktrees<'a>(&self, cx: &'a App) -> impl 'a + Iterator<Item = Entity<Worktree>> {
1987 self.project.read(cx).worktrees(cx)
1988 }
1989
1990 pub fn visible_worktrees<'a>(
1991 &self,
1992 cx: &'a App,
1993 ) -> impl 'a + Iterator<Item = Entity<Worktree>> {
1994 self.project.read(cx).visible_worktrees(cx)
1995 }
1996
1997 #[cfg(any(test, feature = "test-support"))]
1998 pub fn worktree_scans_complete(&self, cx: &App) -> impl Future<Output = ()> + 'static + use<> {
1999 let futures = self
2000 .worktrees(cx)
2001 .filter_map(|worktree| worktree.read(cx).as_local())
2002 .map(|worktree| worktree.scan_complete())
2003 .collect::<Vec<_>>();
2004 async move {
2005 for future in futures {
2006 future.await;
2007 }
2008 }
2009 }
2010
2011 pub fn close_global(_: &CloseWindow, cx: &mut App) {
2012 cx.defer(|cx| {
2013 cx.windows().iter().find(|window| {
2014 window
2015 .update(cx, |_, window, _| {
2016 if window.is_window_active() {
2017 //This can only get called when the window's project connection has been lost
2018 //so we don't need to prompt the user for anything and instead just close the window
2019 window.remove_window();
2020 true
2021 } else {
2022 false
2023 }
2024 })
2025 .unwrap_or(false)
2026 });
2027 });
2028 }
2029
2030 pub fn close_window(&mut self, _: &CloseWindow, window: &mut Window, cx: &mut Context<Self>) {
2031 let prepare = self.prepare_to_close(CloseIntent::CloseWindow, window, cx);
2032 cx.spawn_in(window, async move |_, cx| {
2033 if prepare.await? {
2034 cx.update(|window, _cx| window.remove_window())?;
2035 }
2036 anyhow::Ok(())
2037 })
2038 .detach_and_log_err(cx)
2039 }
2040
2041 pub fn move_focused_panel_to_next_position(
2042 &mut self,
2043 _: &MoveFocusedPanelToNextPosition,
2044 window: &mut Window,
2045 cx: &mut Context<Self>,
2046 ) {
2047 let docks = self.all_docks();
2048 let active_dock = docks
2049 .into_iter()
2050 .find(|dock| dock.focus_handle(cx).contains_focused(window, cx));
2051
2052 if let Some(dock) = active_dock {
2053 dock.update(cx, |dock, cx| {
2054 let active_panel = dock
2055 .active_panel()
2056 .filter(|panel| panel.panel_focus_handle(cx).contains_focused(window, cx));
2057
2058 if let Some(panel) = active_panel {
2059 panel.move_to_next_position(window, cx);
2060 }
2061 })
2062 }
2063 }
2064
2065 pub fn prepare_to_close(
2066 &mut self,
2067 close_intent: CloseIntent,
2068 window: &mut Window,
2069 cx: &mut Context<Self>,
2070 ) -> Task<Result<bool>> {
2071 let active_call = self.active_call().cloned();
2072
2073 // On Linux and Windows, closing the last window should restore the last workspace.
2074 let save_last_workspace = cfg!(not(target_os = "macos"))
2075 && close_intent != CloseIntent::ReplaceWindow
2076 && cx.windows().len() == 1;
2077
2078 cx.spawn_in(window, async move |this, cx| {
2079 let workspace_count = cx.update(|_window, cx| {
2080 cx.windows()
2081 .iter()
2082 .filter(|window| window.downcast::<Workspace>().is_some())
2083 .count()
2084 })?;
2085
2086 if let Some(active_call) = active_call {
2087 if close_intent != CloseIntent::Quit
2088 && workspace_count == 1
2089 && active_call.read_with(cx, |call, _| call.room().is_some())?
2090 {
2091 let answer = cx.update(|window, cx| {
2092 window.prompt(
2093 PromptLevel::Warning,
2094 "Do you want to leave the current call?",
2095 None,
2096 &["Close window and hang up", "Cancel"],
2097 cx,
2098 )
2099 })?;
2100
2101 if answer.await.log_err() == Some(1) {
2102 return anyhow::Ok(false);
2103 } else {
2104 active_call
2105 .update(cx, |call, cx| call.hang_up(cx))?
2106 .await
2107 .log_err();
2108 }
2109 }
2110 }
2111
2112 let save_result = this
2113 .update_in(cx, |this, window, cx| {
2114 this.save_all_internal(SaveIntent::Close, window, cx)
2115 })?
2116 .await;
2117
2118 // If we're not quitting, but closing, we remove the workspace from
2119 // the current session.
2120 if close_intent != CloseIntent::Quit
2121 && !save_last_workspace
2122 && save_result.as_ref().map_or(false, |&res| res)
2123 {
2124 this.update_in(cx, |this, window, cx| this.remove_from_session(window, cx))?
2125 .await;
2126 }
2127
2128 save_result
2129 })
2130 }
2131
2132 fn save_all(&mut self, action: &SaveAll, window: &mut Window, cx: &mut Context<Self>) {
2133 self.save_all_internal(
2134 action.save_intent.unwrap_or(SaveIntent::SaveAll),
2135 window,
2136 cx,
2137 )
2138 .detach_and_log_err(cx);
2139 }
2140
2141 fn send_keystrokes(
2142 &mut self,
2143 action: &SendKeystrokes,
2144 window: &mut Window,
2145 cx: &mut Context<Self>,
2146 ) {
2147 let mut state = self.dispatching_keystrokes.borrow_mut();
2148 if !state.0.insert(action.0.clone()) {
2149 cx.propagate();
2150 return;
2151 }
2152 let mut keystrokes: Vec<Keystroke> = action
2153 .0
2154 .split(' ')
2155 .flat_map(|k| Keystroke::parse(k).log_err())
2156 .collect();
2157 keystrokes.reverse();
2158
2159 state.1.append(&mut keystrokes);
2160 drop(state);
2161
2162 let keystrokes = self.dispatching_keystrokes.clone();
2163 window
2164 .spawn(cx, async move |cx| {
2165 // limit to 100 keystrokes to avoid infinite recursion.
2166 for _ in 0..100 {
2167 let Some(keystroke) = keystrokes.borrow_mut().1.pop() else {
2168 keystrokes.borrow_mut().0.clear();
2169 return Ok(());
2170 };
2171 cx.update(|window, cx| {
2172 let focused = window.focused(cx);
2173 window.dispatch_keystroke(keystroke.clone(), cx);
2174 if window.focused(cx) != focused {
2175 // dispatch_keystroke may cause the focus to change.
2176 // draw's side effect is to schedule the FocusChanged events in the current flush effect cycle
2177 // And we need that to happen before the next keystroke to keep vim mode happy...
2178 // (Note that the tests always do this implicitly, so you must manually test with something like:
2179 // "bindings": { "g z": ["workspace::SendKeystrokes", ": j <enter> u"]}
2180 // )
2181 window.draw(cx);
2182 }
2183 })?;
2184 }
2185
2186 *keystrokes.borrow_mut() = Default::default();
2187 Err(anyhow!("over 100 keystrokes passed to send_keystrokes"))
2188 })
2189 .detach_and_log_err(cx);
2190 }
2191
2192 fn save_all_internal(
2193 &mut self,
2194 mut save_intent: SaveIntent,
2195 window: &mut Window,
2196 cx: &mut Context<Self>,
2197 ) -> Task<Result<bool>> {
2198 if self.project.read(cx).is_disconnected(cx) {
2199 return Task::ready(Ok(true));
2200 }
2201 let dirty_items = self
2202 .panes
2203 .iter()
2204 .flat_map(|pane| {
2205 pane.read(cx).items().filter_map(|item| {
2206 if item.is_dirty(cx) {
2207 item.tab_content_text(0, cx);
2208 Some((pane.downgrade(), item.boxed_clone()))
2209 } else {
2210 None
2211 }
2212 })
2213 })
2214 .collect::<Vec<_>>();
2215
2216 let project = self.project.clone();
2217 cx.spawn_in(window, async move |workspace, cx| {
2218 let dirty_items = if save_intent == SaveIntent::Close && !dirty_items.is_empty() {
2219 let (serialize_tasks, remaining_dirty_items) =
2220 workspace.update_in(cx, |workspace, window, cx| {
2221 let mut remaining_dirty_items = Vec::new();
2222 let mut serialize_tasks = Vec::new();
2223 for (pane, item) in dirty_items {
2224 if let Some(task) = item
2225 .to_serializable_item_handle(cx)
2226 .and_then(|handle| handle.serialize(workspace, true, window, cx))
2227 {
2228 serialize_tasks.push(task);
2229 } else {
2230 remaining_dirty_items.push((pane, item));
2231 }
2232 }
2233 (serialize_tasks, remaining_dirty_items)
2234 })?;
2235
2236 futures::future::try_join_all(serialize_tasks).await?;
2237
2238 if remaining_dirty_items.len() > 1 {
2239 let answer = workspace.update_in(cx, |_, window, cx| {
2240 let detail = Pane::file_names_for_prompt(
2241 &mut remaining_dirty_items.iter().map(|(_, handle)| handle),
2242 cx,
2243 );
2244 window.prompt(
2245 PromptLevel::Warning,
2246 &"Do you want to save all changes in the following files?",
2247 Some(&detail),
2248 &["Save all", "Discard all", "Cancel"],
2249 cx,
2250 )
2251 })?;
2252 match answer.await.log_err() {
2253 Some(0) => save_intent = SaveIntent::SaveAll,
2254 Some(1) => save_intent = SaveIntent::Skip,
2255 Some(2) => return Ok(false),
2256 _ => {}
2257 }
2258 }
2259
2260 remaining_dirty_items
2261 } else {
2262 dirty_items
2263 };
2264
2265 for (pane, item) in dirty_items {
2266 let (singleton, project_entry_ids) =
2267 cx.update(|_, cx| (item.is_singleton(cx), item.project_entry_ids(cx)))?;
2268 if singleton || !project_entry_ids.is_empty() {
2269 if !Pane::save_item(project.clone(), &pane, &*item, save_intent, cx).await? {
2270 return Ok(false);
2271 }
2272 }
2273 }
2274 Ok(true)
2275 })
2276 }
2277
2278 pub fn open_workspace_for_paths(
2279 &mut self,
2280 replace_current_window: bool,
2281 paths: Vec<PathBuf>,
2282 window: &mut Window,
2283 cx: &mut Context<Self>,
2284 ) -> Task<Result<()>> {
2285 let window_handle = window.window_handle().downcast::<Self>();
2286 let is_remote = self.project.read(cx).is_via_collab();
2287 let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
2288 let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
2289
2290 let window_to_replace = if replace_current_window {
2291 window_handle
2292 } else if is_remote || has_worktree || has_dirty_items {
2293 None
2294 } else {
2295 window_handle
2296 };
2297 let app_state = self.app_state.clone();
2298
2299 cx.spawn(async move |_, cx| {
2300 cx.update(|cx| {
2301 open_paths(
2302 &paths,
2303 app_state,
2304 OpenOptions {
2305 replace_window: window_to_replace,
2306 ..Default::default()
2307 },
2308 cx,
2309 )
2310 })?
2311 .await?;
2312 Ok(())
2313 })
2314 }
2315
2316 #[allow(clippy::type_complexity)]
2317 pub fn open_paths(
2318 &mut self,
2319 mut abs_paths: Vec<PathBuf>,
2320 options: OpenOptions,
2321 pane: Option<WeakEntity<Pane>>,
2322 window: &mut Window,
2323 cx: &mut Context<Self>,
2324 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
2325 log::info!("open paths {abs_paths:?}");
2326
2327 let fs = self.app_state.fs.clone();
2328
2329 // Sort the paths to ensure we add worktrees for parents before their children.
2330 abs_paths.sort_unstable();
2331 cx.spawn_in(window, async move |this, cx| {
2332 let mut tasks = Vec::with_capacity(abs_paths.len());
2333
2334 for abs_path in &abs_paths {
2335 let visible = match options.visible.as_ref().unwrap_or(&OpenVisible::None) {
2336 OpenVisible::All => Some(true),
2337 OpenVisible::None => Some(false),
2338 OpenVisible::OnlyFiles => match fs.metadata(abs_path).await.log_err() {
2339 Some(Some(metadata)) => Some(!metadata.is_dir),
2340 Some(None) => Some(true),
2341 None => None,
2342 },
2343 OpenVisible::OnlyDirectories => match fs.metadata(abs_path).await.log_err() {
2344 Some(Some(metadata)) => Some(metadata.is_dir),
2345 Some(None) => Some(false),
2346 None => None,
2347 },
2348 };
2349 let project_path = match visible {
2350 Some(visible) => match this
2351 .update(cx, |this, cx| {
2352 Workspace::project_path_for_path(
2353 this.project.clone(),
2354 abs_path,
2355 visible,
2356 cx,
2357 )
2358 })
2359 .log_err()
2360 {
2361 Some(project_path) => project_path.await.log_err(),
2362 None => None,
2363 },
2364 None => None,
2365 };
2366
2367 let this = this.clone();
2368 let abs_path: Arc<Path> = SanitizedPath::from(abs_path.clone()).into();
2369 let fs = fs.clone();
2370 let pane = pane.clone();
2371 let task = cx.spawn(async move |cx| {
2372 let (worktree, project_path) = project_path?;
2373 if fs.is_dir(&abs_path).await {
2374 this.update(cx, |workspace, cx| {
2375 let worktree = worktree.read(cx);
2376 let worktree_abs_path = worktree.abs_path();
2377 let entry_id = if abs_path.as_ref() == worktree_abs_path.as_ref() {
2378 worktree.root_entry()
2379 } else {
2380 abs_path
2381 .strip_prefix(worktree_abs_path.as_ref())
2382 .ok()
2383 .and_then(|relative_path| {
2384 worktree.entry_for_path(relative_path)
2385 })
2386 }
2387 .map(|entry| entry.id);
2388 if let Some(entry_id) = entry_id {
2389 workspace.project.update(cx, |_, cx| {
2390 cx.emit(project::Event::ActiveEntryChanged(Some(entry_id)));
2391 })
2392 }
2393 })
2394 .log_err()?;
2395 None
2396 } else {
2397 Some(
2398 this.update_in(cx, |this, window, cx| {
2399 this.open_path(
2400 project_path,
2401 pane,
2402 options.focus.unwrap_or(true),
2403 window,
2404 cx,
2405 )
2406 })
2407 .log_err()?
2408 .await,
2409 )
2410 }
2411 });
2412 tasks.push(task);
2413 }
2414
2415 futures::future::join_all(tasks).await
2416 })
2417 }
2418
2419 pub fn open_resolved_path(
2420 &mut self,
2421 path: ResolvedPath,
2422 window: &mut Window,
2423 cx: &mut Context<Self>,
2424 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
2425 match path {
2426 ResolvedPath::ProjectPath { project_path, .. } => {
2427 self.open_path(project_path, None, true, window, cx)
2428 }
2429 ResolvedPath::AbsPath { path, .. } => self.open_abs_path(
2430 path,
2431 OpenOptions {
2432 visible: Some(OpenVisible::None),
2433 ..Default::default()
2434 },
2435 window,
2436 cx,
2437 ),
2438 }
2439 }
2440
2441 pub fn absolute_path_of_worktree(
2442 &self,
2443 worktree_id: WorktreeId,
2444 cx: &mut Context<Self>,
2445 ) -> Option<PathBuf> {
2446 self.project
2447 .read(cx)
2448 .worktree_for_id(worktree_id, cx)
2449 // TODO: use `abs_path` or `root_dir`
2450 .map(|wt| wt.read(cx).abs_path().as_ref().to_path_buf())
2451 }
2452
2453 fn add_folder_to_project(
2454 &mut self,
2455 _: &AddFolderToProject,
2456 window: &mut Window,
2457 cx: &mut Context<Self>,
2458 ) {
2459 let project = self.project.read(cx);
2460 if project.is_via_collab() {
2461 self.show_error(
2462 &anyhow!("You cannot add folders to someone else's project"),
2463 cx,
2464 );
2465 return;
2466 }
2467 let paths = self.prompt_for_open_path(
2468 PathPromptOptions {
2469 files: false,
2470 directories: true,
2471 multiple: true,
2472 },
2473 DirectoryLister::Project(self.project.clone()),
2474 window,
2475 cx,
2476 );
2477 cx.spawn_in(window, async move |this, cx| {
2478 if let Some(paths) = paths.await.log_err().flatten() {
2479 let results = this
2480 .update_in(cx, |this, window, cx| {
2481 this.open_paths(
2482 paths,
2483 OpenOptions {
2484 visible: Some(OpenVisible::All),
2485 ..Default::default()
2486 },
2487 None,
2488 window,
2489 cx,
2490 )
2491 })?
2492 .await;
2493 for result in results.into_iter().flatten() {
2494 result.log_err();
2495 }
2496 }
2497 anyhow::Ok(())
2498 })
2499 .detach_and_log_err(cx);
2500 }
2501
2502 pub fn project_path_for_path(
2503 project: Entity<Project>,
2504 abs_path: &Path,
2505 visible: bool,
2506 cx: &mut App,
2507 ) -> Task<Result<(Entity<Worktree>, ProjectPath)>> {
2508 let entry = project.update(cx, |project, cx| {
2509 project.find_or_create_worktree(abs_path, visible, cx)
2510 });
2511 cx.spawn(async move |cx| {
2512 let (worktree, path) = entry.await?;
2513 let worktree_id = worktree.update(cx, |t, _| t.id())?;
2514 Ok((
2515 worktree,
2516 ProjectPath {
2517 worktree_id,
2518 path: path.into(),
2519 },
2520 ))
2521 })
2522 }
2523
2524 pub fn items<'a>(&'a self, cx: &'a App) -> impl 'a + Iterator<Item = &'a Box<dyn ItemHandle>> {
2525 self.panes.iter().flat_map(|pane| pane.read(cx).items())
2526 }
2527
2528 pub fn item_of_type<T: Item>(&self, cx: &App) -> Option<Entity<T>> {
2529 self.items_of_type(cx).max_by_key(|item| item.item_id())
2530 }
2531
2532 pub fn items_of_type<'a, T: Item>(
2533 &'a self,
2534 cx: &'a App,
2535 ) -> impl 'a + Iterator<Item = Entity<T>> {
2536 self.panes
2537 .iter()
2538 .flat_map(|pane| pane.read(cx).items_of_type())
2539 }
2540
2541 pub fn active_item(&self, cx: &App) -> Option<Box<dyn ItemHandle>> {
2542 self.active_pane().read(cx).active_item()
2543 }
2544
2545 pub fn active_item_as<I: 'static>(&self, cx: &App) -> Option<Entity<I>> {
2546 let item = self.active_item(cx)?;
2547 item.to_any().downcast::<I>().ok()
2548 }
2549
2550 fn active_project_path(&self, cx: &App) -> Option<ProjectPath> {
2551 self.active_item(cx).and_then(|item| item.project_path(cx))
2552 }
2553
2554 pub fn most_recent_active_path(&self, cx: &App) -> Option<PathBuf> {
2555 self.recent_navigation_history_iter(cx)
2556 .filter_map(|(path, abs_path)| {
2557 let worktree = self
2558 .project
2559 .read(cx)
2560 .worktree_for_id(path.worktree_id, cx)?;
2561 if worktree.read(cx).is_visible() {
2562 abs_path
2563 } else {
2564 None
2565 }
2566 })
2567 .next()
2568 }
2569
2570 pub fn save_active_item(
2571 &mut self,
2572 save_intent: SaveIntent,
2573 window: &mut Window,
2574 cx: &mut App,
2575 ) -> Task<Result<()>> {
2576 let project = self.project.clone();
2577 let pane = self.active_pane();
2578 let item = pane.read(cx).active_item();
2579 let pane = pane.downgrade();
2580
2581 window.spawn(cx, async move |mut cx| {
2582 if let Some(item) = item {
2583 Pane::save_item(project, &pane, item.as_ref(), save_intent, &mut cx)
2584 .await
2585 .map(|_| ())
2586 } else {
2587 Ok(())
2588 }
2589 })
2590 }
2591
2592 pub fn close_inactive_items_and_panes(
2593 &mut self,
2594 action: &CloseInactiveTabsAndPanes,
2595 window: &mut Window,
2596 cx: &mut Context<Self>,
2597 ) {
2598 if let Some(task) = self.close_all_internal(
2599 true,
2600 action.save_intent.unwrap_or(SaveIntent::Close),
2601 window,
2602 cx,
2603 ) {
2604 task.detach_and_log_err(cx)
2605 }
2606 }
2607
2608 pub fn close_all_items_and_panes(
2609 &mut self,
2610 action: &CloseAllItemsAndPanes,
2611 window: &mut Window,
2612 cx: &mut Context<Self>,
2613 ) {
2614 if let Some(task) = self.close_all_internal(
2615 false,
2616 action.save_intent.unwrap_or(SaveIntent::Close),
2617 window,
2618 cx,
2619 ) {
2620 task.detach_and_log_err(cx)
2621 }
2622 }
2623
2624 fn close_all_internal(
2625 &mut self,
2626 retain_active_pane: bool,
2627 save_intent: SaveIntent,
2628 window: &mut Window,
2629 cx: &mut Context<Self>,
2630 ) -> Option<Task<Result<()>>> {
2631 let current_pane = self.active_pane();
2632
2633 let mut tasks = Vec::new();
2634
2635 if retain_active_pane {
2636 if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| {
2637 pane.close_inactive_items(
2638 &CloseInactiveItems {
2639 save_intent: None,
2640 close_pinned: false,
2641 },
2642 window,
2643 cx,
2644 )
2645 }) {
2646 tasks.push(current_pane_close);
2647 };
2648 }
2649
2650 for pane in self.panes() {
2651 if retain_active_pane && pane.entity_id() == current_pane.entity_id() {
2652 continue;
2653 }
2654
2655 if let Some(close_pane_items) = pane.update(cx, |pane: &mut Pane, cx| {
2656 pane.close_all_items(
2657 &CloseAllItems {
2658 save_intent: Some(save_intent),
2659 close_pinned: false,
2660 },
2661 window,
2662 cx,
2663 )
2664 }) {
2665 tasks.push(close_pane_items)
2666 }
2667 }
2668
2669 if tasks.is_empty() {
2670 None
2671 } else {
2672 Some(cx.spawn_in(window, async move |_, _| {
2673 for task in tasks {
2674 task.await?
2675 }
2676 Ok(())
2677 }))
2678 }
2679 }
2680
2681 pub fn is_dock_at_position_open(&self, position: DockPosition, cx: &mut Context<Self>) -> bool {
2682 self.dock_at_position(position).read(cx).is_open()
2683 }
2684
2685 pub fn toggle_dock(
2686 &mut self,
2687 dock_side: DockPosition,
2688 window: &mut Window,
2689 cx: &mut Context<Self>,
2690 ) {
2691 let dock = self.dock_at_position(dock_side);
2692 let mut focus_center = false;
2693 let mut reveal_dock = false;
2694 dock.update(cx, |dock, cx| {
2695 let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
2696 let was_visible = dock.is_open() && !other_is_zoomed;
2697 dock.set_open(!was_visible, window, cx);
2698
2699 if dock.active_panel().is_none() {
2700 let Some(panel_ix) = dock
2701 .first_enabled_panel_idx(cx)
2702 .log_with_level(log::Level::Info)
2703 else {
2704 return;
2705 };
2706 dock.activate_panel(panel_ix, window, cx);
2707 }
2708
2709 if let Some(active_panel) = dock.active_panel() {
2710 if was_visible {
2711 if active_panel
2712 .panel_focus_handle(cx)
2713 .contains_focused(window, cx)
2714 {
2715 focus_center = true;
2716 }
2717 } else {
2718 let focus_handle = &active_panel.panel_focus_handle(cx);
2719 window.focus(focus_handle);
2720 reveal_dock = true;
2721 }
2722 }
2723 });
2724
2725 if reveal_dock {
2726 self.dismiss_zoomed_items_to_reveal(Some(dock_side), window, cx);
2727 }
2728
2729 if focus_center {
2730 self.active_pane
2731 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)))
2732 }
2733
2734 cx.notify();
2735 self.serialize_workspace(window, cx);
2736 }
2737
2738 pub fn close_all_docks(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2739 for dock in self.all_docks() {
2740 dock.update(cx, |dock, cx| {
2741 dock.set_open(false, window, cx);
2742 });
2743 }
2744
2745 cx.focus_self(window);
2746 cx.notify();
2747 self.serialize_workspace(window, cx);
2748 }
2749
2750 /// Transfer focus to the panel of the given type.
2751 pub fn focus_panel<T: Panel>(
2752 &mut self,
2753 window: &mut Window,
2754 cx: &mut Context<Self>,
2755 ) -> Option<Entity<T>> {
2756 let panel = self.focus_or_unfocus_panel::<T>(window, cx, |_, _, _| true)?;
2757 panel.to_any().downcast().ok()
2758 }
2759
2760 /// Focus the panel of the given type if it isn't already focused. If it is
2761 /// already focused, then transfer focus back to the workspace center.
2762 pub fn toggle_panel_focus<T: Panel>(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2763 self.focus_or_unfocus_panel::<T>(window, cx, |panel, window, cx| {
2764 !panel.panel_focus_handle(cx).contains_focused(window, cx)
2765 });
2766 }
2767
2768 pub fn activate_panel_for_proto_id(
2769 &mut self,
2770 panel_id: PanelId,
2771 window: &mut Window,
2772 cx: &mut Context<Self>,
2773 ) -> Option<Arc<dyn PanelHandle>> {
2774 let mut panel = None;
2775 for dock in self.all_docks() {
2776 if let Some(panel_index) = dock.read(cx).panel_index_for_proto_id(panel_id) {
2777 panel = dock.update(cx, |dock, cx| {
2778 dock.activate_panel(panel_index, window, cx);
2779 dock.set_open(true, window, cx);
2780 dock.active_panel().cloned()
2781 });
2782 break;
2783 }
2784 }
2785
2786 if panel.is_some() {
2787 cx.notify();
2788 self.serialize_workspace(window, cx);
2789 }
2790
2791 panel
2792 }
2793
2794 /// Focus or unfocus the given panel type, depending on the given callback.
2795 fn focus_or_unfocus_panel<T: Panel>(
2796 &mut self,
2797 window: &mut Window,
2798 cx: &mut Context<Self>,
2799 should_focus: impl Fn(&dyn PanelHandle, &mut Window, &mut Context<Dock>) -> bool,
2800 ) -> Option<Arc<dyn PanelHandle>> {
2801 let mut result_panel = None;
2802 let mut serialize = false;
2803 for dock in self.all_docks() {
2804 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
2805 let mut focus_center = false;
2806 let panel = dock.update(cx, |dock, cx| {
2807 dock.activate_panel(panel_index, window, cx);
2808
2809 let panel = dock.active_panel().cloned();
2810 if let Some(panel) = panel.as_ref() {
2811 if should_focus(&**panel, window, cx) {
2812 dock.set_open(true, window, cx);
2813 panel.panel_focus_handle(cx).focus(window);
2814 } else {
2815 focus_center = true;
2816 }
2817 }
2818 panel
2819 });
2820
2821 if focus_center {
2822 self.active_pane
2823 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)))
2824 }
2825
2826 result_panel = panel;
2827 serialize = true;
2828 break;
2829 }
2830 }
2831
2832 if serialize {
2833 self.serialize_workspace(window, cx);
2834 }
2835
2836 cx.notify();
2837 result_panel
2838 }
2839
2840 /// Open the panel of the given type
2841 pub fn open_panel<T: Panel>(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2842 for dock in self.all_docks() {
2843 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
2844 dock.update(cx, |dock, cx| {
2845 dock.activate_panel(panel_index, window, cx);
2846 dock.set_open(true, window, cx);
2847 });
2848 }
2849 }
2850 }
2851
2852 pub fn panel<T: Panel>(&self, cx: &App) -> Option<Entity<T>> {
2853 self.all_docks()
2854 .iter()
2855 .find_map(|dock| dock.read(cx).panel::<T>())
2856 }
2857
2858 fn dismiss_zoomed_items_to_reveal(
2859 &mut self,
2860 dock_to_reveal: Option<DockPosition>,
2861 window: &mut Window,
2862 cx: &mut Context<Self>,
2863 ) {
2864 // If a center pane is zoomed, unzoom it.
2865 for pane in &self.panes {
2866 if pane != &self.active_pane || dock_to_reveal.is_some() {
2867 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
2868 }
2869 }
2870
2871 // If another dock is zoomed, hide it.
2872 let mut focus_center = false;
2873 for dock in self.all_docks() {
2874 dock.update(cx, |dock, cx| {
2875 if Some(dock.position()) != dock_to_reveal {
2876 if let Some(panel) = dock.active_panel() {
2877 if panel.is_zoomed(window, cx) {
2878 focus_center |=
2879 panel.panel_focus_handle(cx).contains_focused(window, cx);
2880 dock.set_open(false, window, cx);
2881 }
2882 }
2883 }
2884 });
2885 }
2886
2887 if focus_center {
2888 self.active_pane
2889 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)))
2890 }
2891
2892 if self.zoomed_position != dock_to_reveal {
2893 self.zoomed = None;
2894 self.zoomed_position = None;
2895 cx.emit(Event::ZoomChanged);
2896 }
2897
2898 cx.notify();
2899 }
2900
2901 fn add_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<Pane> {
2902 let pane = cx.new(|cx| {
2903 let mut pane = Pane::new(
2904 self.weak_handle(),
2905 self.project.clone(),
2906 self.pane_history_timestamp.clone(),
2907 None,
2908 NewFile.boxed_clone(),
2909 window,
2910 cx,
2911 );
2912 pane.set_can_split(Some(Arc::new(|_, _, _, _| true)));
2913 pane
2914 });
2915 cx.subscribe_in(&pane, window, Self::handle_pane_event)
2916 .detach();
2917 self.panes.push(pane.clone());
2918
2919 window.focus(&pane.focus_handle(cx));
2920
2921 cx.emit(Event::PaneAdded(pane.clone()));
2922 pane
2923 }
2924
2925 pub fn add_item_to_center(
2926 &mut self,
2927 item: Box<dyn ItemHandle>,
2928 window: &mut Window,
2929 cx: &mut Context<Self>,
2930 ) -> bool {
2931 if let Some(center_pane) = self.last_active_center_pane.clone() {
2932 if let Some(center_pane) = center_pane.upgrade() {
2933 center_pane.update(cx, |pane, cx| {
2934 pane.add_item(item, true, true, None, window, cx)
2935 });
2936 true
2937 } else {
2938 false
2939 }
2940 } else {
2941 false
2942 }
2943 }
2944
2945 pub fn add_item_to_active_pane(
2946 &mut self,
2947 item: Box<dyn ItemHandle>,
2948 destination_index: Option<usize>,
2949 focus_item: bool,
2950 window: &mut Window,
2951 cx: &mut App,
2952 ) {
2953 self.add_item(
2954 self.active_pane.clone(),
2955 item,
2956 destination_index,
2957 false,
2958 focus_item,
2959 window,
2960 cx,
2961 )
2962 }
2963
2964 pub fn add_item(
2965 &mut self,
2966 pane: Entity<Pane>,
2967 item: Box<dyn ItemHandle>,
2968 destination_index: Option<usize>,
2969 activate_pane: bool,
2970 focus_item: bool,
2971 window: &mut Window,
2972 cx: &mut App,
2973 ) {
2974 if let Some(text) = item.telemetry_event_text(cx) {
2975 telemetry::event!(text);
2976 }
2977
2978 pane.update(cx, |pane, cx| {
2979 pane.add_item(
2980 item,
2981 activate_pane,
2982 focus_item,
2983 destination_index,
2984 window,
2985 cx,
2986 )
2987 });
2988 }
2989
2990 pub fn split_item(
2991 &mut self,
2992 split_direction: SplitDirection,
2993 item: Box<dyn ItemHandle>,
2994 window: &mut Window,
2995 cx: &mut Context<Self>,
2996 ) {
2997 let new_pane = self.split_pane(self.active_pane.clone(), split_direction, window, cx);
2998 self.add_item(new_pane, item, None, true, true, window, cx);
2999 }
3000
3001 pub fn open_abs_path(
3002 &mut self,
3003 abs_path: PathBuf,
3004 options: OpenOptions,
3005 window: &mut Window,
3006 cx: &mut Context<Self>,
3007 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
3008 cx.spawn_in(window, async move |workspace, cx| {
3009 let open_paths_task_result = workspace
3010 .update_in(cx, |workspace, window, cx| {
3011 workspace.open_paths(vec![abs_path.clone()], options, None, window, cx)
3012 })
3013 .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
3014 .await;
3015 anyhow::ensure!(
3016 open_paths_task_result.len() == 1,
3017 "open abs path {abs_path:?} task returned incorrect number of results"
3018 );
3019 match open_paths_task_result
3020 .into_iter()
3021 .next()
3022 .expect("ensured single task result")
3023 {
3024 Some(open_result) => {
3025 open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
3026 }
3027 None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
3028 }
3029 })
3030 }
3031
3032 pub fn split_abs_path(
3033 &mut self,
3034 abs_path: PathBuf,
3035 visible: bool,
3036 window: &mut Window,
3037 cx: &mut Context<Self>,
3038 ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
3039 let project_path_task =
3040 Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
3041 cx.spawn_in(window, async move |this, cx| {
3042 let (_, path) = project_path_task.await?;
3043 this.update_in(cx, |this, window, cx| this.split_path(path, window, cx))?
3044 .await
3045 })
3046 }
3047
3048 pub fn open_path(
3049 &mut self,
3050 path: impl Into<ProjectPath>,
3051 pane: Option<WeakEntity<Pane>>,
3052 focus_item: bool,
3053 window: &mut Window,
3054 cx: &mut App,
3055 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
3056 self.open_path_preview(path, pane, focus_item, false, true, window, cx)
3057 }
3058
3059 pub fn open_path_preview(
3060 &mut self,
3061 path: impl Into<ProjectPath>,
3062 pane: Option<WeakEntity<Pane>>,
3063 focus_item: bool,
3064 allow_preview: bool,
3065 activate: bool,
3066 window: &mut Window,
3067 cx: &mut App,
3068 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
3069 let pane = pane.unwrap_or_else(|| {
3070 self.last_active_center_pane.clone().unwrap_or_else(|| {
3071 self.panes
3072 .first()
3073 .expect("There must be an active pane")
3074 .downgrade()
3075 })
3076 });
3077
3078 let task = self.load_path(path.into(), window, cx);
3079 window.spawn(cx, async move |cx| {
3080 let (project_entry_id, build_item) = task.await?;
3081 let result = pane.update_in(cx, |pane, window, cx| {
3082 let result = pane.open_item(
3083 project_entry_id,
3084 focus_item,
3085 allow_preview,
3086 activate,
3087 None,
3088 window,
3089 cx,
3090 build_item,
3091 );
3092
3093 result
3094 });
3095 result
3096 })
3097 }
3098
3099 pub fn split_path(
3100 &mut self,
3101 path: impl Into<ProjectPath>,
3102 window: &mut Window,
3103 cx: &mut Context<Self>,
3104 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
3105 self.split_path_preview(path, false, None, window, cx)
3106 }
3107
3108 pub fn split_path_preview(
3109 &mut self,
3110 path: impl Into<ProjectPath>,
3111 allow_preview: bool,
3112 split_direction: Option<SplitDirection>,
3113 window: &mut Window,
3114 cx: &mut Context<Self>,
3115 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
3116 let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
3117 self.panes
3118 .first()
3119 .expect("There must be an active pane")
3120 .downgrade()
3121 });
3122
3123 if let Member::Pane(center_pane) = &self.center.root {
3124 if center_pane.read(cx).items_len() == 0 {
3125 return self.open_path(path, Some(pane), true, window, cx);
3126 }
3127 }
3128
3129 let task = self.load_path(path.into(), window, cx);
3130 cx.spawn_in(window, async move |this, cx| {
3131 let (project_entry_id, build_item) = task.await?;
3132 this.update_in(cx, move |this, window, cx| -> Option<_> {
3133 let pane = pane.upgrade()?;
3134 let new_pane = this.split_pane(
3135 pane,
3136 split_direction.unwrap_or(SplitDirection::Right),
3137 window,
3138 cx,
3139 );
3140 new_pane.update(cx, |new_pane, cx| {
3141 Some(new_pane.open_item(
3142 project_entry_id,
3143 true,
3144 allow_preview,
3145 true,
3146 None,
3147 window,
3148 cx,
3149 build_item,
3150 ))
3151 })
3152 })
3153 .map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))?
3154 })
3155 }
3156
3157 fn load_path(
3158 &mut self,
3159 path: ProjectPath,
3160 window: &mut Window,
3161 cx: &mut App,
3162 ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
3163 let project = self.project().clone();
3164 let registry = cx.default_global::<ProjectItemRegistry>().clone();
3165 registry.open_path(&project, &path, window, cx)
3166 }
3167
3168 pub fn find_project_item<T>(
3169 &self,
3170 pane: &Entity<Pane>,
3171 project_item: &Entity<T::Item>,
3172 cx: &App,
3173 ) -> Option<Entity<T>>
3174 where
3175 T: ProjectItem,
3176 {
3177 use project::ProjectItem as _;
3178 let project_item = project_item.read(cx);
3179 let entry_id = project_item.entry_id(cx);
3180 let project_path = project_item.project_path(cx);
3181
3182 let mut item = None;
3183 if let Some(entry_id) = entry_id {
3184 item = pane.read(cx).item_for_entry(entry_id, cx);
3185 }
3186 if item.is_none() {
3187 if let Some(project_path) = project_path {
3188 item = pane.read(cx).item_for_path(project_path, cx);
3189 }
3190 }
3191
3192 item.and_then(|item| item.downcast::<T>())
3193 }
3194
3195 pub fn is_project_item_open<T>(
3196 &self,
3197 pane: &Entity<Pane>,
3198 project_item: &Entity<T::Item>,
3199 cx: &App,
3200 ) -> bool
3201 where
3202 T: ProjectItem,
3203 {
3204 self.find_project_item::<T>(pane, project_item, cx)
3205 .is_some()
3206 }
3207
3208 pub fn open_project_item<T>(
3209 &mut self,
3210 pane: Entity<Pane>,
3211 project_item: Entity<T::Item>,
3212 activate_pane: bool,
3213 focus_item: bool,
3214 window: &mut Window,
3215 cx: &mut Context<Self>,
3216 ) -> Entity<T>
3217 where
3218 T: ProjectItem,
3219 {
3220 if let Some(item) = self.find_project_item(&pane, &project_item, cx) {
3221 self.activate_item(&item, activate_pane, focus_item, window, cx);
3222 return item;
3223 }
3224
3225 let item = pane.update(cx, |pane, cx| {
3226 cx.new(|cx| {
3227 T::for_project_item(self.project().clone(), Some(pane), project_item, window, cx)
3228 })
3229 });
3230 let item_id = item.item_id();
3231 let mut destination_index = None;
3232 pane.update(cx, |pane, cx| {
3233 if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation {
3234 if let Some(preview_item_id) = pane.preview_item_id() {
3235 if preview_item_id != item_id {
3236 destination_index = pane.close_current_preview_item(window, cx);
3237 }
3238 }
3239 }
3240 pane.set_preview_item_id(Some(item.item_id()), cx)
3241 });
3242
3243 self.add_item(
3244 pane,
3245 Box::new(item.clone()),
3246 destination_index,
3247 activate_pane,
3248 focus_item,
3249 window,
3250 cx,
3251 );
3252 item
3253 }
3254
3255 pub fn open_shared_screen(
3256 &mut self,
3257 peer_id: PeerId,
3258 window: &mut Window,
3259 cx: &mut Context<Self>,
3260 ) {
3261 if let Some(shared_screen) =
3262 self.shared_screen_for_peer(peer_id, &self.active_pane, window, cx)
3263 {
3264 self.active_pane.update(cx, |pane, cx| {
3265 pane.add_item(Box::new(shared_screen), false, true, None, window, cx)
3266 });
3267 }
3268 }
3269
3270 pub fn activate_item(
3271 &mut self,
3272 item: &dyn ItemHandle,
3273 activate_pane: bool,
3274 focus_item: bool,
3275 window: &mut Window,
3276 cx: &mut App,
3277 ) -> bool {
3278 let result = self.panes.iter().find_map(|pane| {
3279 pane.read(cx)
3280 .index_for_item(item)
3281 .map(|ix| (pane.clone(), ix))
3282 });
3283 if let Some((pane, ix)) = result {
3284 pane.update(cx, |pane, cx| {
3285 pane.activate_item(ix, activate_pane, focus_item, window, cx)
3286 });
3287 true
3288 } else {
3289 false
3290 }
3291 }
3292
3293 fn activate_pane_at_index(
3294 &mut self,
3295 action: &ActivatePane,
3296 window: &mut Window,
3297 cx: &mut Context<Self>,
3298 ) {
3299 let panes = self.center.panes();
3300 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
3301 window.focus(&pane.focus_handle(cx));
3302 } else {
3303 self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, window, cx);
3304 }
3305 }
3306
3307 fn move_item_to_pane_at_index(
3308 &mut self,
3309 action: &MoveItemToPane,
3310 window: &mut Window,
3311 cx: &mut Context<Self>,
3312 ) {
3313 let Some(&target_pane) = self.center.panes().get(action.destination) else {
3314 return;
3315 };
3316 move_active_item(
3317 &self.active_pane,
3318 target_pane,
3319 action.focus,
3320 true,
3321 window,
3322 cx,
3323 );
3324 }
3325
3326 pub fn activate_next_pane(&mut self, window: &mut Window, cx: &mut App) {
3327 let panes = self.center.panes();
3328 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
3329 let next_ix = (ix + 1) % panes.len();
3330 let next_pane = panes[next_ix].clone();
3331 window.focus(&next_pane.focus_handle(cx));
3332 }
3333 }
3334
3335 pub fn activate_previous_pane(&mut self, window: &mut Window, cx: &mut App) {
3336 let panes = self.center.panes();
3337 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
3338 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
3339 let prev_pane = panes[prev_ix].clone();
3340 window.focus(&prev_pane.focus_handle(cx));
3341 }
3342 }
3343
3344 pub fn activate_pane_in_direction(
3345 &mut self,
3346 direction: SplitDirection,
3347 window: &mut Window,
3348 cx: &mut App,
3349 ) {
3350 use ActivateInDirectionTarget as Target;
3351 enum Origin {
3352 LeftDock,
3353 RightDock,
3354 BottomDock,
3355 Center,
3356 }
3357
3358 let origin: Origin = [
3359 (&self.left_dock, Origin::LeftDock),
3360 (&self.right_dock, Origin::RightDock),
3361 (&self.bottom_dock, Origin::BottomDock),
3362 ]
3363 .into_iter()
3364 .find_map(|(dock, origin)| {
3365 if dock.focus_handle(cx).contains_focused(window, cx) && dock.read(cx).is_open() {
3366 Some(origin)
3367 } else {
3368 None
3369 }
3370 })
3371 .unwrap_or(Origin::Center);
3372
3373 let get_last_active_pane = || {
3374 let pane = self
3375 .last_active_center_pane
3376 .clone()
3377 .unwrap_or_else(|| {
3378 self.panes
3379 .first()
3380 .expect("There must be an active pane")
3381 .downgrade()
3382 })
3383 .upgrade()?;
3384 (pane.read(cx).items_len() != 0).then_some(pane)
3385 };
3386
3387 let try_dock =
3388 |dock: &Entity<Dock>| dock.read(cx).is_open().then(|| Target::Dock(dock.clone()));
3389
3390 let target = match (origin, direction) {
3391 // We're in the center, so we first try to go to a different pane,
3392 // otherwise try to go to a dock.
3393 (Origin::Center, direction) => {
3394 if let Some(pane) = self.find_pane_in_direction(direction, cx) {
3395 Some(Target::Pane(pane))
3396 } else {
3397 match direction {
3398 SplitDirection::Up => None,
3399 SplitDirection::Down => try_dock(&self.bottom_dock),
3400 SplitDirection::Left => try_dock(&self.left_dock),
3401 SplitDirection::Right => try_dock(&self.right_dock),
3402 }
3403 }
3404 }
3405
3406 (Origin::LeftDock, SplitDirection::Right) => {
3407 if let Some(last_active_pane) = get_last_active_pane() {
3408 Some(Target::Pane(last_active_pane))
3409 } else {
3410 try_dock(&self.bottom_dock).or_else(|| try_dock(&self.right_dock))
3411 }
3412 }
3413
3414 (Origin::LeftDock, SplitDirection::Down)
3415 | (Origin::RightDock, SplitDirection::Down) => try_dock(&self.bottom_dock),
3416
3417 (Origin::BottomDock, SplitDirection::Up) => get_last_active_pane().map(Target::Pane),
3418 (Origin::BottomDock, SplitDirection::Left) => try_dock(&self.left_dock),
3419 (Origin::BottomDock, SplitDirection::Right) => try_dock(&self.right_dock),
3420
3421 (Origin::RightDock, SplitDirection::Left) => {
3422 if let Some(last_active_pane) = get_last_active_pane() {
3423 Some(Target::Pane(last_active_pane))
3424 } else {
3425 try_dock(&self.bottom_dock).or_else(|| try_dock(&self.left_dock))
3426 }
3427 }
3428
3429 _ => None,
3430 };
3431
3432 match target {
3433 Some(ActivateInDirectionTarget::Pane(pane)) => {
3434 window.focus(&pane.focus_handle(cx));
3435 }
3436 Some(ActivateInDirectionTarget::Dock(dock)) => {
3437 // Defer this to avoid a panic when the dock's active panel is already on the stack.
3438 window.defer(cx, move |window, cx| {
3439 let dock = dock.read(cx);
3440 if let Some(panel) = dock.active_panel() {
3441 panel.panel_focus_handle(cx).focus(window);
3442 } else {
3443 log::error!("Could not find a focus target when in switching focus in {direction} direction for a {:?} dock", dock.position());
3444 }
3445 })
3446 }
3447 None => {}
3448 }
3449 }
3450
3451 pub fn move_item_to_pane_in_direction(
3452 &mut self,
3453 action: &MoveItemToPaneInDirection,
3454 window: &mut Window,
3455 cx: &mut App,
3456 ) {
3457 if let Some(destination) = self.find_pane_in_direction(action.direction, cx) {
3458 move_active_item(
3459 &self.active_pane,
3460 &destination,
3461 action.focus,
3462 true,
3463 window,
3464 cx,
3465 );
3466 }
3467 }
3468
3469 pub fn bounding_box_for_pane(&self, pane: &Entity<Pane>) -> Option<Bounds<Pixels>> {
3470 self.center.bounding_box_for_pane(pane)
3471 }
3472
3473 pub fn find_pane_in_direction(
3474 &mut self,
3475 direction: SplitDirection,
3476 cx: &App,
3477 ) -> Option<Entity<Pane>> {
3478 self.center
3479 .find_pane_in_direction(&self.active_pane, direction, cx)
3480 .cloned()
3481 }
3482
3483 pub fn swap_pane_in_direction(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
3484 if let Some(to) = self.find_pane_in_direction(direction, cx) {
3485 self.center.swap(&self.active_pane, &to);
3486 cx.notify();
3487 }
3488 }
3489
3490 pub fn resize_pane(
3491 &mut self,
3492 axis: gpui::Axis,
3493 amount: Pixels,
3494 window: &mut Window,
3495 cx: &mut Context<Self>,
3496 ) {
3497 let docks = self.all_docks();
3498 let active_dock = docks
3499 .into_iter()
3500 .find(|dock| dock.focus_handle(cx).contains_focused(window, cx));
3501
3502 if let Some(dock) = active_dock {
3503 let Some(panel_size) = dock.read(cx).active_panel_size(window, cx) else {
3504 return;
3505 };
3506 match dock.read(cx).position() {
3507 DockPosition::Left => resize_left_dock(panel_size + amount, self, window, cx),
3508 DockPosition::Bottom => resize_bottom_dock(panel_size + amount, self, window, cx),
3509 DockPosition::Right => resize_right_dock(panel_size + amount, self, window, cx),
3510 }
3511 } else {
3512 self.center
3513 .resize(&self.active_pane, axis, amount, &self.bounds);
3514 }
3515 cx.notify();
3516 }
3517
3518 pub fn reset_pane_sizes(&mut self, cx: &mut Context<Self>) {
3519 self.center.reset_pane_sizes();
3520 cx.notify();
3521 }
3522
3523 fn handle_pane_focused(
3524 &mut self,
3525 pane: Entity<Pane>,
3526 window: &mut Window,
3527 cx: &mut Context<Self>,
3528 ) {
3529 // This is explicitly hoisted out of the following check for pane identity as
3530 // terminal panel panes are not registered as a center panes.
3531 self.status_bar.update(cx, |status_bar, cx| {
3532 status_bar.set_active_pane(&pane, window, cx);
3533 });
3534 if self.active_pane != pane {
3535 self.set_active_pane(&pane, window, cx);
3536 }
3537
3538 if self.last_active_center_pane.is_none() {
3539 self.last_active_center_pane = Some(pane.downgrade());
3540 }
3541
3542 self.dismiss_zoomed_items_to_reveal(None, window, cx);
3543 if pane.read(cx).is_zoomed() {
3544 self.zoomed = Some(pane.downgrade().into());
3545 } else {
3546 self.zoomed = None;
3547 }
3548 self.zoomed_position = None;
3549 cx.emit(Event::ZoomChanged);
3550 self.update_active_view_for_followers(window, cx);
3551 pane.update(cx, |pane, _| {
3552 pane.track_alternate_file_items();
3553 });
3554
3555 cx.notify();
3556 }
3557
3558 fn set_active_pane(
3559 &mut self,
3560 pane: &Entity<Pane>,
3561 window: &mut Window,
3562 cx: &mut Context<Self>,
3563 ) {
3564 self.active_pane = pane.clone();
3565 self.active_item_path_changed(window, cx);
3566 self.last_active_center_pane = Some(pane.downgrade());
3567 }
3568
3569 fn handle_panel_focused(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3570 self.update_active_view_for_followers(window, cx);
3571 }
3572
3573 fn handle_pane_event(
3574 &mut self,
3575 pane: &Entity<Pane>,
3576 event: &pane::Event,
3577 window: &mut Window,
3578 cx: &mut Context<Self>,
3579 ) {
3580 let mut serialize_workspace = true;
3581 match event {
3582 pane::Event::AddItem { item } => {
3583 item.added_to_pane(self, pane.clone(), window, cx);
3584 cx.emit(Event::ItemAdded {
3585 item: item.boxed_clone(),
3586 });
3587 }
3588 pane::Event::Split(direction) => {
3589 self.split_and_clone(pane.clone(), *direction, window, cx);
3590 }
3591 pane::Event::JoinIntoNext => {
3592 self.join_pane_into_next(pane.clone(), window, cx);
3593 }
3594 pane::Event::JoinAll => {
3595 self.join_all_panes(window, cx);
3596 }
3597 pane::Event::Remove { focus_on_pane } => {
3598 self.remove_pane(pane.clone(), focus_on_pane.clone(), window, cx);
3599 }
3600 pane::Event::ActivateItem {
3601 local,
3602 focus_changed,
3603 } => {
3604 cx.on_next_frame(window, |_, window, _| {
3605 window.invalidate_character_coordinates();
3606 });
3607
3608 pane.update(cx, |pane, _| {
3609 pane.track_alternate_file_items();
3610 });
3611 if *local {
3612 self.unfollow_in_pane(&pane, window, cx);
3613 }
3614 if pane == self.active_pane() {
3615 self.active_item_path_changed(window, cx);
3616 self.update_active_view_for_followers(window, cx);
3617 }
3618 serialize_workspace = *focus_changed || pane != self.active_pane();
3619 }
3620 pane::Event::UserSavedItem { item, save_intent } => {
3621 cx.emit(Event::UserSavedItem {
3622 pane: pane.downgrade(),
3623 item: item.boxed_clone(),
3624 save_intent: *save_intent,
3625 });
3626 serialize_workspace = false;
3627 }
3628 pane::Event::ChangeItemTitle => {
3629 if *pane == self.active_pane {
3630 self.active_item_path_changed(window, cx);
3631 }
3632 serialize_workspace = false;
3633 }
3634 pane::Event::RemoveItem { .. } => {}
3635 pane::Event::RemovedItem { item } => {
3636 cx.emit(Event::ActiveItemChanged);
3637 self.update_window_edited(window, cx);
3638 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(item.item_id()) {
3639 if entry.get().entity_id() == pane.entity_id() {
3640 entry.remove();
3641 }
3642 }
3643 }
3644 pane::Event::Focus => {
3645 cx.on_next_frame(window, |_, window, _| {
3646 window.invalidate_character_coordinates();
3647 });
3648 self.handle_pane_focused(pane.clone(), window, cx);
3649 }
3650 pane::Event::ZoomIn => {
3651 if *pane == self.active_pane {
3652 pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
3653 if pane.read(cx).has_focus(window, cx) {
3654 self.zoomed = Some(pane.downgrade().into());
3655 self.zoomed_position = None;
3656 cx.emit(Event::ZoomChanged);
3657 }
3658 cx.notify();
3659 }
3660 }
3661 pane::Event::ZoomOut => {
3662 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
3663 if self.zoomed_position.is_none() {
3664 self.zoomed = None;
3665 cx.emit(Event::ZoomChanged);
3666 }
3667 cx.notify();
3668 }
3669 }
3670
3671 if serialize_workspace {
3672 self.serialize_workspace(window, cx);
3673 }
3674 }
3675
3676 pub fn unfollow_in_pane(
3677 &mut self,
3678 pane: &Entity<Pane>,
3679 window: &mut Window,
3680 cx: &mut Context<Workspace>,
3681 ) -> Option<CollaboratorId> {
3682 let leader_id = self.leader_for_pane(pane)?;
3683 self.unfollow(leader_id, window, cx);
3684 Some(leader_id)
3685 }
3686
3687 pub fn split_pane(
3688 &mut self,
3689 pane_to_split: Entity<Pane>,
3690 split_direction: SplitDirection,
3691 window: &mut Window,
3692 cx: &mut Context<Self>,
3693 ) -> Entity<Pane> {
3694 let new_pane = self.add_pane(window, cx);
3695 self.center
3696 .split(&pane_to_split, &new_pane, split_direction)
3697 .unwrap();
3698 cx.notify();
3699 new_pane
3700 }
3701
3702 pub fn split_and_clone(
3703 &mut self,
3704 pane: Entity<Pane>,
3705 direction: SplitDirection,
3706 window: &mut Window,
3707 cx: &mut Context<Self>,
3708 ) -> Option<Entity<Pane>> {
3709 let item = pane.read(cx).active_item()?;
3710 let maybe_pane_handle =
3711 if let Some(clone) = item.clone_on_split(self.database_id(), window, cx) {
3712 let new_pane = self.add_pane(window, cx);
3713 new_pane.update(cx, |pane, cx| {
3714 pane.add_item(clone, true, true, None, window, cx)
3715 });
3716 self.center.split(&pane, &new_pane, direction).unwrap();
3717 Some(new_pane)
3718 } else {
3719 None
3720 };
3721 cx.notify();
3722 maybe_pane_handle
3723 }
3724
3725 pub fn split_pane_with_item(
3726 &mut self,
3727 pane_to_split: WeakEntity<Pane>,
3728 split_direction: SplitDirection,
3729 from: WeakEntity<Pane>,
3730 item_id_to_move: EntityId,
3731 window: &mut Window,
3732 cx: &mut Context<Self>,
3733 ) {
3734 let Some(pane_to_split) = pane_to_split.upgrade() else {
3735 return;
3736 };
3737 let Some(from) = from.upgrade() else {
3738 return;
3739 };
3740
3741 let new_pane = self.add_pane(window, cx);
3742 move_item(&from, &new_pane, item_id_to_move, 0, window, cx);
3743 self.center
3744 .split(&pane_to_split, &new_pane, split_direction)
3745 .unwrap();
3746 cx.notify();
3747 }
3748
3749 pub fn split_pane_with_project_entry(
3750 &mut self,
3751 pane_to_split: WeakEntity<Pane>,
3752 split_direction: SplitDirection,
3753 project_entry: ProjectEntryId,
3754 window: &mut Window,
3755 cx: &mut Context<Self>,
3756 ) -> Option<Task<Result<()>>> {
3757 let pane_to_split = pane_to_split.upgrade()?;
3758 let new_pane = self.add_pane(window, cx);
3759 self.center
3760 .split(&pane_to_split, &new_pane, split_direction)
3761 .unwrap();
3762
3763 let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
3764 let task = self.open_path(path, Some(new_pane.downgrade()), true, window, cx);
3765 Some(cx.foreground_executor().spawn(async move {
3766 task.await?;
3767 Ok(())
3768 }))
3769 }
3770
3771 pub fn join_all_panes(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3772 let active_item = self.active_pane.read(cx).active_item();
3773 for pane in &self.panes {
3774 join_pane_into_active(&self.active_pane, pane, window, cx);
3775 }
3776 if let Some(active_item) = active_item {
3777 self.activate_item(active_item.as_ref(), true, true, window, cx);
3778 }
3779 cx.notify();
3780 }
3781
3782 pub fn join_pane_into_next(
3783 &mut self,
3784 pane: Entity<Pane>,
3785 window: &mut Window,
3786 cx: &mut Context<Self>,
3787 ) {
3788 let next_pane = self
3789 .find_pane_in_direction(SplitDirection::Right, cx)
3790 .or_else(|| self.find_pane_in_direction(SplitDirection::Down, cx))
3791 .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
3792 .or_else(|| self.find_pane_in_direction(SplitDirection::Up, cx));
3793 let Some(next_pane) = next_pane else {
3794 return;
3795 };
3796 move_all_items(&pane, &next_pane, window, cx);
3797 cx.notify();
3798 }
3799
3800 fn remove_pane(
3801 &mut self,
3802 pane: Entity<Pane>,
3803 focus_on: Option<Entity<Pane>>,
3804 window: &mut Window,
3805 cx: &mut Context<Self>,
3806 ) {
3807 if self.center.remove(&pane).unwrap() {
3808 self.force_remove_pane(&pane, &focus_on, window, cx);
3809 self.unfollow_in_pane(&pane, window, cx);
3810 self.last_leaders_by_pane.remove(&pane.downgrade());
3811 for removed_item in pane.read(cx).items() {
3812 self.panes_by_item.remove(&removed_item.item_id());
3813 }
3814
3815 cx.notify();
3816 } else {
3817 self.active_item_path_changed(window, cx);
3818 }
3819 cx.emit(Event::PaneRemoved);
3820 }
3821
3822 pub fn panes(&self) -> &[Entity<Pane>] {
3823 &self.panes
3824 }
3825
3826 pub fn active_pane(&self) -> &Entity<Pane> {
3827 &self.active_pane
3828 }
3829
3830 pub fn focused_pane(&self, window: &Window, cx: &App) -> Entity<Pane> {
3831 for dock in self.all_docks() {
3832 if dock.focus_handle(cx).contains_focused(window, cx) {
3833 if let Some(pane) = dock
3834 .read(cx)
3835 .active_panel()
3836 .and_then(|panel| panel.pane(cx))
3837 {
3838 return pane;
3839 }
3840 }
3841 }
3842 self.active_pane().clone()
3843 }
3844
3845 pub fn adjacent_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<Pane> {
3846 self.find_pane_in_direction(SplitDirection::Right, cx)
3847 .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
3848 .unwrap_or_else(|| {
3849 self.split_pane(self.active_pane.clone(), SplitDirection::Right, window, cx)
3850 })
3851 .clone()
3852 }
3853
3854 pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<Entity<Pane>> {
3855 let weak_pane = self.panes_by_item.get(&handle.item_id())?;
3856 weak_pane.upgrade()
3857 }
3858
3859 fn collaborator_left(&mut self, peer_id: PeerId, window: &mut Window, cx: &mut Context<Self>) {
3860 self.follower_states.retain(|leader_id, state| {
3861 if *leader_id == CollaboratorId::PeerId(peer_id) {
3862 for item in state.items_by_leader_view_id.values() {
3863 item.view.set_leader_id(None, window, cx);
3864 }
3865 false
3866 } else {
3867 true
3868 }
3869 });
3870 cx.notify();
3871 }
3872
3873 pub fn start_following(
3874 &mut self,
3875 leader_id: impl Into<CollaboratorId>,
3876 window: &mut Window,
3877 cx: &mut Context<Self>,
3878 ) -> Option<Task<Result<()>>> {
3879 let leader_id = leader_id.into();
3880 let pane = self.active_pane().clone();
3881
3882 self.last_leaders_by_pane
3883 .insert(pane.downgrade(), leader_id);
3884 self.unfollow(leader_id, window, cx);
3885 self.unfollow_in_pane(&pane, window, cx);
3886 self.follower_states.insert(
3887 leader_id,
3888 FollowerState {
3889 center_pane: pane.clone(),
3890 dock_pane: None,
3891 active_view_id: None,
3892 items_by_leader_view_id: Default::default(),
3893 },
3894 );
3895 cx.notify();
3896
3897 match leader_id {
3898 CollaboratorId::PeerId(leader_peer_id) => {
3899 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
3900 let project_id = self.project.read(cx).remote_id();
3901 let request = self.app_state.client.request(proto::Follow {
3902 room_id,
3903 project_id,
3904 leader_id: Some(leader_peer_id),
3905 });
3906
3907 Some(cx.spawn_in(window, async move |this, cx| {
3908 let response = request.await?;
3909 this.update(cx, |this, _| {
3910 let state = this
3911 .follower_states
3912 .get_mut(&leader_id)
3913 .ok_or_else(|| anyhow!("following interrupted"))?;
3914 state.active_view_id = response
3915 .active_view
3916 .as_ref()
3917 .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
3918 Ok::<_, anyhow::Error>(())
3919 })??;
3920 if let Some(view) = response.active_view {
3921 Self::add_view_from_leader(this.clone(), leader_peer_id, &view, cx).await?;
3922 }
3923 this.update_in(cx, |this, window, cx| {
3924 this.leader_updated(leader_id, window, cx)
3925 })?;
3926 Ok(())
3927 }))
3928 }
3929 CollaboratorId::Agent => {
3930 self.leader_updated(leader_id, window, cx)?;
3931 Some(Task::ready(Ok(())))
3932 }
3933 }
3934 }
3935
3936 pub fn follow_next_collaborator(
3937 &mut self,
3938 _: &FollowNextCollaborator,
3939 window: &mut Window,
3940 cx: &mut Context<Self>,
3941 ) {
3942 let collaborators = self.project.read(cx).collaborators();
3943 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
3944 let mut collaborators = collaborators.keys().copied();
3945 for peer_id in collaborators.by_ref() {
3946 if CollaboratorId::PeerId(peer_id) == leader_id {
3947 break;
3948 }
3949 }
3950 collaborators.next().map(CollaboratorId::PeerId)
3951 } else if let Some(last_leader_id) =
3952 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
3953 {
3954 match last_leader_id {
3955 CollaboratorId::PeerId(peer_id) => {
3956 if collaborators.contains_key(peer_id) {
3957 Some(*last_leader_id)
3958 } else {
3959 None
3960 }
3961 }
3962 CollaboratorId::Agent => Some(CollaboratorId::Agent),
3963 }
3964 } else {
3965 None
3966 };
3967
3968 let pane = self.active_pane.clone();
3969 let Some(leader_id) = next_leader_id.or_else(|| {
3970 Some(CollaboratorId::PeerId(
3971 collaborators.keys().copied().next()?,
3972 ))
3973 }) else {
3974 return;
3975 };
3976 if self.unfollow_in_pane(&pane, window, cx) == Some(leader_id) {
3977 return;
3978 }
3979 if let Some(task) = self.start_following(leader_id, window, cx) {
3980 task.detach_and_log_err(cx)
3981 }
3982 }
3983
3984 pub fn follow(
3985 &mut self,
3986 leader_id: impl Into<CollaboratorId>,
3987 window: &mut Window,
3988 cx: &mut Context<Self>,
3989 ) {
3990 let leader_id = leader_id.into();
3991
3992 if let CollaboratorId::PeerId(peer_id) = leader_id {
3993 let Some(room) = ActiveCall::global(cx).read(cx).room() else {
3994 return;
3995 };
3996 let room = room.read(cx);
3997 let Some(remote_participant) = room.remote_participant_for_peer_id(peer_id) else {
3998 return;
3999 };
4000
4001 let project = self.project.read(cx);
4002
4003 let other_project_id = match remote_participant.location {
4004 call::ParticipantLocation::External => None,
4005 call::ParticipantLocation::UnsharedProject => None,
4006 call::ParticipantLocation::SharedProject { project_id } => {
4007 if Some(project_id) == project.remote_id() {
4008 None
4009 } else {
4010 Some(project_id)
4011 }
4012 }
4013 };
4014
4015 // if they are active in another project, follow there.
4016 if let Some(project_id) = other_project_id {
4017 let app_state = self.app_state.clone();
4018 crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx)
4019 .detach_and_log_err(cx);
4020 }
4021 }
4022
4023 // if you're already following, find the right pane and focus it.
4024 if let Some(follower_state) = self.follower_states.get(&leader_id) {
4025 window.focus(&follower_state.pane().focus_handle(cx));
4026
4027 return;
4028 }
4029
4030 // Otherwise, follow.
4031 if let Some(task) = self.start_following(leader_id, window, cx) {
4032 task.detach_and_log_err(cx)
4033 }
4034 }
4035
4036 pub fn unfollow(
4037 &mut self,
4038 leader_id: impl Into<CollaboratorId>,
4039 window: &mut Window,
4040 cx: &mut Context<Self>,
4041 ) -> Option<()> {
4042 cx.notify();
4043
4044 let leader_id = leader_id.into();
4045 let state = self.follower_states.remove(&leader_id)?;
4046 for (_, item) in state.items_by_leader_view_id {
4047 item.view.set_leader_id(None, window, cx);
4048 }
4049
4050 if let CollaboratorId::PeerId(leader_peer_id) = leader_id {
4051 let project_id = self.project.read(cx).remote_id();
4052 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
4053 self.app_state
4054 .client
4055 .send(proto::Unfollow {
4056 room_id,
4057 project_id,
4058 leader_id: Some(leader_peer_id),
4059 })
4060 .log_err();
4061 }
4062
4063 Some(())
4064 }
4065
4066 pub fn is_being_followed(&self, id: impl Into<CollaboratorId>) -> bool {
4067 self.follower_states.contains_key(&id.into())
4068 }
4069
4070 fn active_item_path_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4071 cx.emit(Event::ActiveItemChanged);
4072 let active_entry = self.active_project_path(cx);
4073 self.project
4074 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
4075
4076 self.update_window_title(window, cx);
4077 }
4078
4079 fn update_window_title(&mut self, window: &mut Window, cx: &mut App) {
4080 let project = self.project().read(cx);
4081 let mut title = String::new();
4082
4083 for (i, name) in project.worktree_root_names(cx).enumerate() {
4084 if i > 0 {
4085 title.push_str(", ");
4086 }
4087 title.push_str(name);
4088 }
4089
4090 if title.is_empty() {
4091 title = "empty project".to_string();
4092 }
4093
4094 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
4095 let filename = path
4096 .path
4097 .file_name()
4098 .map(|s| s.to_string_lossy())
4099 .or_else(|| {
4100 Some(Cow::Borrowed(
4101 project
4102 .worktree_for_id(path.worktree_id, cx)?
4103 .read(cx)
4104 .root_name(),
4105 ))
4106 });
4107
4108 if let Some(filename) = filename {
4109 title.push_str(" — ");
4110 title.push_str(filename.as_ref());
4111 }
4112 }
4113
4114 if project.is_via_collab() {
4115 title.push_str(" ↙");
4116 } else if project.is_shared() {
4117 title.push_str(" ↗");
4118 }
4119
4120 window.set_window_title(&title);
4121 }
4122
4123 fn update_window_edited(&mut self, window: &mut Window, cx: &mut App) {
4124 let is_edited = !self.project.read(cx).is_disconnected(cx) && !self.dirty_items.is_empty();
4125 if is_edited != self.window_edited {
4126 self.window_edited = is_edited;
4127 window.set_window_edited(self.window_edited)
4128 }
4129 }
4130
4131 fn update_item_dirty_state(
4132 &mut self,
4133 item: &dyn ItemHandle,
4134 window: &mut Window,
4135 cx: &mut App,
4136 ) {
4137 let is_dirty = item.is_dirty(cx);
4138 let item_id = item.item_id();
4139 let was_dirty = self.dirty_items.contains_key(&item_id);
4140 if is_dirty == was_dirty {
4141 return;
4142 }
4143 if was_dirty {
4144 self.dirty_items.remove(&item_id);
4145 self.update_window_edited(window, cx);
4146 return;
4147 }
4148 if let Some(window_handle) = window.window_handle().downcast::<Self>() {
4149 let s = item.on_release(
4150 cx,
4151 Box::new(move |cx| {
4152 window_handle
4153 .update(cx, |this, window, cx| {
4154 this.dirty_items.remove(&item_id);
4155 this.update_window_edited(window, cx)
4156 })
4157 .ok();
4158 }),
4159 );
4160 self.dirty_items.insert(item_id, s);
4161 self.update_window_edited(window, cx);
4162 }
4163 }
4164
4165 fn render_notifications(&self, _window: &mut Window, _cx: &mut Context<Self>) -> Option<Div> {
4166 if self.notifications.is_empty() {
4167 None
4168 } else {
4169 Some(
4170 div()
4171 .absolute()
4172 .right_3()
4173 .bottom_3()
4174 .w_112()
4175 .h_full()
4176 .flex()
4177 .flex_col()
4178 .justify_end()
4179 .gap_2()
4180 .children(
4181 self.notifications
4182 .iter()
4183 .map(|(_, notification)| notification.clone().into_any()),
4184 ),
4185 )
4186 }
4187 }
4188
4189 // RPC handlers
4190
4191 fn active_view_for_follower(
4192 &self,
4193 follower_project_id: Option<u64>,
4194 window: &mut Window,
4195 cx: &mut Context<Self>,
4196 ) -> Option<proto::View> {
4197 let (item, panel_id) = self.active_item_for_followers(window, cx);
4198 let item = item?;
4199 let leader_id = self
4200 .pane_for(&*item)
4201 .and_then(|pane| self.leader_for_pane(&pane));
4202 let leader_peer_id = match leader_id {
4203 Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
4204 Some(CollaboratorId::Agent) | None => None,
4205 };
4206
4207 let item_handle = item.to_followable_item_handle(cx)?;
4208 let id = item_handle.remote_id(&self.app_state.client, window, cx)?;
4209 let variant = item_handle.to_state_proto(window, cx)?;
4210
4211 if item_handle.is_project_item(window, cx)
4212 && (follower_project_id.is_none()
4213 || follower_project_id != self.project.read(cx).remote_id())
4214 {
4215 return None;
4216 }
4217
4218 Some(proto::View {
4219 id: id.to_proto(),
4220 leader_id: leader_peer_id,
4221 variant: Some(variant),
4222 panel_id: panel_id.map(|id| id as i32),
4223 })
4224 }
4225
4226 fn handle_follow(
4227 &mut self,
4228 follower_project_id: Option<u64>,
4229 window: &mut Window,
4230 cx: &mut Context<Self>,
4231 ) -> proto::FollowResponse {
4232 let active_view = self.active_view_for_follower(follower_project_id, window, cx);
4233
4234 cx.notify();
4235 proto::FollowResponse {
4236 // TODO: Remove after version 0.145.x stabilizes.
4237 active_view_id: active_view.as_ref().and_then(|view| view.id.clone()),
4238 views: active_view.iter().cloned().collect(),
4239 active_view,
4240 }
4241 }
4242
4243 fn handle_update_followers(
4244 &mut self,
4245 leader_id: PeerId,
4246 message: proto::UpdateFollowers,
4247 _window: &mut Window,
4248 _cx: &mut Context<Self>,
4249 ) {
4250 self.leader_updates_tx
4251 .unbounded_send((leader_id, message))
4252 .ok();
4253 }
4254
4255 async fn process_leader_update(
4256 this: &WeakEntity<Self>,
4257 leader_id: PeerId,
4258 update: proto::UpdateFollowers,
4259 cx: &mut AsyncWindowContext,
4260 ) -> Result<()> {
4261 match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
4262 proto::update_followers::Variant::CreateView(view) => {
4263 let view_id = ViewId::from_proto(view.id.clone().context("invalid view id")?)?;
4264 let should_add_view = this.update(cx, |this, _| {
4265 if let Some(state) = this.follower_states.get_mut(&leader_id.into()) {
4266 anyhow::Ok(!state.items_by_leader_view_id.contains_key(&view_id))
4267 } else {
4268 anyhow::Ok(false)
4269 }
4270 })??;
4271
4272 if should_add_view {
4273 Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
4274 }
4275 }
4276 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
4277 let should_add_view = this.update(cx, |this, _| {
4278 if let Some(state) = this.follower_states.get_mut(&leader_id.into()) {
4279 state.active_view_id = update_active_view
4280 .view
4281 .as_ref()
4282 .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
4283
4284 if state.active_view_id.is_some_and(|view_id| {
4285 !state.items_by_leader_view_id.contains_key(&view_id)
4286 }) {
4287 anyhow::Ok(true)
4288 } else {
4289 anyhow::Ok(false)
4290 }
4291 } else {
4292 anyhow::Ok(false)
4293 }
4294 })??;
4295
4296 if should_add_view {
4297 if let Some(view) = update_active_view.view {
4298 Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
4299 }
4300 }
4301 }
4302 proto::update_followers::Variant::UpdateView(update_view) => {
4303 let variant = update_view
4304 .variant
4305 .ok_or_else(|| anyhow!("missing update view variant"))?;
4306 let id = update_view
4307 .id
4308 .ok_or_else(|| anyhow!("missing update view id"))?;
4309 let mut tasks = Vec::new();
4310 this.update_in(cx, |this, window, cx| {
4311 let project = this.project.clone();
4312 if let Some(state) = this.follower_states.get(&leader_id.into()) {
4313 let view_id = ViewId::from_proto(id.clone())?;
4314 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
4315 tasks.push(item.view.apply_update_proto(
4316 &project,
4317 variant.clone(),
4318 window,
4319 cx,
4320 ));
4321 }
4322 }
4323 anyhow::Ok(())
4324 })??;
4325 try_join_all(tasks).await.log_err();
4326 }
4327 }
4328 this.update_in(cx, |this, window, cx| {
4329 this.leader_updated(leader_id, window, cx)
4330 })?;
4331 Ok(())
4332 }
4333
4334 async fn add_view_from_leader(
4335 this: WeakEntity<Self>,
4336 leader_id: PeerId,
4337 view: &proto::View,
4338 cx: &mut AsyncWindowContext,
4339 ) -> Result<()> {
4340 let this = this.upgrade().context("workspace dropped")?;
4341
4342 let Some(id) = view.id.clone() else {
4343 return Err(anyhow!("no id for view"));
4344 };
4345 let id = ViewId::from_proto(id)?;
4346 let panel_id = view.panel_id.and_then(proto::PanelId::from_i32);
4347
4348 let pane = this.update(cx, |this, _cx| {
4349 let state = this
4350 .follower_states
4351 .get(&leader_id.into())
4352 .context("stopped following")?;
4353 anyhow::Ok(state.pane().clone())
4354 })??;
4355 let existing_item = pane.update_in(cx, |pane, window, cx| {
4356 let client = this.read(cx).client().clone();
4357 pane.items().find_map(|item| {
4358 let item = item.to_followable_item_handle(cx)?;
4359 if item.remote_id(&client, window, cx) == Some(id) {
4360 Some(item)
4361 } else {
4362 None
4363 }
4364 })
4365 })?;
4366 let item = if let Some(existing_item) = existing_item {
4367 existing_item
4368 } else {
4369 let variant = view.variant.clone();
4370 if variant.is_none() {
4371 Err(anyhow!("missing view variant"))?;
4372 }
4373
4374 let task = cx.update(|window, cx| {
4375 FollowableViewRegistry::from_state_proto(this.clone(), id, variant, window, cx)
4376 })?;
4377
4378 let Some(task) = task else {
4379 return Err(anyhow!(
4380 "failed to construct view from leader (maybe from a different version of zed?)"
4381 ));
4382 };
4383
4384 let mut new_item = task.await?;
4385 pane.update_in(cx, |pane, window, cx| {
4386 let mut item_to_remove = None;
4387 for (ix, item) in pane.items().enumerate() {
4388 if let Some(item) = item.to_followable_item_handle(cx) {
4389 match new_item.dedup(item.as_ref(), window, cx) {
4390 Some(item::Dedup::KeepExisting) => {
4391 new_item =
4392 item.boxed_clone().to_followable_item_handle(cx).unwrap();
4393 break;
4394 }
4395 Some(item::Dedup::ReplaceExisting) => {
4396 item_to_remove = Some((ix, item.item_id()));
4397 break;
4398 }
4399 None => {}
4400 }
4401 }
4402 }
4403
4404 if let Some((ix, id)) = item_to_remove {
4405 pane.remove_item(id, false, false, window, cx);
4406 pane.add_item(new_item.boxed_clone(), false, false, Some(ix), window, cx);
4407 }
4408 })?;
4409
4410 new_item
4411 };
4412
4413 this.update_in(cx, |this, window, cx| {
4414 let state = this.follower_states.get_mut(&leader_id.into())?;
4415 item.set_leader_id(Some(leader_id.into()), window, cx);
4416 state.items_by_leader_view_id.insert(
4417 id,
4418 FollowerView {
4419 view: item,
4420 location: panel_id,
4421 },
4422 );
4423
4424 Some(())
4425 })?;
4426
4427 Ok(())
4428 }
4429
4430 fn handle_agent_location_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4431 let Some(follower_state) = self.follower_states.get_mut(&CollaboratorId::Agent) else {
4432 return;
4433 };
4434
4435 if let Some(agent_location) = self.project.read(cx).agent_location() {
4436 let buffer_entity_id = agent_location.buffer.entity_id();
4437 let view_id = ViewId {
4438 creator: CollaboratorId::Agent,
4439 id: buffer_entity_id.as_u64(),
4440 };
4441 follower_state.active_view_id = Some(view_id);
4442
4443 let item = match follower_state.items_by_leader_view_id.entry(view_id) {
4444 hash_map::Entry::Occupied(entry) => Some(entry.into_mut()),
4445 hash_map::Entry::Vacant(entry) => {
4446 let existing_view =
4447 follower_state
4448 .center_pane
4449 .read(cx)
4450 .items()
4451 .find_map(|item| {
4452 let item = item.to_followable_item_handle(cx)?;
4453 if item.is_singleton(cx)
4454 && item.project_item_model_ids(cx).as_slice()
4455 == [buffer_entity_id]
4456 {
4457 Some(item)
4458 } else {
4459 None
4460 }
4461 });
4462 let view = existing_view.or_else(|| {
4463 agent_location.buffer.upgrade().and_then(|buffer| {
4464 cx.update_default_global(|registry: &mut ProjectItemRegistry, cx| {
4465 registry.build_item(buffer, self.project.clone(), None, window, cx)
4466 })?
4467 .to_followable_item_handle(cx)
4468 })
4469 });
4470
4471 if let Some(view) = view {
4472 Some(entry.insert(FollowerView {
4473 view,
4474 location: None,
4475 }))
4476 } else {
4477 None
4478 }
4479 }
4480 };
4481
4482 if let Some(item) = item {
4483 item.view
4484 .set_leader_id(Some(CollaboratorId::Agent), window, cx);
4485 item.view
4486 .update_agent_location(agent_location.position, window, cx);
4487 }
4488 } else {
4489 follower_state.active_view_id = None;
4490 }
4491
4492 self.leader_updated(CollaboratorId::Agent, window, cx);
4493 }
4494
4495 pub fn update_active_view_for_followers(&mut self, window: &mut Window, cx: &mut App) {
4496 let mut is_project_item = true;
4497 let mut update = proto::UpdateActiveView::default();
4498 if window.is_window_active() {
4499 let (active_item, panel_id) = self.active_item_for_followers(window, cx);
4500
4501 if let Some(item) = active_item {
4502 if item.item_focus_handle(cx).contains_focused(window, cx) {
4503 let leader_id = self
4504 .pane_for(&*item)
4505 .and_then(|pane| self.leader_for_pane(&pane));
4506 let leader_peer_id = match leader_id {
4507 Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
4508 Some(CollaboratorId::Agent) | None => None,
4509 };
4510
4511 if let Some(item) = item.to_followable_item_handle(cx) {
4512 let id = item
4513 .remote_id(&self.app_state.client, window, cx)
4514 .map(|id| id.to_proto());
4515
4516 if let Some(id) = id.clone() {
4517 if let Some(variant) = item.to_state_proto(window, cx) {
4518 let view = Some(proto::View {
4519 id: id.clone(),
4520 leader_id: leader_peer_id,
4521 variant: Some(variant),
4522 panel_id: panel_id.map(|id| id as i32),
4523 });
4524
4525 is_project_item = item.is_project_item(window, cx);
4526 update = proto::UpdateActiveView {
4527 view,
4528 // TODO: Remove after version 0.145.x stabilizes.
4529 id: id.clone(),
4530 leader_id: leader_peer_id,
4531 };
4532 }
4533 };
4534 }
4535 }
4536 }
4537 }
4538
4539 let active_view_id = update.view.as_ref().and_then(|view| view.id.as_ref());
4540 if active_view_id != self.last_active_view_id.as_ref() {
4541 self.last_active_view_id = active_view_id.cloned();
4542 self.update_followers(
4543 is_project_item,
4544 proto::update_followers::Variant::UpdateActiveView(update),
4545 window,
4546 cx,
4547 );
4548 }
4549 }
4550
4551 fn active_item_for_followers(
4552 &self,
4553 window: &mut Window,
4554 cx: &mut App,
4555 ) -> (Option<Box<dyn ItemHandle>>, Option<proto::PanelId>) {
4556 let mut active_item = None;
4557 let mut panel_id = None;
4558 for dock in self.all_docks() {
4559 if dock.focus_handle(cx).contains_focused(window, cx) {
4560 if let Some(panel) = dock.read(cx).active_panel() {
4561 if let Some(pane) = panel.pane(cx) {
4562 if let Some(item) = pane.read(cx).active_item() {
4563 active_item = Some(item);
4564 panel_id = panel.remote_id();
4565 break;
4566 }
4567 }
4568 }
4569 }
4570 }
4571
4572 if active_item.is_none() {
4573 active_item = self.active_pane().read(cx).active_item();
4574 }
4575 (active_item, panel_id)
4576 }
4577
4578 fn update_followers(
4579 &self,
4580 project_only: bool,
4581 update: proto::update_followers::Variant,
4582 _: &mut Window,
4583 cx: &mut App,
4584 ) -> Option<()> {
4585 // If this update only applies to for followers in the current project,
4586 // then skip it unless this project is shared. If it applies to all
4587 // followers, regardless of project, then set `project_id` to none,
4588 // indicating that it goes to all followers.
4589 let project_id = if project_only {
4590 Some(self.project.read(cx).remote_id()?)
4591 } else {
4592 None
4593 };
4594 self.app_state().workspace_store.update(cx, |store, cx| {
4595 store.update_followers(project_id, update, cx)
4596 })
4597 }
4598
4599 pub fn leader_for_pane(&self, pane: &Entity<Pane>) -> Option<CollaboratorId> {
4600 self.follower_states.iter().find_map(|(leader_id, state)| {
4601 if state.center_pane == *pane || state.dock_pane.as_ref() == Some(pane) {
4602 Some(*leader_id)
4603 } else {
4604 None
4605 }
4606 })
4607 }
4608
4609 fn leader_updated(
4610 &mut self,
4611 leader_id: impl Into<CollaboratorId>,
4612 window: &mut Window,
4613 cx: &mut Context<Self>,
4614 ) -> Option<Box<dyn ItemHandle>> {
4615 cx.notify();
4616
4617 let leader_id = leader_id.into();
4618 let (panel_id, item) = match leader_id {
4619 CollaboratorId::PeerId(peer_id) => self.active_item_for_peer(peer_id, window, cx)?,
4620 CollaboratorId::Agent => (None, self.active_item_for_agent()?),
4621 };
4622
4623 let state = self.follower_states.get(&leader_id)?;
4624 let mut transfer_focus = state.center_pane.read(cx).has_focus(window, cx);
4625 let pane;
4626 if let Some(panel_id) = panel_id {
4627 pane = self
4628 .activate_panel_for_proto_id(panel_id, window, cx)?
4629 .pane(cx)?;
4630 let state = self.follower_states.get_mut(&leader_id)?;
4631 state.dock_pane = Some(pane.clone());
4632 } else {
4633 pane = state.center_pane.clone();
4634 let state = self.follower_states.get_mut(&leader_id)?;
4635 if let Some(dock_pane) = state.dock_pane.take() {
4636 transfer_focus |= dock_pane.focus_handle(cx).contains_focused(window, cx);
4637 }
4638 }
4639
4640 pane.update(cx, |pane, cx| {
4641 let focus_active_item = pane.has_focus(window, cx) || transfer_focus;
4642 if let Some(index) = pane.index_for_item(item.as_ref()) {
4643 pane.activate_item(index, false, false, window, cx);
4644 } else {
4645 pane.add_item(item.boxed_clone(), false, false, None, window, cx)
4646 }
4647
4648 if focus_active_item {
4649 pane.focus_active_item(window, cx)
4650 }
4651 });
4652
4653 Some(item)
4654 }
4655
4656 fn active_item_for_agent(&self) -> Option<Box<dyn ItemHandle>> {
4657 let state = self.follower_states.get(&CollaboratorId::Agent)?;
4658 let active_view_id = state.active_view_id?;
4659 Some(
4660 state
4661 .items_by_leader_view_id
4662 .get(&active_view_id)?
4663 .view
4664 .boxed_clone(),
4665 )
4666 }
4667
4668 fn active_item_for_peer(
4669 &self,
4670 peer_id: PeerId,
4671 window: &mut Window,
4672 cx: &mut Context<Self>,
4673 ) -> Option<(Option<PanelId>, Box<dyn ItemHandle>)> {
4674 let call = self.active_call()?;
4675 let room = call.read(cx).room()?.read(cx);
4676 let participant = room.remote_participant_for_peer_id(peer_id)?;
4677 let leader_in_this_app;
4678 let leader_in_this_project;
4679 match participant.location {
4680 call::ParticipantLocation::SharedProject { project_id } => {
4681 leader_in_this_app = true;
4682 leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
4683 }
4684 call::ParticipantLocation::UnsharedProject => {
4685 leader_in_this_app = true;
4686 leader_in_this_project = false;
4687 }
4688 call::ParticipantLocation::External => {
4689 leader_in_this_app = false;
4690 leader_in_this_project = false;
4691 }
4692 };
4693 let state = self.follower_states.get(&peer_id.into())?;
4694 let mut item_to_activate = None;
4695 if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
4696 if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
4697 if leader_in_this_project || !item.view.is_project_item(window, cx) {
4698 item_to_activate = Some((item.location, item.view.boxed_clone()));
4699 }
4700 }
4701 } else if let Some(shared_screen) =
4702 self.shared_screen_for_peer(peer_id, &state.center_pane, window, cx)
4703 {
4704 item_to_activate = Some((None, Box::new(shared_screen)));
4705 }
4706 item_to_activate
4707 }
4708
4709 fn shared_screen_for_peer(
4710 &self,
4711 peer_id: PeerId,
4712 pane: &Entity<Pane>,
4713 window: &mut Window,
4714 cx: &mut App,
4715 ) -> Option<Entity<SharedScreen>> {
4716 let call = self.active_call()?;
4717 let room = call.read(cx).room()?.clone();
4718 let participant = room.read(cx).remote_participant_for_peer_id(peer_id)?;
4719 let track = participant.video_tracks.values().next()?.clone();
4720 let user = participant.user.clone();
4721
4722 for item in pane.read(cx).items_of_type::<SharedScreen>() {
4723 if item.read(cx).peer_id == peer_id {
4724 return Some(item);
4725 }
4726 }
4727
4728 Some(cx.new(|cx| SharedScreen::new(track, peer_id, user.clone(), room.clone(), window, cx)))
4729 }
4730
4731 pub fn on_window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4732 if window.is_window_active() {
4733 self.update_active_view_for_followers(window, cx);
4734
4735 if let Some(database_id) = self.database_id {
4736 cx.background_spawn(persistence::DB.update_timestamp(database_id))
4737 .detach();
4738 }
4739 } else {
4740 for pane in &self.panes {
4741 pane.update(cx, |pane, cx| {
4742 if let Some(item) = pane.active_item() {
4743 item.workspace_deactivated(window, cx);
4744 }
4745 for item in pane.items() {
4746 if matches!(
4747 item.workspace_settings(cx).autosave,
4748 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
4749 ) {
4750 Pane::autosave_item(item.as_ref(), self.project.clone(), window, cx)
4751 .detach_and_log_err(cx);
4752 }
4753 }
4754 });
4755 }
4756 }
4757 }
4758
4759 pub fn active_call(&self) -> Option<&Entity<ActiveCall>> {
4760 self.active_call.as_ref().map(|(call, _)| call)
4761 }
4762
4763 fn on_active_call_event(
4764 &mut self,
4765 _: &Entity<ActiveCall>,
4766 event: &call::room::Event,
4767 window: &mut Window,
4768 cx: &mut Context<Self>,
4769 ) {
4770 match event {
4771 call::room::Event::ParticipantLocationChanged { participant_id }
4772 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
4773 self.leader_updated(participant_id, window, cx);
4774 }
4775 _ => {}
4776 }
4777 }
4778
4779 pub fn database_id(&self) -> Option<WorkspaceId> {
4780 self.database_id
4781 }
4782
4783 pub fn session_id(&self) -> Option<String> {
4784 self.session_id.clone()
4785 }
4786
4787 fn local_paths(&self, cx: &App) -> Option<Vec<Arc<Path>>> {
4788 let project = self.project().read(cx);
4789
4790 if project.is_local() {
4791 Some(
4792 project
4793 .visible_worktrees(cx)
4794 .map(|worktree| worktree.read(cx).abs_path())
4795 .collect::<Vec<_>>(),
4796 )
4797 } else {
4798 None
4799 }
4800 }
4801
4802 fn remove_panes(&mut self, member: Member, window: &mut Window, cx: &mut Context<Workspace>) {
4803 match member {
4804 Member::Axis(PaneAxis { members, .. }) => {
4805 for child in members.iter() {
4806 self.remove_panes(child.clone(), window, cx)
4807 }
4808 }
4809 Member::Pane(pane) => {
4810 self.force_remove_pane(&pane, &None, window, cx);
4811 }
4812 }
4813 }
4814
4815 fn remove_from_session(&mut self, window: &mut Window, cx: &mut App) -> Task<()> {
4816 self.session_id.take();
4817 self.serialize_workspace_internal(window, cx)
4818 }
4819
4820 fn force_remove_pane(
4821 &mut self,
4822 pane: &Entity<Pane>,
4823 focus_on: &Option<Entity<Pane>>,
4824 window: &mut Window,
4825 cx: &mut Context<Workspace>,
4826 ) {
4827 self.panes.retain(|p| p != pane);
4828 if let Some(focus_on) = focus_on {
4829 focus_on.update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
4830 } else {
4831 if self.active_pane() == pane {
4832 self.panes
4833 .last()
4834 .unwrap()
4835 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
4836 }
4837 }
4838 if self.last_active_center_pane == Some(pane.downgrade()) {
4839 self.last_active_center_pane = None;
4840 }
4841 cx.notify();
4842 }
4843
4844 fn serialize_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4845 if self._schedule_serialize.is_none() {
4846 self._schedule_serialize = Some(cx.spawn_in(window, async move |this, cx| {
4847 cx.background_executor()
4848 .timer(Duration::from_millis(100))
4849 .await;
4850 this.update_in(cx, |this, window, cx| {
4851 this.serialize_workspace_internal(window, cx).detach();
4852 this._schedule_serialize.take();
4853 })
4854 .log_err();
4855 }));
4856 }
4857 }
4858
4859 fn serialize_workspace_internal(&self, window: &mut Window, cx: &mut App) -> Task<()> {
4860 let Some(database_id) = self.database_id() else {
4861 return Task::ready(());
4862 };
4863
4864 fn serialize_pane_handle(
4865 pane_handle: &Entity<Pane>,
4866 window: &mut Window,
4867 cx: &mut App,
4868 ) -> SerializedPane {
4869 let (items, active, pinned_count) = {
4870 let pane = pane_handle.read(cx);
4871 let active_item_id = pane.active_item().map(|item| item.item_id());
4872 (
4873 pane.items()
4874 .filter_map(|handle| {
4875 let handle = handle.to_serializable_item_handle(cx)?;
4876
4877 Some(SerializedItem {
4878 kind: Arc::from(handle.serialized_item_kind()),
4879 item_id: handle.item_id().as_u64(),
4880 active: Some(handle.item_id()) == active_item_id,
4881 preview: pane.is_active_preview_item(handle.item_id()),
4882 })
4883 })
4884 .collect::<Vec<_>>(),
4885 pane.has_focus(window, cx),
4886 pane.pinned_count(),
4887 )
4888 };
4889
4890 SerializedPane::new(items, active, pinned_count)
4891 }
4892
4893 fn build_serialized_pane_group(
4894 pane_group: &Member,
4895 window: &mut Window,
4896 cx: &mut App,
4897 ) -> SerializedPaneGroup {
4898 match pane_group {
4899 Member::Axis(PaneAxis {
4900 axis,
4901 members,
4902 flexes,
4903 bounding_boxes: _,
4904 }) => SerializedPaneGroup::Group {
4905 axis: SerializedAxis(*axis),
4906 children: members
4907 .iter()
4908 .map(|member| build_serialized_pane_group(member, window, cx))
4909 .collect::<Vec<_>>(),
4910 flexes: Some(flexes.lock().clone()),
4911 },
4912 Member::Pane(pane_handle) => {
4913 SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, window, cx))
4914 }
4915 }
4916 }
4917
4918 fn build_serialized_docks(
4919 this: &Workspace,
4920 window: &mut Window,
4921 cx: &mut App,
4922 ) -> DockStructure {
4923 let left_dock = this.left_dock.read(cx);
4924 let left_visible = left_dock.is_open();
4925 let left_active_panel = left_dock
4926 .active_panel()
4927 .map(|panel| panel.persistent_name().to_string());
4928 let left_dock_zoom = left_dock
4929 .active_panel()
4930 .map(|panel| panel.is_zoomed(window, cx))
4931 .unwrap_or(false);
4932
4933 let right_dock = this.right_dock.read(cx);
4934 let right_visible = right_dock.is_open();
4935 let right_active_panel = right_dock
4936 .active_panel()
4937 .map(|panel| panel.persistent_name().to_string());
4938 let right_dock_zoom = right_dock
4939 .active_panel()
4940 .map(|panel| panel.is_zoomed(window, cx))
4941 .unwrap_or(false);
4942
4943 let bottom_dock = this.bottom_dock.read(cx);
4944 let bottom_visible = bottom_dock.is_open();
4945 let bottom_active_panel = bottom_dock
4946 .active_panel()
4947 .map(|panel| panel.persistent_name().to_string());
4948 let bottom_dock_zoom = bottom_dock
4949 .active_panel()
4950 .map(|panel| panel.is_zoomed(window, cx))
4951 .unwrap_or(false);
4952
4953 DockStructure {
4954 left: DockData {
4955 visible: left_visible,
4956 active_panel: left_active_panel,
4957 zoom: left_dock_zoom,
4958 },
4959 right: DockData {
4960 visible: right_visible,
4961 active_panel: right_active_panel,
4962 zoom: right_dock_zoom,
4963 },
4964 bottom: DockData {
4965 visible: bottom_visible,
4966 active_panel: bottom_active_panel,
4967 zoom: bottom_dock_zoom,
4968 },
4969 }
4970 }
4971
4972 if let Some(location) = self.serialize_workspace_location(cx) {
4973 let breakpoints = self.project.update(cx, |project, cx| {
4974 project.breakpoint_store().read(cx).all_breakpoints(cx)
4975 });
4976
4977 let center_group = build_serialized_pane_group(&self.center.root, window, cx);
4978 let docks = build_serialized_docks(self, window, cx);
4979 let window_bounds = Some(SerializedWindowBounds(window.window_bounds()));
4980 let serialized_workspace = SerializedWorkspace {
4981 id: database_id,
4982 location,
4983 center_group,
4984 window_bounds,
4985 display: Default::default(),
4986 docks,
4987 centered_layout: self.centered_layout,
4988 session_id: self.session_id.clone(),
4989 breakpoints,
4990 window_id: Some(window.window_handle().window_id().as_u64()),
4991 };
4992
4993 return window.spawn(cx, async move |_| {
4994 persistence::DB.save_workspace(serialized_workspace).await;
4995 });
4996 }
4997 Task::ready(())
4998 }
4999
5000 fn serialize_workspace_location(&self, cx: &App) -> Option<SerializedWorkspaceLocation> {
5001 if let Some(ssh_project) = &self.serialized_ssh_project {
5002 Some(SerializedWorkspaceLocation::Ssh(ssh_project.clone()))
5003 } else if let Some(local_paths) = self.local_paths(cx) {
5004 if !local_paths.is_empty() {
5005 Some(SerializedWorkspaceLocation::from_local_paths(local_paths))
5006 } else {
5007 None
5008 }
5009 } else {
5010 None
5011 }
5012 }
5013
5014 fn update_history(&self, cx: &mut App) {
5015 let Some(id) = self.database_id() else {
5016 return;
5017 };
5018 let Some(location) = self.serialize_workspace_location(cx) else {
5019 return;
5020 };
5021 if let Some(manager) = HistoryManager::global(cx) {
5022 manager.update(cx, |this, cx| {
5023 this.update_history(id, HistoryManagerEntry::new(id, &location), cx);
5024 });
5025 }
5026 }
5027
5028 async fn serialize_items(
5029 this: &WeakEntity<Self>,
5030 items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
5031 cx: &mut AsyncWindowContext,
5032 ) -> Result<()> {
5033 const CHUNK_SIZE: usize = 200;
5034
5035 let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
5036
5037 while let Some(items_received) = serializable_items.next().await {
5038 let unique_items =
5039 items_received
5040 .into_iter()
5041 .fold(HashMap::default(), |mut acc, item| {
5042 acc.entry(item.item_id()).or_insert(item);
5043 acc
5044 });
5045
5046 // We use into_iter() here so that the references to the items are moved into
5047 // the tasks and not kept alive while we're sleeping.
5048 for (_, item) in unique_items.into_iter() {
5049 if let Ok(Some(task)) = this.update_in(cx, |workspace, window, cx| {
5050 item.serialize(workspace, false, window, cx)
5051 }) {
5052 cx.background_spawn(async move { task.await.log_err() })
5053 .detach();
5054 }
5055 }
5056
5057 cx.background_executor()
5058 .timer(SERIALIZATION_THROTTLE_TIME)
5059 .await;
5060 }
5061
5062 Ok(())
5063 }
5064
5065 pub(crate) fn enqueue_item_serialization(
5066 &mut self,
5067 item: Box<dyn SerializableItemHandle>,
5068 ) -> Result<()> {
5069 self.serializable_items_tx
5070 .unbounded_send(item)
5071 .map_err(|err| anyhow!("failed to send serializable item over channel: {}", err))
5072 }
5073
5074 pub(crate) fn load_workspace(
5075 serialized_workspace: SerializedWorkspace,
5076 paths_to_open: Vec<Option<ProjectPath>>,
5077 window: &mut Window,
5078 cx: &mut Context<Workspace>,
5079 ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
5080 cx.spawn_in(window, async move |workspace, cx| {
5081 let project = workspace.update(cx, |workspace, _| workspace.project().clone())?;
5082
5083 let mut center_group = None;
5084 let mut center_items = None;
5085
5086 // Traverse the splits tree and add to things
5087 if let Some((group, active_pane, items)) = serialized_workspace
5088 .center_group
5089 .deserialize(&project, serialized_workspace.id, workspace.clone(), cx)
5090 .await
5091 {
5092 center_items = Some(items);
5093 center_group = Some((group, active_pane))
5094 }
5095
5096 let mut items_by_project_path = HashMap::default();
5097 let mut item_ids_by_kind = HashMap::default();
5098 let mut all_deserialized_items = Vec::default();
5099 cx.update(|_, cx| {
5100 for item in center_items.unwrap_or_default().into_iter().flatten() {
5101 if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) {
5102 item_ids_by_kind
5103 .entry(serializable_item_handle.serialized_item_kind())
5104 .or_insert(Vec::new())
5105 .push(item.item_id().as_u64() as ItemId);
5106 }
5107
5108 if let Some(project_path) = item.project_path(cx) {
5109 items_by_project_path.insert(project_path, item.clone());
5110 }
5111 all_deserialized_items.push(item);
5112 }
5113 })?;
5114
5115 let opened_items = paths_to_open
5116 .into_iter()
5117 .map(|path_to_open| {
5118 path_to_open
5119 .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
5120 })
5121 .collect::<Vec<_>>();
5122
5123 // Remove old panes from workspace panes list
5124 workspace.update_in(cx, |workspace, window, cx| {
5125 if let Some((center_group, active_pane)) = center_group {
5126 workspace.remove_panes(workspace.center.root.clone(), window, cx);
5127
5128 // Swap workspace center group
5129 workspace.center = PaneGroup::with_root(center_group);
5130 if let Some(active_pane) = active_pane {
5131 workspace.set_active_pane(&active_pane, window, cx);
5132 cx.focus_self(window);
5133 } else {
5134 workspace.set_active_pane(&workspace.center.first_pane(), window, cx);
5135 }
5136 }
5137
5138 let docks = serialized_workspace.docks;
5139
5140 for (dock, serialized_dock) in [
5141 (&mut workspace.right_dock, docks.right),
5142 (&mut workspace.left_dock, docks.left),
5143 (&mut workspace.bottom_dock, docks.bottom),
5144 ]
5145 .iter_mut()
5146 {
5147 dock.update(cx, |dock, cx| {
5148 dock.serialized_dock = Some(serialized_dock.clone());
5149 dock.restore_state(window, cx);
5150 });
5151 }
5152
5153 cx.notify();
5154 })?;
5155
5156 let _ = project
5157 .update(cx, |project, cx| {
5158 project
5159 .breakpoint_store()
5160 .update(cx, |breakpoint_store, cx| {
5161 breakpoint_store
5162 .with_serialized_breakpoints(serialized_workspace.breakpoints, cx)
5163 })
5164 })?
5165 .await;
5166
5167 // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means
5168 // after loading the items, we might have different items and in order to avoid
5169 // the database filling up, we delete items that haven't been loaded now.
5170 //
5171 // The items that have been loaded, have been saved after they've been added to the workspace.
5172 let clean_up_tasks = workspace.update_in(cx, |_, window, cx| {
5173 item_ids_by_kind
5174 .into_iter()
5175 .map(|(item_kind, loaded_items)| {
5176 SerializableItemRegistry::cleanup(
5177 item_kind,
5178 serialized_workspace.id,
5179 loaded_items,
5180 window,
5181 cx,
5182 )
5183 .log_err()
5184 })
5185 .collect::<Vec<_>>()
5186 })?;
5187
5188 futures::future::join_all(clean_up_tasks).await;
5189
5190 workspace
5191 .update_in(cx, |workspace, window, cx| {
5192 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
5193 workspace.serialize_workspace_internal(window, cx).detach();
5194
5195 // Ensure that we mark the window as edited if we did load dirty items
5196 workspace.update_window_edited(window, cx);
5197 })
5198 .ok();
5199
5200 Ok(opened_items)
5201 })
5202 }
5203
5204 fn actions(&self, div: Div, window: &mut Window, cx: &mut Context<Self>) -> Div {
5205 self.add_workspace_actions_listeners(div, window, cx)
5206 .on_action(cx.listener(Self::close_inactive_items_and_panes))
5207 .on_action(cx.listener(Self::close_all_items_and_panes))
5208 .on_action(cx.listener(Self::save_all))
5209 .on_action(cx.listener(Self::send_keystrokes))
5210 .on_action(cx.listener(Self::add_folder_to_project))
5211 .on_action(cx.listener(Self::follow_next_collaborator))
5212 .on_action(cx.listener(Self::close_window))
5213 .on_action(cx.listener(Self::activate_pane_at_index))
5214 .on_action(cx.listener(Self::move_item_to_pane_at_index))
5215 .on_action(cx.listener(Self::move_focused_panel_to_next_position))
5216 .on_action(cx.listener(|workspace, _: &Unfollow, window, cx| {
5217 let pane = workspace.active_pane().clone();
5218 workspace.unfollow_in_pane(&pane, window, cx);
5219 }))
5220 .on_action(cx.listener(|workspace, action: &Save, window, cx| {
5221 workspace
5222 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), window, cx)
5223 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
5224 }))
5225 .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, window, cx| {
5226 workspace
5227 .save_active_item(SaveIntent::SaveWithoutFormat, window, cx)
5228 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
5229 }))
5230 .on_action(cx.listener(|workspace, _: &SaveAs, window, cx| {
5231 workspace
5232 .save_active_item(SaveIntent::SaveAs, window, cx)
5233 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
5234 }))
5235 .on_action(
5236 cx.listener(|workspace, _: &ActivatePreviousPane, window, cx| {
5237 workspace.activate_previous_pane(window, cx)
5238 }),
5239 )
5240 .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
5241 workspace.activate_next_pane(window, cx)
5242 }))
5243 .on_action(
5244 cx.listener(|workspace, _: &ActivateNextWindow, _window, cx| {
5245 workspace.activate_next_window(cx)
5246 }),
5247 )
5248 .on_action(
5249 cx.listener(|workspace, _: &ActivatePreviousWindow, _window, cx| {
5250 workspace.activate_previous_window(cx)
5251 }),
5252 )
5253 .on_action(cx.listener(|workspace, _: &ActivatePaneLeft, window, cx| {
5254 workspace.activate_pane_in_direction(SplitDirection::Left, window, cx)
5255 }))
5256 .on_action(cx.listener(|workspace, _: &ActivatePaneRight, window, cx| {
5257 workspace.activate_pane_in_direction(SplitDirection::Right, window, cx)
5258 }))
5259 .on_action(cx.listener(|workspace, _: &ActivatePaneUp, window, cx| {
5260 workspace.activate_pane_in_direction(SplitDirection::Up, window, cx)
5261 }))
5262 .on_action(cx.listener(|workspace, _: &ActivatePaneDown, window, cx| {
5263 workspace.activate_pane_in_direction(SplitDirection::Down, window, cx)
5264 }))
5265 .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
5266 workspace.activate_next_pane(window, cx)
5267 }))
5268 .on_action(cx.listener(
5269 |workspace, action: &MoveItemToPaneInDirection, window, cx| {
5270 workspace.move_item_to_pane_in_direction(action, window, cx)
5271 },
5272 ))
5273 .on_action(cx.listener(|workspace, _: &SwapPaneLeft, _, cx| {
5274 workspace.swap_pane_in_direction(SplitDirection::Left, cx)
5275 }))
5276 .on_action(cx.listener(|workspace, _: &SwapPaneRight, _, cx| {
5277 workspace.swap_pane_in_direction(SplitDirection::Right, cx)
5278 }))
5279 .on_action(cx.listener(|workspace, _: &SwapPaneUp, _, cx| {
5280 workspace.swap_pane_in_direction(SplitDirection::Up, cx)
5281 }))
5282 .on_action(cx.listener(|workspace, _: &SwapPaneDown, _, cx| {
5283 workspace.swap_pane_in_direction(SplitDirection::Down, cx)
5284 }))
5285 .on_action(cx.listener(|this, _: &ToggleLeftDock, window, cx| {
5286 this.toggle_dock(DockPosition::Left, window, cx);
5287 }))
5288 .on_action(cx.listener(
5289 |workspace: &mut Workspace, _: &ToggleRightDock, window, cx| {
5290 workspace.toggle_dock(DockPosition::Right, window, cx);
5291 },
5292 ))
5293 .on_action(cx.listener(
5294 |workspace: &mut Workspace, _: &ToggleBottomDock, window, cx| {
5295 workspace.toggle_dock(DockPosition::Bottom, window, cx);
5296 },
5297 ))
5298 .on_action(
5299 cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, window, cx| {
5300 workspace.close_all_docks(window, cx);
5301 }),
5302 )
5303 .on_action(cx.listener(
5304 |workspace: &mut Workspace, _: &ClearAllNotifications, _, cx| {
5305 workspace.clear_all_notifications(cx);
5306 },
5307 ))
5308 .on_action(cx.listener(
5309 |workspace: &mut Workspace, _: &SuppressNotification, _, cx| {
5310 if let Some((notification_id, _)) = workspace.notifications.pop() {
5311 workspace.suppress_notification(¬ification_id, cx);
5312 }
5313 },
5314 ))
5315 .on_action(cx.listener(
5316 |workspace: &mut Workspace, _: &ReopenClosedItem, window, cx| {
5317 workspace.reopen_closed_item(window, cx).detach();
5318 },
5319 ))
5320 .on_action(cx.listener(Workspace::toggle_centered_layout))
5321 .on_action(cx.listener(Workspace::cancel))
5322 }
5323
5324 #[cfg(any(test, feature = "test-support"))]
5325 pub fn test_new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
5326 use node_runtime::NodeRuntime;
5327 use session::Session;
5328
5329 let client = project.read(cx).client();
5330 let user_store = project.read(cx).user_store();
5331
5332 let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
5333 let session = cx.new(|cx| AppSession::new(Session::test(), cx));
5334 window.activate_window();
5335 let app_state = Arc::new(AppState {
5336 languages: project.read(cx).languages().clone(),
5337 workspace_store,
5338 client,
5339 user_store,
5340 fs: project.read(cx).fs().clone(),
5341 build_window_options: |_, _| Default::default(),
5342 node_runtime: NodeRuntime::unavailable(),
5343 session,
5344 });
5345 let workspace = Self::new(Default::default(), project, app_state, window, cx);
5346 workspace
5347 .active_pane
5348 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
5349 workspace
5350 }
5351
5352 pub fn register_action<A: Action>(
5353 &mut self,
5354 callback: impl Fn(&mut Self, &A, &mut Window, &mut Context<Self>) + 'static,
5355 ) -> &mut Self {
5356 let callback = Arc::new(callback);
5357
5358 self.workspace_actions.push(Box::new(move |div, _, cx| {
5359 let callback = callback.clone();
5360 div.on_action(cx.listener(move |workspace, event, window, cx| {
5361 (callback.clone())(workspace, event, window, cx)
5362 }))
5363 }));
5364 self
5365 }
5366
5367 fn add_workspace_actions_listeners(
5368 &self,
5369 mut div: Div,
5370 window: &mut Window,
5371 cx: &mut Context<Self>,
5372 ) -> Div {
5373 for action in self.workspace_actions.iter() {
5374 div = (action)(div, window, cx)
5375 }
5376 div
5377 }
5378
5379 pub fn has_active_modal(&self, _: &mut Window, cx: &mut App) -> bool {
5380 self.modal_layer.read(cx).has_active_modal()
5381 }
5382
5383 pub fn active_modal<V: ManagedView + 'static>(&self, cx: &App) -> Option<Entity<V>> {
5384 self.modal_layer.read(cx).active_modal()
5385 }
5386
5387 pub fn toggle_modal<V: ModalView, B>(&mut self, window: &mut Window, cx: &mut App, build: B)
5388 where
5389 B: FnOnce(&mut Window, &mut Context<V>) -> V,
5390 {
5391 self.modal_layer.update(cx, |modal_layer, cx| {
5392 modal_layer.toggle_modal(window, cx, build)
5393 })
5394 }
5395
5396 pub fn toggle_status_toast<V: ToastView>(&mut self, entity: Entity<V>, cx: &mut App) {
5397 self.toast_layer
5398 .update(cx, |toast_layer, cx| toast_layer.toggle_toast(cx, entity))
5399 }
5400
5401 pub fn toggle_centered_layout(
5402 &mut self,
5403 _: &ToggleCenteredLayout,
5404 _: &mut Window,
5405 cx: &mut Context<Self>,
5406 ) {
5407 self.centered_layout = !self.centered_layout;
5408 if let Some(database_id) = self.database_id() {
5409 cx.background_spawn(DB.set_centered_layout(database_id, self.centered_layout))
5410 .detach_and_log_err(cx);
5411 }
5412 cx.notify();
5413 }
5414
5415 fn adjust_padding(padding: Option<f32>) -> f32 {
5416 padding
5417 .unwrap_or(Self::DEFAULT_PADDING)
5418 .clamp(0.0, Self::MAX_PADDING)
5419 }
5420
5421 fn render_dock(
5422 &self,
5423 position: DockPosition,
5424 dock: &Entity<Dock>,
5425 window: &mut Window,
5426 cx: &mut App,
5427 ) -> Option<Div> {
5428 if self.zoomed_position == Some(position) {
5429 return None;
5430 }
5431
5432 let leader_border = dock.read(cx).active_panel().and_then(|panel| {
5433 let pane = panel.pane(cx)?;
5434 let follower_states = &self.follower_states;
5435 leader_border_for_pane(follower_states, &pane, window, cx)
5436 });
5437
5438 Some(
5439 div()
5440 .flex()
5441 .flex_none()
5442 .overflow_hidden()
5443 .child(dock.clone())
5444 .children(leader_border),
5445 )
5446 }
5447
5448 pub fn for_window(window: &mut Window, _: &mut App) -> Option<Entity<Workspace>> {
5449 window.root().flatten()
5450 }
5451
5452 pub fn zoomed_item(&self) -> Option<&AnyWeakView> {
5453 self.zoomed.as_ref()
5454 }
5455
5456 pub fn activate_next_window(&mut self, cx: &mut Context<Self>) {
5457 let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
5458 return;
5459 };
5460 let windows = cx.windows();
5461 let Some(next_window) = windows
5462 .iter()
5463 .cycle()
5464 .skip_while(|window| window.window_id() != current_window_id)
5465 .nth(1)
5466 else {
5467 return;
5468 };
5469 next_window
5470 .update(cx, |_, window, _| window.activate_window())
5471 .ok();
5472 }
5473
5474 pub fn activate_previous_window(&mut self, cx: &mut Context<Self>) {
5475 let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
5476 return;
5477 };
5478 let windows = cx.windows();
5479 let Some(prev_window) = windows
5480 .iter()
5481 .rev()
5482 .cycle()
5483 .skip_while(|window| window.window_id() != current_window_id)
5484 .nth(1)
5485 else {
5486 return;
5487 };
5488 prev_window
5489 .update(cx, |_, window, _| window.activate_window())
5490 .ok();
5491 }
5492
5493 pub fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
5494 if let Some((notification_id, _)) = self.notifications.pop() {
5495 dismiss_app_notification(¬ification_id, cx);
5496 return;
5497 }
5498
5499 cx.propagate();
5500 }
5501}
5502
5503fn leader_border_for_pane(
5504 follower_states: &HashMap<CollaboratorId, FollowerState>,
5505 pane: &Entity<Pane>,
5506 _: &Window,
5507 cx: &App,
5508) -> Option<Div> {
5509 let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
5510 if state.pane() == pane {
5511 Some((*leader_id, state))
5512 } else {
5513 None
5514 }
5515 })?;
5516
5517 let mut leader_color = match leader_id {
5518 CollaboratorId::PeerId(leader_peer_id) => {
5519 let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
5520 let leader = room.remote_participant_for_peer_id(leader_peer_id)?;
5521
5522 cx.theme()
5523 .players()
5524 .color_for_participant(leader.participant_index.0)
5525 .cursor
5526 }
5527 CollaboratorId::Agent => cx.theme().players().agent().cursor,
5528 };
5529 leader_color.fade_out(0.3);
5530 Some(
5531 div()
5532 .absolute()
5533 .size_full()
5534 .left_0()
5535 .top_0()
5536 .border_2()
5537 .border_color(leader_color),
5538 )
5539}
5540
5541fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
5542 ZED_WINDOW_POSITION
5543 .zip(*ZED_WINDOW_SIZE)
5544 .map(|(position, size)| Bounds {
5545 origin: position,
5546 size,
5547 })
5548}
5549
5550fn open_items(
5551 serialized_workspace: Option<SerializedWorkspace>,
5552 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
5553 window: &mut Window,
5554 cx: &mut Context<Workspace>,
5555) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> + use<> {
5556 let restored_items = serialized_workspace.map(|serialized_workspace| {
5557 Workspace::load_workspace(
5558 serialized_workspace,
5559 project_paths_to_open
5560 .iter()
5561 .map(|(_, project_path)| project_path)
5562 .cloned()
5563 .collect(),
5564 window,
5565 cx,
5566 )
5567 });
5568
5569 cx.spawn_in(window, async move |workspace, cx| {
5570 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
5571
5572 if let Some(restored_items) = restored_items {
5573 let restored_items = restored_items.await?;
5574
5575 let restored_project_paths = restored_items
5576 .iter()
5577 .filter_map(|item| {
5578 cx.update(|_, cx| item.as_ref()?.project_path(cx))
5579 .ok()
5580 .flatten()
5581 })
5582 .collect::<HashSet<_>>();
5583
5584 for restored_item in restored_items {
5585 opened_items.push(restored_item.map(Ok));
5586 }
5587
5588 project_paths_to_open
5589 .iter_mut()
5590 .for_each(|(_, project_path)| {
5591 if let Some(project_path_to_open) = project_path {
5592 if restored_project_paths.contains(project_path_to_open) {
5593 *project_path = None;
5594 }
5595 }
5596 });
5597 } else {
5598 for _ in 0..project_paths_to_open.len() {
5599 opened_items.push(None);
5600 }
5601 }
5602 assert!(opened_items.len() == project_paths_to_open.len());
5603
5604 let tasks =
5605 project_paths_to_open
5606 .into_iter()
5607 .enumerate()
5608 .map(|(ix, (abs_path, project_path))| {
5609 let workspace = workspace.clone();
5610 cx.spawn(async move |cx| {
5611 let file_project_path = project_path?;
5612 let abs_path_task = workspace.update(cx, |workspace, cx| {
5613 workspace.project().update(cx, |project, cx| {
5614 project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
5615 })
5616 });
5617
5618 // We only want to open file paths here. If one of the items
5619 // here is a directory, it was already opened further above
5620 // with a `find_or_create_worktree`.
5621 if let Ok(task) = abs_path_task {
5622 if task.await.map_or(true, |p| p.is_file()) {
5623 return Some((
5624 ix,
5625 workspace
5626 .update_in(cx, |workspace, window, cx| {
5627 workspace.open_path(
5628 file_project_path,
5629 None,
5630 true,
5631 window,
5632 cx,
5633 )
5634 })
5635 .log_err()?
5636 .await,
5637 ));
5638 }
5639 }
5640 None
5641 })
5642 });
5643
5644 let tasks = tasks.collect::<Vec<_>>();
5645
5646 let tasks = futures::future::join_all(tasks);
5647 for (ix, path_open_result) in tasks.await.into_iter().flatten() {
5648 opened_items[ix] = Some(path_open_result);
5649 }
5650
5651 Ok(opened_items)
5652 })
5653}
5654
5655enum ActivateInDirectionTarget {
5656 Pane(Entity<Pane>),
5657 Dock(Entity<Dock>),
5658}
5659
5660fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncApp) {
5661 workspace
5662 .update(cx, |workspace, _, cx| {
5663 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
5664 struct DatabaseFailedNotification;
5665
5666 workspace.show_notification(
5667 NotificationId::unique::<DatabaseFailedNotification>(),
5668 cx,
5669 |cx| {
5670 cx.new(|cx| {
5671 MessageNotification::new("Failed to load the database file.", cx)
5672 .primary_message("File an Issue")
5673 .primary_icon(IconName::Plus)
5674 .primary_on_click(|window, cx| {
5675 window.dispatch_action(Box::new(FileBugReport), cx)
5676 })
5677 })
5678 },
5679 );
5680 }
5681 })
5682 .log_err();
5683}
5684
5685impl Focusable for Workspace {
5686 fn focus_handle(&self, cx: &App) -> FocusHandle {
5687 self.active_pane.focus_handle(cx)
5688 }
5689}
5690
5691#[derive(Clone)]
5692struct DraggedDock(DockPosition);
5693
5694impl Render for DraggedDock {
5695 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
5696 gpui::Empty
5697 }
5698}
5699
5700impl Render for Workspace {
5701 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
5702 let mut context = KeyContext::new_with_defaults();
5703 context.add("Workspace");
5704 context.set("keyboard_layout", cx.keyboard_layout().name().to_string());
5705 let centered_layout = self.centered_layout
5706 && self.center.panes().len() == 1
5707 && self.active_item(cx).is_some();
5708 let render_padding = |size| {
5709 (size > 0.0).then(|| {
5710 div()
5711 .h_full()
5712 .w(relative(size))
5713 .bg(cx.theme().colors().editor_background)
5714 .border_color(cx.theme().colors().pane_group_border)
5715 })
5716 };
5717 let paddings = if centered_layout {
5718 let settings = WorkspaceSettings::get_global(cx).centered_layout;
5719 (
5720 render_padding(Self::adjust_padding(settings.left_padding)),
5721 render_padding(Self::adjust_padding(settings.right_padding)),
5722 )
5723 } else {
5724 (None, None)
5725 };
5726 let ui_font = theme::setup_ui_font(window, cx);
5727
5728 let theme = cx.theme().clone();
5729 let colors = theme.colors();
5730
5731 client_side_decorations(
5732 self.actions(div(), window, cx)
5733 .key_context(context)
5734 .relative()
5735 .size_full()
5736 .flex()
5737 .flex_col()
5738 .font(ui_font)
5739 .gap_0()
5740 .justify_start()
5741 .items_start()
5742 .text_color(colors.text)
5743 .overflow_hidden()
5744 .children(self.titlebar_item.clone())
5745 .child(
5746 div()
5747 .size_full()
5748 .relative()
5749 .flex_1()
5750 .flex()
5751 .flex_col()
5752 .child(
5753 div()
5754 .id("workspace")
5755 .bg(colors.background)
5756 .relative()
5757 .flex_1()
5758 .w_full()
5759 .flex()
5760 .flex_col()
5761 .overflow_hidden()
5762 .border_t_1()
5763 .border_b_1()
5764 .border_color(colors.border)
5765 .child({
5766 let this = cx.entity().clone();
5767 canvas(
5768 move |bounds, window, cx| {
5769 this.update(cx, |this, cx| {
5770 let bounds_changed = this.bounds != bounds;
5771 this.bounds = bounds;
5772
5773 if bounds_changed {
5774 this.left_dock.update(cx, |dock, cx| {
5775 dock.clamp_panel_size(
5776 bounds.size.width,
5777 window,
5778 cx,
5779 )
5780 });
5781
5782 this.right_dock.update(cx, |dock, cx| {
5783 dock.clamp_panel_size(
5784 bounds.size.width,
5785 window,
5786 cx,
5787 )
5788 });
5789
5790 this.bottom_dock.update(cx, |dock, cx| {
5791 dock.clamp_panel_size(
5792 bounds.size.height,
5793 window,
5794 cx,
5795 )
5796 });
5797 }
5798 })
5799 },
5800 |_, _, _, _| {},
5801 )
5802 .absolute()
5803 .size_full()
5804 })
5805 .when(self.zoomed.is_none(), |this| {
5806 this.on_drag_move(cx.listener(
5807 move |workspace,
5808 e: &DragMoveEvent<DraggedDock>,
5809 window,
5810 cx| {
5811 if workspace.previous_dock_drag_coordinates
5812 != Some(e.event.position)
5813 {
5814 workspace.previous_dock_drag_coordinates =
5815 Some(e.event.position);
5816 match e.drag(cx).0 {
5817 DockPosition::Left => {
5818 resize_left_dock(
5819 e.event.position.x
5820 - workspace.bounds.left(),
5821 workspace,
5822 window,
5823 cx,
5824 );
5825 }
5826 DockPosition::Right => {
5827 resize_right_dock(
5828 workspace.bounds.right()
5829 - e.event.position.x,
5830 workspace,
5831 window,
5832 cx,
5833 );
5834 }
5835 DockPosition::Bottom => {
5836 resize_bottom_dock(
5837 workspace.bounds.bottom()
5838 - e.event.position.y,
5839 workspace,
5840 window,
5841 cx,
5842 );
5843 }
5844 };
5845 workspace.serialize_workspace(window, cx);
5846 }
5847 },
5848 ))
5849 })
5850 .child({
5851 match self.bottom_dock_layout {
5852 BottomDockLayout::Full => div()
5853 .flex()
5854 .flex_col()
5855 .h_full()
5856 .child(
5857 div()
5858 .flex()
5859 .flex_row()
5860 .flex_1()
5861 .overflow_hidden()
5862 .children(self.render_dock(
5863 DockPosition::Left,
5864 &self.left_dock,
5865 window,
5866 cx,
5867 ))
5868 .child(
5869 div()
5870 .flex()
5871 .flex_col()
5872 .flex_1()
5873 .overflow_hidden()
5874 .child(
5875 h_flex()
5876 .flex_1()
5877 .when_some(
5878 paddings.0,
5879 |this, p| {
5880 this.child(
5881 p.border_r_1(),
5882 )
5883 },
5884 )
5885 .child(self.center.render(
5886 self.zoomed.as_ref(),
5887 &PaneRenderContext {
5888 follower_states:
5889 &self.follower_states,
5890 active_call: self.active_call(),
5891 active_pane: &self.active_pane,
5892 app_state: &self.app_state,
5893 project: &self.project,
5894 workspace: &self.weak_self,
5895 },
5896 window,
5897 cx,
5898 ))
5899 .when_some(
5900 paddings.1,
5901 |this, p| {
5902 this.child(
5903 p.border_l_1(),
5904 )
5905 },
5906 ),
5907 ),
5908 )
5909 .children(self.render_dock(
5910 DockPosition::Right,
5911 &self.right_dock,
5912 window,
5913 cx,
5914 )),
5915 )
5916 .child(div().w_full().children(self.render_dock(
5917 DockPosition::Bottom,
5918 &self.bottom_dock,
5919 window,
5920 cx
5921 ))),
5922
5923 BottomDockLayout::LeftAligned => div()
5924 .flex()
5925 .flex_row()
5926 .h_full()
5927 .child(
5928 div()
5929 .flex()
5930 .flex_col()
5931 .flex_1()
5932 .h_full()
5933 .child(
5934 div()
5935 .flex()
5936 .flex_row()
5937 .flex_1()
5938 .children(self.render_dock(DockPosition::Left, &self.left_dock, window, cx))
5939 .child(
5940 div()
5941 .flex()
5942 .flex_col()
5943 .flex_1()
5944 .overflow_hidden()
5945 .child(
5946 h_flex()
5947 .flex_1()
5948 .when_some(paddings.0, |this, p| this.child(p.border_r_1()))
5949 .child(self.center.render(
5950 self.zoomed.as_ref(),
5951 &PaneRenderContext {
5952 follower_states:
5953 &self.follower_states,
5954 active_call: self.active_call(),
5955 active_pane: &self.active_pane,
5956 app_state: &self.app_state,
5957 project: &self.project,
5958 workspace: &self.weak_self,
5959 },
5960 window,
5961 cx,
5962 ))
5963 .when_some(paddings.1, |this, p| this.child(p.border_l_1())),
5964 )
5965 )
5966 )
5967 .child(
5968 div()
5969 .w_full()
5970 .children(self.render_dock(DockPosition::Bottom, &self.bottom_dock, window, cx))
5971 ),
5972 )
5973 .children(self.render_dock(
5974 DockPosition::Right,
5975 &self.right_dock,
5976 window,
5977 cx,
5978 )),
5979
5980 BottomDockLayout::RightAligned => div()
5981 .flex()
5982 .flex_row()
5983 .h_full()
5984 .children(self.render_dock(
5985 DockPosition::Left,
5986 &self.left_dock,
5987 window,
5988 cx,
5989 ))
5990 .child(
5991 div()
5992 .flex()
5993 .flex_col()
5994 .flex_1()
5995 .h_full()
5996 .child(
5997 div()
5998 .flex()
5999 .flex_row()
6000 .flex_1()
6001 .child(
6002 div()
6003 .flex()
6004 .flex_col()
6005 .flex_1()
6006 .overflow_hidden()
6007 .child(
6008 h_flex()
6009 .flex_1()
6010 .when_some(paddings.0, |this, p| this.child(p.border_r_1()))
6011 .child(self.center.render(
6012 self.zoomed.as_ref(),
6013 &PaneRenderContext {
6014 follower_states:
6015 &self.follower_states,
6016 active_call: self.active_call(),
6017 active_pane: &self.active_pane,
6018 app_state: &self.app_state,
6019 project: &self.project,
6020 workspace: &self.weak_self,
6021 },
6022 window,
6023 cx,
6024 ))
6025 .when_some(paddings.1, |this, p| this.child(p.border_l_1())),
6026 )
6027 )
6028 .children(self.render_dock(DockPosition::Right, &self.right_dock, window, cx))
6029 )
6030 .child(
6031 div()
6032 .w_full()
6033 .children(self.render_dock(DockPosition::Bottom, &self.bottom_dock, window, cx))
6034 ),
6035 ),
6036
6037 BottomDockLayout::Contained => div()
6038 .flex()
6039 .flex_row()
6040 .h_full()
6041 .children(self.render_dock(
6042 DockPosition::Left,
6043 &self.left_dock,
6044 window,
6045 cx,
6046 ))
6047 .child(
6048 div()
6049 .flex()
6050 .flex_col()
6051 .flex_1()
6052 .overflow_hidden()
6053 .child(
6054 h_flex()
6055 .flex_1()
6056 .when_some(paddings.0, |this, p| {
6057 this.child(p.border_r_1())
6058 })
6059 .child(self.center.render(
6060 self.zoomed.as_ref(),
6061 &PaneRenderContext {
6062 follower_states:
6063 &self.follower_states,
6064 active_call: self.active_call(),
6065 active_pane: &self.active_pane,
6066 app_state: &self.app_state,
6067 project: &self.project,
6068 workspace: &self.weak_self,
6069 },
6070 window,
6071 cx,
6072 ))
6073 .when_some(paddings.1, |this, p| {
6074 this.child(p.border_l_1())
6075 }),
6076 )
6077 .children(self.render_dock(
6078 DockPosition::Bottom,
6079 &self.bottom_dock,
6080 window,
6081 cx,
6082 )),
6083 )
6084 .children(self.render_dock(
6085 DockPosition::Right,
6086 &self.right_dock,
6087 window,
6088 cx,
6089 )),
6090 }
6091 })
6092 .children(self.zoomed.as_ref().and_then(|view| {
6093 let zoomed_view = view.upgrade()?;
6094 let div = div()
6095 .occlude()
6096 .absolute()
6097 .overflow_hidden()
6098 .border_color(colors.border)
6099 .bg(colors.background)
6100 .child(zoomed_view)
6101 .inset_0()
6102 .shadow_lg();
6103
6104 Some(match self.zoomed_position {
6105 Some(DockPosition::Left) => div.right_2().border_r_1(),
6106 Some(DockPosition::Right) => div.left_2().border_l_1(),
6107 Some(DockPosition::Bottom) => div.top_2().border_t_1(),
6108 None => {
6109 div.top_2().bottom_2().left_2().right_2().border_1()
6110 }
6111 })
6112 }))
6113 .children(self.render_notifications(window, cx)),
6114 )
6115 .child(self.status_bar.clone())
6116 .child(self.modal_layer.clone())
6117 .child(self.toast_layer.clone()),
6118 ),
6119 window,
6120 cx,
6121 )
6122 }
6123}
6124
6125fn resize_bottom_dock(
6126 new_size: Pixels,
6127 workspace: &mut Workspace,
6128 window: &mut Window,
6129 cx: &mut App,
6130) {
6131 let size =
6132 new_size.min(workspace.bounds.bottom() - RESIZE_HANDLE_SIZE - workspace.bounds.top());
6133 workspace.bottom_dock.update(cx, |bottom_dock, cx| {
6134 bottom_dock.resize_active_panel(Some(size), window, cx);
6135 });
6136}
6137
6138fn resize_right_dock(
6139 new_size: Pixels,
6140 workspace: &mut Workspace,
6141 window: &mut Window,
6142 cx: &mut App,
6143) {
6144 let size = new_size.max(workspace.bounds.left() - RESIZE_HANDLE_SIZE);
6145 workspace.right_dock.update(cx, |right_dock, cx| {
6146 right_dock.resize_active_panel(Some(size), window, cx);
6147 });
6148}
6149
6150fn resize_left_dock(
6151 new_size: Pixels,
6152 workspace: &mut Workspace,
6153 window: &mut Window,
6154 cx: &mut App,
6155) {
6156 let size = new_size.min(workspace.bounds.right() - RESIZE_HANDLE_SIZE);
6157
6158 workspace.left_dock.update(cx, |left_dock, cx| {
6159 left_dock.resize_active_panel(Some(size), window, cx);
6160 });
6161}
6162
6163impl WorkspaceStore {
6164 pub fn new(client: Arc<Client>, cx: &mut Context<Self>) -> Self {
6165 Self {
6166 workspaces: Default::default(),
6167 _subscriptions: vec![
6168 client.add_request_handler(cx.weak_entity(), Self::handle_follow),
6169 client.add_message_handler(cx.weak_entity(), Self::handle_update_followers),
6170 ],
6171 client,
6172 }
6173 }
6174
6175 pub fn update_followers(
6176 &self,
6177 project_id: Option<u64>,
6178 update: proto::update_followers::Variant,
6179 cx: &App,
6180 ) -> Option<()> {
6181 let active_call = ActiveCall::try_global(cx)?;
6182 let room_id = active_call.read(cx).room()?.read(cx).id();
6183 self.client
6184 .send(proto::UpdateFollowers {
6185 room_id,
6186 project_id,
6187 variant: Some(update),
6188 })
6189 .log_err()
6190 }
6191
6192 pub async fn handle_follow(
6193 this: Entity<Self>,
6194 envelope: TypedEnvelope<proto::Follow>,
6195 mut cx: AsyncApp,
6196 ) -> Result<proto::FollowResponse> {
6197 this.update(&mut cx, |this, cx| {
6198 let follower = Follower {
6199 project_id: envelope.payload.project_id,
6200 peer_id: envelope.original_sender_id()?,
6201 };
6202
6203 let mut response = proto::FollowResponse::default();
6204 this.workspaces.retain(|workspace| {
6205 workspace
6206 .update(cx, |workspace, window, cx| {
6207 let handler_response =
6208 workspace.handle_follow(follower.project_id, window, cx);
6209 if let Some(active_view) = handler_response.active_view.clone() {
6210 if workspace.project.read(cx).remote_id() == follower.project_id {
6211 response.active_view = Some(active_view)
6212 }
6213 }
6214 })
6215 .is_ok()
6216 });
6217
6218 Ok(response)
6219 })?
6220 }
6221
6222 async fn handle_update_followers(
6223 this: Entity<Self>,
6224 envelope: TypedEnvelope<proto::UpdateFollowers>,
6225 mut cx: AsyncApp,
6226 ) -> Result<()> {
6227 let leader_id = envelope.original_sender_id()?;
6228 let update = envelope.payload;
6229
6230 this.update(&mut cx, |this, cx| {
6231 this.workspaces.retain(|workspace| {
6232 workspace
6233 .update(cx, |workspace, window, cx| {
6234 let project_id = workspace.project.read(cx).remote_id();
6235 if update.project_id != project_id && update.project_id.is_some() {
6236 return;
6237 }
6238 workspace.handle_update_followers(leader_id, update.clone(), window, cx);
6239 })
6240 .is_ok()
6241 });
6242 Ok(())
6243 })?
6244 }
6245}
6246
6247impl ViewId {
6248 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
6249 Ok(Self {
6250 creator: message
6251 .creator
6252 .map(CollaboratorId::PeerId)
6253 .ok_or_else(|| anyhow!("creator is missing"))?,
6254 id: message.id,
6255 })
6256 }
6257
6258 pub(crate) fn to_proto(self) -> Option<proto::ViewId> {
6259 if let CollaboratorId::PeerId(peer_id) = self.creator {
6260 Some(proto::ViewId {
6261 creator: Some(peer_id),
6262 id: self.id,
6263 })
6264 } else {
6265 None
6266 }
6267 }
6268}
6269
6270impl FollowerState {
6271 fn pane(&self) -> &Entity<Pane> {
6272 self.dock_pane.as_ref().unwrap_or(&self.center_pane)
6273 }
6274}
6275
6276pub trait WorkspaceHandle {
6277 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath>;
6278}
6279
6280impl WorkspaceHandle for Entity<Workspace> {
6281 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath> {
6282 self.read(cx)
6283 .worktrees(cx)
6284 .flat_map(|worktree| {
6285 let worktree_id = worktree.read(cx).id();
6286 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
6287 worktree_id,
6288 path: f.path.clone(),
6289 })
6290 })
6291 .collect::<Vec<_>>()
6292 }
6293}
6294
6295impl std::fmt::Debug for OpenPaths {
6296 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
6297 f.debug_struct("OpenPaths")
6298 .field("paths", &self.paths)
6299 .finish()
6300 }
6301}
6302
6303pub async fn last_opened_workspace_location() -> Option<SerializedWorkspaceLocation> {
6304 DB.last_workspace().await.log_err().flatten()
6305}
6306
6307pub fn last_session_workspace_locations(
6308 last_session_id: &str,
6309 last_session_window_stack: Option<Vec<WindowId>>,
6310) -> Option<Vec<SerializedWorkspaceLocation>> {
6311 DB.last_session_workspace_locations(last_session_id, last_session_window_stack)
6312 .log_err()
6313}
6314
6315actions!(
6316 collab,
6317 [
6318 OpenChannelNotes,
6319 Mute,
6320 Deafen,
6321 LeaveCall,
6322 ShareProject,
6323 ScreenShare
6324 ]
6325);
6326actions!(zed, [OpenLog]);
6327
6328async fn join_channel_internal(
6329 channel_id: ChannelId,
6330 app_state: &Arc<AppState>,
6331 requesting_window: Option<WindowHandle<Workspace>>,
6332 active_call: &Entity<ActiveCall>,
6333 cx: &mut AsyncApp,
6334) -> Result<bool> {
6335 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
6336 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
6337 return (false, None);
6338 };
6339
6340 let already_in_channel = room.channel_id() == Some(channel_id);
6341 let should_prompt = room.is_sharing_project()
6342 && !room.remote_participants().is_empty()
6343 && !already_in_channel;
6344 let open_room = if already_in_channel {
6345 active_call.room().cloned()
6346 } else {
6347 None
6348 };
6349 (should_prompt, open_room)
6350 })?;
6351
6352 if let Some(room) = open_room {
6353 let task = room.update(cx, |room, cx| {
6354 if let Some((project, host)) = room.most_active_project(cx) {
6355 return Some(join_in_room_project(project, host, app_state.clone(), cx));
6356 }
6357
6358 None
6359 })?;
6360 if let Some(task) = task {
6361 task.await?;
6362 }
6363 return anyhow::Ok(true);
6364 }
6365
6366 if should_prompt {
6367 if let Some(workspace) = requesting_window {
6368 let answer = workspace
6369 .update(cx, |_, window, cx| {
6370 window.prompt(
6371 PromptLevel::Warning,
6372 "Do you want to switch channels?",
6373 Some("Leaving this call will unshare your current project."),
6374 &["Yes, Join Channel", "Cancel"],
6375 cx,
6376 )
6377 })?
6378 .await;
6379
6380 if answer == Ok(1) {
6381 return Ok(false);
6382 }
6383 } else {
6384 return Ok(false); // unreachable!() hopefully
6385 }
6386 }
6387
6388 let client = cx.update(|cx| active_call.read(cx).client())?;
6389
6390 let mut client_status = client.status();
6391
6392 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
6393 'outer: loop {
6394 let Some(status) = client_status.recv().await else {
6395 return Err(anyhow!("error connecting"));
6396 };
6397
6398 match status {
6399 Status::Connecting
6400 | Status::Authenticating
6401 | Status::Reconnecting
6402 | Status::Reauthenticating => continue,
6403 Status::Connected { .. } => break 'outer,
6404 Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
6405 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
6406 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
6407 return Err(ErrorCode::Disconnected.into());
6408 }
6409 }
6410 }
6411
6412 let room = active_call
6413 .update(cx, |active_call, cx| {
6414 active_call.join_channel(channel_id, cx)
6415 })?
6416 .await?;
6417
6418 let Some(room) = room else {
6419 return anyhow::Ok(true);
6420 };
6421
6422 room.update(cx, |room, _| room.room_update_completed())?
6423 .await;
6424
6425 let task = room.update(cx, |room, cx| {
6426 if let Some((project, host)) = room.most_active_project(cx) {
6427 return Some(join_in_room_project(project, host, app_state.clone(), cx));
6428 }
6429
6430 // If you are the first to join a channel, see if you should share your project.
6431 if room.remote_participants().is_empty() && !room.local_participant_is_guest() {
6432 if let Some(workspace) = requesting_window {
6433 let project = workspace.update(cx, |workspace, _, cx| {
6434 let project = workspace.project.read(cx);
6435
6436 if !CallSettings::get_global(cx).share_on_join {
6437 return None;
6438 }
6439
6440 if (project.is_local() || project.is_via_ssh())
6441 && project.visible_worktrees(cx).any(|tree| {
6442 tree.read(cx)
6443 .root_entry()
6444 .map_or(false, |entry| entry.is_dir())
6445 })
6446 {
6447 Some(workspace.project.clone())
6448 } else {
6449 None
6450 }
6451 });
6452 if let Ok(Some(project)) = project {
6453 return Some(cx.spawn(async move |room, cx| {
6454 room.update(cx, |room, cx| room.share_project(project, cx))?
6455 .await?;
6456 Ok(())
6457 }));
6458 }
6459 }
6460 }
6461
6462 None
6463 })?;
6464 if let Some(task) = task {
6465 task.await?;
6466 return anyhow::Ok(true);
6467 }
6468 anyhow::Ok(false)
6469}
6470
6471pub fn join_channel(
6472 channel_id: ChannelId,
6473 app_state: Arc<AppState>,
6474 requesting_window: Option<WindowHandle<Workspace>>,
6475 cx: &mut App,
6476) -> Task<Result<()>> {
6477 let active_call = ActiveCall::global(cx);
6478 cx.spawn(async move |cx| {
6479 let result = join_channel_internal(
6480 channel_id,
6481 &app_state,
6482 requesting_window,
6483 &active_call,
6484 cx,
6485 )
6486 .await;
6487
6488 // join channel succeeded, and opened a window
6489 if matches!(result, Ok(true)) {
6490 return anyhow::Ok(());
6491 }
6492
6493 // find an existing workspace to focus and show call controls
6494 let mut active_window =
6495 requesting_window.or_else(|| activate_any_workspace_window( cx));
6496 if active_window.is_none() {
6497 // no open workspaces, make one to show the error in (blergh)
6498 let (window_handle, _) = cx
6499 .update(|cx| {
6500 Workspace::new_local(vec![], app_state.clone(), requesting_window, None, cx)
6501 })?
6502 .await?;
6503
6504 if result.is_ok() {
6505 cx.update(|cx| {
6506 cx.dispatch_action(&OpenChannelNotes);
6507 }).log_err();
6508 }
6509
6510 active_window = Some(window_handle);
6511 }
6512
6513 if let Err(err) = result {
6514 log::error!("failed to join channel: {}", err);
6515 if let Some(active_window) = active_window {
6516 active_window
6517 .update(cx, |_, window, cx| {
6518 let detail: SharedString = match err.error_code() {
6519 ErrorCode::SignedOut => {
6520 "Please sign in to continue.".into()
6521 }
6522 ErrorCode::UpgradeRequired => {
6523 "Your are running an unsupported version of Zed. Please update to continue.".into()
6524 }
6525 ErrorCode::NoSuchChannel => {
6526 "No matching channel was found. Please check the link and try again.".into()
6527 }
6528 ErrorCode::Forbidden => {
6529 "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
6530 }
6531 ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
6532 _ => format!("{}\n\nPlease try again.", err).into(),
6533 };
6534 window.prompt(
6535 PromptLevel::Critical,
6536 "Failed to join channel",
6537 Some(&detail),
6538 &["Ok"],
6539 cx)
6540 })?
6541 .await
6542 .ok();
6543 }
6544 }
6545
6546 // return ok, we showed the error to the user.
6547 anyhow::Ok(())
6548 })
6549}
6550
6551pub async fn get_any_active_workspace(
6552 app_state: Arc<AppState>,
6553 mut cx: AsyncApp,
6554) -> anyhow::Result<WindowHandle<Workspace>> {
6555 // find an existing workspace to focus and show call controls
6556 let active_window = activate_any_workspace_window(&mut cx);
6557 if active_window.is_none() {
6558 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, cx))?
6559 .await?;
6560 }
6561 activate_any_workspace_window(&mut cx).context("could not open zed")
6562}
6563
6564fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<Workspace>> {
6565 cx.update(|cx| {
6566 if let Some(workspace_window) = cx
6567 .active_window()
6568 .and_then(|window| window.downcast::<Workspace>())
6569 {
6570 return Some(workspace_window);
6571 }
6572
6573 for window in cx.windows() {
6574 if let Some(workspace_window) = window.downcast::<Workspace>() {
6575 workspace_window
6576 .update(cx, |_, window, _| window.activate_window())
6577 .ok();
6578 return Some(workspace_window);
6579 }
6580 }
6581 None
6582 })
6583 .ok()
6584 .flatten()
6585}
6586
6587pub fn local_workspace_windows(cx: &App) -> Vec<WindowHandle<Workspace>> {
6588 cx.windows()
6589 .into_iter()
6590 .filter_map(|window| window.downcast::<Workspace>())
6591 .filter(|workspace| {
6592 workspace
6593 .read(cx)
6594 .is_ok_and(|workspace| workspace.project.read(cx).is_local())
6595 })
6596 .collect()
6597}
6598
6599#[derive(Default)]
6600pub struct OpenOptions {
6601 pub visible: Option<OpenVisible>,
6602 pub focus: Option<bool>,
6603 pub open_new_workspace: Option<bool>,
6604 pub replace_window: Option<WindowHandle<Workspace>>,
6605 pub env: Option<HashMap<String, String>>,
6606}
6607
6608#[allow(clippy::type_complexity)]
6609pub fn open_paths(
6610 abs_paths: &[PathBuf],
6611 app_state: Arc<AppState>,
6612 open_options: OpenOptions,
6613 cx: &mut App,
6614) -> Task<
6615 anyhow::Result<(
6616 WindowHandle<Workspace>,
6617 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
6618 )>,
6619> {
6620 let abs_paths = abs_paths.to_vec();
6621 let mut existing = None;
6622 let mut best_match = None;
6623 let mut open_visible = OpenVisible::All;
6624
6625 cx.spawn(async move |cx| {
6626 if open_options.open_new_workspace != Some(true) {
6627 let all_paths = abs_paths.iter().map(|path| app_state.fs.metadata(path));
6628 let all_metadatas = futures::future::join_all(all_paths)
6629 .await
6630 .into_iter()
6631 .filter_map(|result| result.ok().flatten())
6632 .collect::<Vec<_>>();
6633
6634 cx.update(|cx| {
6635 for window in local_workspace_windows(&cx) {
6636 if let Ok(workspace) = window.read(&cx) {
6637 let m = workspace.project.read(&cx).visibility_for_paths(
6638 &abs_paths,
6639 &all_metadatas,
6640 open_options.open_new_workspace == None,
6641 cx,
6642 );
6643 if m > best_match {
6644 existing = Some(window);
6645 best_match = m;
6646 } else if best_match.is_none()
6647 && open_options.open_new_workspace == Some(false)
6648 {
6649 existing = Some(window)
6650 }
6651 }
6652 }
6653 })?;
6654
6655 if open_options.open_new_workspace.is_none() && existing.is_none() {
6656 if all_metadatas.iter().all(|file| !file.is_dir) {
6657 cx.update(|cx| {
6658 if let Some(window) = cx
6659 .active_window()
6660 .and_then(|window| window.downcast::<Workspace>())
6661 {
6662 if let Ok(workspace) = window.read(cx) {
6663 let project = workspace.project().read(cx);
6664 if project.is_local() && !project.is_via_collab() {
6665 existing = Some(window);
6666 open_visible = OpenVisible::None;
6667 return;
6668 }
6669 }
6670 }
6671 for window in local_workspace_windows(cx) {
6672 if let Ok(workspace) = window.read(cx) {
6673 let project = workspace.project().read(cx);
6674 if project.is_via_collab() {
6675 continue;
6676 }
6677 existing = Some(window);
6678 open_visible = OpenVisible::None;
6679 break;
6680 }
6681 }
6682 })?;
6683 }
6684 }
6685 }
6686
6687 if let Some(existing) = existing {
6688 let open_task = existing
6689 .update(cx, |workspace, window, cx| {
6690 window.activate_window();
6691 workspace.open_paths(
6692 abs_paths,
6693 OpenOptions {
6694 visible: Some(open_visible),
6695 ..Default::default()
6696 },
6697 None,
6698 window,
6699 cx,
6700 )
6701 })?
6702 .await;
6703
6704 _ = existing.update(cx, |workspace, _, cx| {
6705 for item in open_task.iter().flatten() {
6706 if let Err(e) = item {
6707 workspace.show_error(&e, cx);
6708 }
6709 }
6710 });
6711
6712 Ok((existing, open_task))
6713 } else {
6714 cx.update(move |cx| {
6715 Workspace::new_local(
6716 abs_paths,
6717 app_state.clone(),
6718 open_options.replace_window,
6719 open_options.env,
6720 cx,
6721 )
6722 })?
6723 .await
6724 }
6725 })
6726}
6727
6728pub fn open_new(
6729 open_options: OpenOptions,
6730 app_state: Arc<AppState>,
6731 cx: &mut App,
6732 init: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + 'static + Send,
6733) -> Task<anyhow::Result<()>> {
6734 let task = Workspace::new_local(Vec::new(), app_state, None, open_options.env, cx);
6735 cx.spawn(async move |cx| {
6736 let (workspace, opened_paths) = task.await?;
6737 workspace.update(cx, |workspace, window, cx| {
6738 if opened_paths.is_empty() {
6739 init(workspace, window, cx)
6740 }
6741 })?;
6742 Ok(())
6743 })
6744}
6745
6746pub fn create_and_open_local_file(
6747 path: &'static Path,
6748 window: &mut Window,
6749 cx: &mut Context<Workspace>,
6750 default_content: impl 'static + Send + FnOnce() -> Rope,
6751) -> Task<Result<Box<dyn ItemHandle>>> {
6752 cx.spawn_in(window, async move |workspace, cx| {
6753 let fs = workspace.update(cx, |workspace, _| workspace.app_state().fs.clone())?;
6754 if !fs.is_file(path).await {
6755 fs.create_file(path, Default::default()).await?;
6756 fs.save(path, &default_content(), Default::default())
6757 .await?;
6758 }
6759
6760 let mut items = workspace
6761 .update_in(cx, |workspace, window, cx| {
6762 workspace.with_local_workspace(window, cx, |workspace, window, cx| {
6763 workspace.open_paths(
6764 vec![path.to_path_buf()],
6765 OpenOptions {
6766 visible: Some(OpenVisible::None),
6767 ..Default::default()
6768 },
6769 None,
6770 window,
6771 cx,
6772 )
6773 })
6774 })?
6775 .await?
6776 .await;
6777
6778 let item = items.pop().flatten();
6779 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
6780 })
6781}
6782
6783pub fn open_ssh_project_with_new_connection(
6784 window: WindowHandle<Workspace>,
6785 connection_options: SshConnectionOptions,
6786 cancel_rx: oneshot::Receiver<()>,
6787 delegate: Arc<dyn SshClientDelegate>,
6788 app_state: Arc<AppState>,
6789 paths: Vec<PathBuf>,
6790 cx: &mut App,
6791) -> Task<Result<()>> {
6792 cx.spawn(async move |cx| {
6793 let (serialized_ssh_project, workspace_id, serialized_workspace) =
6794 serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
6795
6796 let session = match cx
6797 .update(|cx| {
6798 remote::SshRemoteClient::new(
6799 ConnectionIdentifier::Workspace(workspace_id.0),
6800 connection_options,
6801 cancel_rx,
6802 delegate,
6803 cx,
6804 )
6805 })?
6806 .await?
6807 {
6808 Some(result) => result,
6809 None => return Ok(()),
6810 };
6811
6812 let project = cx.update(|cx| {
6813 project::Project::ssh(
6814 session,
6815 app_state.client.clone(),
6816 app_state.node_runtime.clone(),
6817 app_state.user_store.clone(),
6818 app_state.languages.clone(),
6819 app_state.fs.clone(),
6820 cx,
6821 )
6822 })?;
6823
6824 open_ssh_project_inner(
6825 project,
6826 paths,
6827 serialized_ssh_project,
6828 workspace_id,
6829 serialized_workspace,
6830 app_state,
6831 window,
6832 cx,
6833 )
6834 .await
6835 })
6836}
6837
6838pub fn open_ssh_project_with_existing_connection(
6839 connection_options: SshConnectionOptions,
6840 project: Entity<Project>,
6841 paths: Vec<PathBuf>,
6842 app_state: Arc<AppState>,
6843 window: WindowHandle<Workspace>,
6844 cx: &mut AsyncApp,
6845) -> Task<Result<()>> {
6846 cx.spawn(async move |cx| {
6847 let (serialized_ssh_project, workspace_id, serialized_workspace) =
6848 serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
6849
6850 open_ssh_project_inner(
6851 project,
6852 paths,
6853 serialized_ssh_project,
6854 workspace_id,
6855 serialized_workspace,
6856 app_state,
6857 window,
6858 cx,
6859 )
6860 .await
6861 })
6862}
6863
6864async fn open_ssh_project_inner(
6865 project: Entity<Project>,
6866 paths: Vec<PathBuf>,
6867 serialized_ssh_project: SerializedSshProject,
6868 workspace_id: WorkspaceId,
6869 serialized_workspace: Option<SerializedWorkspace>,
6870 app_state: Arc<AppState>,
6871 window: WindowHandle<Workspace>,
6872 cx: &mut AsyncApp,
6873) -> Result<()> {
6874 let toolchains = DB.toolchains(workspace_id).await?;
6875 for (toolchain, worktree_id, path) in toolchains {
6876 project
6877 .update(cx, |this, cx| {
6878 this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx)
6879 })?
6880 .await;
6881 }
6882 let mut project_paths_to_open = vec![];
6883 let mut project_path_errors = vec![];
6884
6885 for path in paths {
6886 let result = cx
6887 .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))?
6888 .await;
6889 match result {
6890 Ok((_, project_path)) => {
6891 project_paths_to_open.push((path.clone(), Some(project_path)));
6892 }
6893 Err(error) => {
6894 project_path_errors.push(error);
6895 }
6896 };
6897 }
6898
6899 if project_paths_to_open.is_empty() {
6900 return Err(project_path_errors
6901 .pop()
6902 .unwrap_or_else(|| anyhow!("no paths given")));
6903 }
6904
6905 cx.update_window(window.into(), |_, window, cx| {
6906 window.replace_root(cx, |window, cx| {
6907 telemetry::event!("SSH Project Opened");
6908
6909 let mut workspace =
6910 Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
6911 workspace.set_serialized_ssh_project(serialized_ssh_project);
6912 workspace.update_history(cx);
6913 workspace
6914 });
6915 })?;
6916
6917 window
6918 .update(cx, |_, window, cx| {
6919 window.activate_window();
6920 open_items(serialized_workspace, project_paths_to_open, window, cx)
6921 })?
6922 .await?;
6923
6924 window.update(cx, |workspace, _, cx| {
6925 for error in project_path_errors {
6926 if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
6927 if let Some(path) = error.error_tag("path") {
6928 workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
6929 }
6930 } else {
6931 workspace.show_error(&error, cx)
6932 }
6933 }
6934 })?;
6935
6936 Ok(())
6937}
6938
6939fn serialize_ssh_project(
6940 connection_options: SshConnectionOptions,
6941 paths: Vec<PathBuf>,
6942 cx: &AsyncApp,
6943) -> Task<
6944 Result<(
6945 SerializedSshProject,
6946 WorkspaceId,
6947 Option<SerializedWorkspace>,
6948 )>,
6949> {
6950 cx.background_spawn(async move {
6951 let serialized_ssh_project = persistence::DB
6952 .get_or_create_ssh_project(
6953 connection_options.host.clone(),
6954 connection_options.port,
6955 paths
6956 .iter()
6957 .map(|path| path.to_string_lossy().to_string())
6958 .collect::<Vec<_>>(),
6959 connection_options.username.clone(),
6960 )
6961 .await?;
6962
6963 let serialized_workspace =
6964 persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
6965
6966 let workspace_id = if let Some(workspace_id) =
6967 serialized_workspace.as_ref().map(|workspace| workspace.id)
6968 {
6969 workspace_id
6970 } else {
6971 persistence::DB.next_id().await?
6972 };
6973
6974 Ok((serialized_ssh_project, workspace_id, serialized_workspace))
6975 })
6976}
6977
6978pub fn join_in_room_project(
6979 project_id: u64,
6980 follow_user_id: u64,
6981 app_state: Arc<AppState>,
6982 cx: &mut App,
6983) -> Task<Result<()>> {
6984 let windows = cx.windows();
6985 cx.spawn(async move |cx| {
6986 let existing_workspace = windows.into_iter().find_map(|window_handle| {
6987 window_handle
6988 .downcast::<Workspace>()
6989 .and_then(|window_handle| {
6990 window_handle
6991 .update(cx, |workspace, _window, cx| {
6992 if workspace.project().read(cx).remote_id() == Some(project_id) {
6993 Some(window_handle)
6994 } else {
6995 None
6996 }
6997 })
6998 .unwrap_or(None)
6999 })
7000 });
7001
7002 let workspace = if let Some(existing_workspace) = existing_workspace {
7003 existing_workspace
7004 } else {
7005 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
7006 let room = active_call
7007 .read_with(cx, |call, _| call.room().cloned())?
7008 .ok_or_else(|| anyhow!("not in a call"))?;
7009 let project = room
7010 .update(cx, |room, cx| {
7011 room.join_project(
7012 project_id,
7013 app_state.languages.clone(),
7014 app_state.fs.clone(),
7015 cx,
7016 )
7017 })?
7018 .await?;
7019
7020 let window_bounds_override = window_bounds_env_override();
7021 cx.update(|cx| {
7022 let mut options = (app_state.build_window_options)(None, cx);
7023 options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
7024 cx.open_window(options, |window, cx| {
7025 cx.new(|cx| {
7026 Workspace::new(Default::default(), project, app_state.clone(), window, cx)
7027 })
7028 })
7029 })??
7030 };
7031
7032 workspace.update(cx, |workspace, window, cx| {
7033 cx.activate(true);
7034 window.activate_window();
7035
7036 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
7037 let follow_peer_id = room
7038 .read(cx)
7039 .remote_participants()
7040 .iter()
7041 .find(|(_, participant)| participant.user.id == follow_user_id)
7042 .map(|(_, p)| p.peer_id)
7043 .or_else(|| {
7044 // If we couldn't follow the given user, follow the host instead.
7045 let collaborator = workspace
7046 .project()
7047 .read(cx)
7048 .collaborators()
7049 .values()
7050 .find(|collaborator| collaborator.is_host)?;
7051 Some(collaborator.peer_id)
7052 });
7053
7054 if let Some(follow_peer_id) = follow_peer_id {
7055 workspace.follow(follow_peer_id, window, cx);
7056 }
7057 }
7058 })?;
7059
7060 anyhow::Ok(())
7061 })
7062}
7063
7064pub fn reload(reload: &Reload, cx: &mut App) {
7065 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
7066 let mut workspace_windows = cx
7067 .windows()
7068 .into_iter()
7069 .filter_map(|window| window.downcast::<Workspace>())
7070 .collect::<Vec<_>>();
7071
7072 // If multiple windows have unsaved changes, and need a save prompt,
7073 // prompt in the active window before switching to a different window.
7074 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
7075
7076 let mut prompt = None;
7077 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
7078 prompt = window
7079 .update(cx, |_, window, cx| {
7080 window.prompt(
7081 PromptLevel::Info,
7082 "Are you sure you want to restart?",
7083 None,
7084 &["Restart", "Cancel"],
7085 cx,
7086 )
7087 })
7088 .ok();
7089 }
7090
7091 let binary_path = reload.binary_path.clone();
7092 cx.spawn(async move |cx| {
7093 if let Some(prompt) = prompt {
7094 let answer = prompt.await?;
7095 if answer != 0 {
7096 return Ok(());
7097 }
7098 }
7099
7100 // If the user cancels any save prompt, then keep the app open.
7101 for window in workspace_windows {
7102 if let Ok(should_close) = window.update(cx, |workspace, window, cx| {
7103 workspace.prepare_to_close(CloseIntent::Quit, window, cx)
7104 }) {
7105 if !should_close.await? {
7106 return Ok(());
7107 }
7108 }
7109 }
7110
7111 cx.update(|cx| cx.restart(binary_path))
7112 })
7113 .detach_and_log_err(cx);
7114}
7115
7116fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
7117 let mut parts = value.split(',');
7118 let x: usize = parts.next()?.parse().ok()?;
7119 let y: usize = parts.next()?.parse().ok()?;
7120 Some(point(px(x as f32), px(y as f32)))
7121}
7122
7123fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
7124 let mut parts = value.split(',');
7125 let width: usize = parts.next()?.parse().ok()?;
7126 let height: usize = parts.next()?.parse().ok()?;
7127 Some(size(px(width as f32), px(height as f32)))
7128}
7129
7130pub fn client_side_decorations(
7131 element: impl IntoElement,
7132 window: &mut Window,
7133 cx: &mut App,
7134) -> Stateful<Div> {
7135 const BORDER_SIZE: Pixels = px(1.0);
7136 let decorations = window.window_decorations();
7137
7138 if matches!(decorations, Decorations::Client { .. }) {
7139 window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
7140 }
7141
7142 struct GlobalResizeEdge(ResizeEdge);
7143 impl Global for GlobalResizeEdge {}
7144
7145 div()
7146 .id("window-backdrop")
7147 .bg(transparent_black())
7148 .map(|div| match decorations {
7149 Decorations::Server => div,
7150 Decorations::Client { tiling, .. } => div
7151 .when(!(tiling.top || tiling.right), |div| {
7152 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7153 })
7154 .when(!(tiling.top || tiling.left), |div| {
7155 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7156 })
7157 .when(!(tiling.bottom || tiling.right), |div| {
7158 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7159 })
7160 .when(!(tiling.bottom || tiling.left), |div| {
7161 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7162 })
7163 .when(!tiling.top, |div| {
7164 div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
7165 })
7166 .when(!tiling.bottom, |div| {
7167 div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
7168 })
7169 .when(!tiling.left, |div| {
7170 div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
7171 })
7172 .when(!tiling.right, |div| {
7173 div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
7174 })
7175 .on_mouse_move(move |e, window, cx| {
7176 let size = window.window_bounds().get_bounds().size;
7177 let pos = e.position;
7178
7179 let new_edge =
7180 resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
7181
7182 let edge = cx.try_global::<GlobalResizeEdge>();
7183 if new_edge != edge.map(|edge| edge.0) {
7184 window
7185 .window_handle()
7186 .update(cx, |workspace, _, cx| {
7187 cx.notify(workspace.entity_id());
7188 })
7189 .ok();
7190 }
7191 })
7192 .on_mouse_down(MouseButton::Left, move |e, window, _| {
7193 let size = window.window_bounds().get_bounds().size;
7194 let pos = e.position;
7195
7196 let edge = match resize_edge(
7197 pos,
7198 theme::CLIENT_SIDE_DECORATION_SHADOW,
7199 size,
7200 tiling,
7201 ) {
7202 Some(value) => value,
7203 None => return,
7204 };
7205
7206 window.start_window_resize(edge);
7207 }),
7208 })
7209 .size_full()
7210 .child(
7211 div()
7212 .cursor(CursorStyle::Arrow)
7213 .map(|div| match decorations {
7214 Decorations::Server => div,
7215 Decorations::Client { tiling } => div
7216 .border_color(cx.theme().colors().border)
7217 .when(!(tiling.top || tiling.right), |div| {
7218 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7219 })
7220 .when(!(tiling.top || tiling.left), |div| {
7221 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7222 })
7223 .when(!(tiling.bottom || tiling.right), |div| {
7224 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7225 })
7226 .when(!(tiling.bottom || tiling.left), |div| {
7227 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7228 })
7229 .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
7230 .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
7231 .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
7232 .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
7233 .when(!tiling.is_tiled(), |div| {
7234 div.shadow(smallvec::smallvec![gpui::BoxShadow {
7235 color: Hsla {
7236 h: 0.,
7237 s: 0.,
7238 l: 0.,
7239 a: 0.4,
7240 },
7241 blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
7242 spread_radius: px(0.),
7243 offset: point(px(0.0), px(0.0)),
7244 }])
7245 }),
7246 })
7247 .on_mouse_move(|_e, _, cx| {
7248 cx.stop_propagation();
7249 })
7250 .size_full()
7251 .child(element),
7252 )
7253 .map(|div| match decorations {
7254 Decorations::Server => div,
7255 Decorations::Client { tiling, .. } => div.child(
7256 canvas(
7257 |_bounds, window, _| {
7258 window.insert_hitbox(
7259 Bounds::new(
7260 point(px(0.0), px(0.0)),
7261 window.window_bounds().get_bounds().size,
7262 ),
7263 false,
7264 )
7265 },
7266 move |_bounds, hitbox, window, cx| {
7267 let mouse = window.mouse_position();
7268 let size = window.window_bounds().get_bounds().size;
7269 let Some(edge) =
7270 resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
7271 else {
7272 return;
7273 };
7274 cx.set_global(GlobalResizeEdge(edge));
7275 window.set_cursor_style(
7276 match edge {
7277 ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
7278 ResizeEdge::Left | ResizeEdge::Right => {
7279 CursorStyle::ResizeLeftRight
7280 }
7281 ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
7282 CursorStyle::ResizeUpLeftDownRight
7283 }
7284 ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
7285 CursorStyle::ResizeUpRightDownLeft
7286 }
7287 },
7288 Some(&hitbox),
7289 );
7290 },
7291 )
7292 .size_full()
7293 .absolute(),
7294 ),
7295 })
7296}
7297
7298fn resize_edge(
7299 pos: Point<Pixels>,
7300 shadow_size: Pixels,
7301 window_size: Size<Pixels>,
7302 tiling: Tiling,
7303) -> Option<ResizeEdge> {
7304 let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
7305 if bounds.contains(&pos) {
7306 return None;
7307 }
7308
7309 let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
7310 let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
7311 if !tiling.top && top_left_bounds.contains(&pos) {
7312 return Some(ResizeEdge::TopLeft);
7313 }
7314
7315 let top_right_bounds = Bounds::new(
7316 Point::new(window_size.width - corner_size.width, px(0.)),
7317 corner_size,
7318 );
7319 if !tiling.top && top_right_bounds.contains(&pos) {
7320 return Some(ResizeEdge::TopRight);
7321 }
7322
7323 let bottom_left_bounds = Bounds::new(
7324 Point::new(px(0.), window_size.height - corner_size.height),
7325 corner_size,
7326 );
7327 if !tiling.bottom && bottom_left_bounds.contains(&pos) {
7328 return Some(ResizeEdge::BottomLeft);
7329 }
7330
7331 let bottom_right_bounds = Bounds::new(
7332 Point::new(
7333 window_size.width - corner_size.width,
7334 window_size.height - corner_size.height,
7335 ),
7336 corner_size,
7337 );
7338 if !tiling.bottom && bottom_right_bounds.contains(&pos) {
7339 return Some(ResizeEdge::BottomRight);
7340 }
7341
7342 if !tiling.top && pos.y < shadow_size {
7343 Some(ResizeEdge::Top)
7344 } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
7345 Some(ResizeEdge::Bottom)
7346 } else if !tiling.left && pos.x < shadow_size {
7347 Some(ResizeEdge::Left)
7348 } else if !tiling.right && pos.x > window_size.width - shadow_size {
7349 Some(ResizeEdge::Right)
7350 } else {
7351 None
7352 }
7353}
7354
7355fn join_pane_into_active(
7356 active_pane: &Entity<Pane>,
7357 pane: &Entity<Pane>,
7358 window: &mut Window,
7359 cx: &mut App,
7360) {
7361 if pane == active_pane {
7362 return;
7363 } else if pane.read(cx).items_len() == 0 {
7364 pane.update(cx, |_, cx| {
7365 cx.emit(pane::Event::Remove {
7366 focus_on_pane: None,
7367 });
7368 })
7369 } else {
7370 move_all_items(pane, active_pane, window, cx);
7371 }
7372}
7373
7374fn move_all_items(
7375 from_pane: &Entity<Pane>,
7376 to_pane: &Entity<Pane>,
7377 window: &mut Window,
7378 cx: &mut App,
7379) {
7380 let destination_is_different = from_pane != to_pane;
7381 let mut moved_items = 0;
7382 for (item_ix, item_handle) in from_pane
7383 .read(cx)
7384 .items()
7385 .enumerate()
7386 .map(|(ix, item)| (ix, item.clone()))
7387 .collect::<Vec<_>>()
7388 {
7389 let ix = item_ix - moved_items;
7390 if destination_is_different {
7391 // Close item from previous pane
7392 from_pane.update(cx, |source, cx| {
7393 source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), window, cx);
7394 });
7395 moved_items += 1;
7396 }
7397
7398 // This automatically removes duplicate items in the pane
7399 to_pane.update(cx, |destination, cx| {
7400 destination.add_item(item_handle, true, true, None, window, cx);
7401 window.focus(&destination.focus_handle(cx))
7402 });
7403 }
7404}
7405
7406pub fn move_item(
7407 source: &Entity<Pane>,
7408 destination: &Entity<Pane>,
7409 item_id_to_move: EntityId,
7410 destination_index: usize,
7411 window: &mut Window,
7412 cx: &mut App,
7413) {
7414 let Some((item_ix, item_handle)) = source
7415 .read(cx)
7416 .items()
7417 .enumerate()
7418 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
7419 .map(|(ix, item)| (ix, item.clone()))
7420 else {
7421 // Tab was closed during drag
7422 return;
7423 };
7424
7425 if source != destination {
7426 // Close item from previous pane
7427 source.update(cx, |source, cx| {
7428 source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), window, cx);
7429 });
7430 }
7431
7432 // This automatically removes duplicate items in the pane
7433 destination.update(cx, |destination, cx| {
7434 destination.add_item(item_handle, true, true, Some(destination_index), window, cx);
7435 window.focus(&destination.focus_handle(cx))
7436 });
7437}
7438
7439pub fn move_active_item(
7440 source: &Entity<Pane>,
7441 destination: &Entity<Pane>,
7442 focus_destination: bool,
7443 close_if_empty: bool,
7444 window: &mut Window,
7445 cx: &mut App,
7446) {
7447 if source == destination {
7448 return;
7449 }
7450 let Some(active_item) = source.read(cx).active_item() else {
7451 return;
7452 };
7453 source.update(cx, |source_pane, cx| {
7454 let item_id = active_item.item_id();
7455 source_pane.remove_item(item_id, false, close_if_empty, window, cx);
7456 destination.update(cx, |target_pane, cx| {
7457 target_pane.add_item(
7458 active_item,
7459 focus_destination,
7460 focus_destination,
7461 Some(target_pane.items_len()),
7462 window,
7463 cx,
7464 );
7465 });
7466 });
7467}
7468
7469#[derive(Debug)]
7470pub struct WorkspacePosition {
7471 pub window_bounds: Option<WindowBounds>,
7472 pub display: Option<Uuid>,
7473 pub centered_layout: bool,
7474}
7475
7476pub fn ssh_workspace_position_from_db(
7477 host: String,
7478 port: Option<u16>,
7479 user: Option<String>,
7480 paths_to_open: &[PathBuf],
7481 cx: &App,
7482) -> Task<Result<WorkspacePosition>> {
7483 let paths = paths_to_open
7484 .iter()
7485 .map(|path| path.to_string_lossy().to_string())
7486 .collect::<Vec<_>>();
7487
7488 cx.background_spawn(async move {
7489 let serialized_ssh_project = persistence::DB
7490 .get_or_create_ssh_project(host, port, paths, user)
7491 .await
7492 .context("fetching serialized ssh project")?;
7493 let serialized_workspace =
7494 persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
7495
7496 let (window_bounds, display) = if let Some(bounds) = window_bounds_env_override() {
7497 (Some(WindowBounds::Windowed(bounds)), None)
7498 } else {
7499 let restorable_bounds = serialized_workspace
7500 .as_ref()
7501 .and_then(|workspace| Some((workspace.display?, workspace.window_bounds?)))
7502 .or_else(|| {
7503 let (display, window_bounds) = DB.last_window().log_err()?;
7504 Some((display?, window_bounds?))
7505 });
7506
7507 if let Some((serialized_display, serialized_status)) = restorable_bounds {
7508 (Some(serialized_status.0), Some(serialized_display))
7509 } else {
7510 (None, None)
7511 }
7512 };
7513
7514 let centered_layout = serialized_workspace
7515 .as_ref()
7516 .map(|w| w.centered_layout)
7517 .unwrap_or(false);
7518
7519 Ok(WorkspacePosition {
7520 window_bounds,
7521 display,
7522 centered_layout,
7523 })
7524 })
7525}
7526
7527#[cfg(test)]
7528mod tests {
7529 use std::{cell::RefCell, rc::Rc};
7530
7531 use super::*;
7532 use crate::{
7533 dock::{PanelEvent, test::TestPanel},
7534 item::{
7535 ItemEvent,
7536 test::{TestItem, TestProjectItem},
7537 },
7538 };
7539 use fs::FakeFs;
7540 use gpui::{
7541 DismissEvent, Empty, EventEmitter, FocusHandle, Focusable, Render, TestAppContext,
7542 UpdateGlobal, VisualTestContext, px,
7543 };
7544 use project::{Project, ProjectEntryId};
7545 use serde_json::json;
7546 use settings::SettingsStore;
7547
7548 #[gpui::test]
7549 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
7550 init_test(cx);
7551
7552 let fs = FakeFs::new(cx.executor());
7553 let project = Project::test(fs, [], cx).await;
7554 let (workspace, cx) =
7555 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7556
7557 // Adding an item with no ambiguity renders the tab without detail.
7558 let item1 = cx.new(|cx| {
7559 let mut item = TestItem::new(cx);
7560 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
7561 item
7562 });
7563 workspace.update_in(cx, |workspace, window, cx| {
7564 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7565 });
7566 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
7567
7568 // Adding an item that creates ambiguity increases the level of detail on
7569 // both tabs.
7570 let item2 = cx.new_window_entity(|_window, cx| {
7571 let mut item = TestItem::new(cx);
7572 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
7573 item
7574 });
7575 workspace.update_in(cx, |workspace, window, cx| {
7576 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7577 });
7578 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
7579 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
7580
7581 // Adding an item that creates ambiguity increases the level of detail only
7582 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
7583 // we stop at the highest detail available.
7584 let item3 = cx.new(|cx| {
7585 let mut item = TestItem::new(cx);
7586 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
7587 item
7588 });
7589 workspace.update_in(cx, |workspace, window, cx| {
7590 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7591 });
7592 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
7593 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
7594 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
7595 }
7596
7597 #[gpui::test]
7598 async fn test_tracking_active_path(cx: &mut TestAppContext) {
7599 init_test(cx);
7600
7601 let fs = FakeFs::new(cx.executor());
7602 fs.insert_tree(
7603 "/root1",
7604 json!({
7605 "one.txt": "",
7606 "two.txt": "",
7607 }),
7608 )
7609 .await;
7610 fs.insert_tree(
7611 "/root2",
7612 json!({
7613 "three.txt": "",
7614 }),
7615 )
7616 .await;
7617
7618 let project = Project::test(fs, ["root1".as_ref()], cx).await;
7619 let (workspace, cx) =
7620 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7621 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7622 let worktree_id = project.update(cx, |project, cx| {
7623 project.worktrees(cx).next().unwrap().read(cx).id()
7624 });
7625
7626 let item1 = cx.new(|cx| {
7627 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
7628 });
7629 let item2 = cx.new(|cx| {
7630 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
7631 });
7632
7633 // Add an item to an empty pane
7634 workspace.update_in(cx, |workspace, window, cx| {
7635 workspace.add_item_to_active_pane(Box::new(item1), None, true, window, cx)
7636 });
7637 project.update(cx, |project, cx| {
7638 assert_eq!(
7639 project.active_entry(),
7640 project
7641 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
7642 .map(|e| e.id)
7643 );
7644 });
7645 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
7646
7647 // Add a second item to a non-empty pane
7648 workspace.update_in(cx, |workspace, window, cx| {
7649 workspace.add_item_to_active_pane(Box::new(item2), None, true, window, cx)
7650 });
7651 assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
7652 project.update(cx, |project, cx| {
7653 assert_eq!(
7654 project.active_entry(),
7655 project
7656 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
7657 .map(|e| e.id)
7658 );
7659 });
7660
7661 // Close the active item
7662 pane.update_in(cx, |pane, window, cx| {
7663 pane.close_active_item(&Default::default(), window, cx)
7664 .unwrap()
7665 })
7666 .await
7667 .unwrap();
7668 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
7669 project.update(cx, |project, cx| {
7670 assert_eq!(
7671 project.active_entry(),
7672 project
7673 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
7674 .map(|e| e.id)
7675 );
7676 });
7677
7678 // Add a project folder
7679 project
7680 .update(cx, |project, cx| {
7681 project.find_or_create_worktree("root2", true, cx)
7682 })
7683 .await
7684 .unwrap();
7685 assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
7686
7687 // Remove a project folder
7688 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
7689 assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
7690 }
7691
7692 #[gpui::test]
7693 async fn test_close_window(cx: &mut TestAppContext) {
7694 init_test(cx);
7695
7696 let fs = FakeFs::new(cx.executor());
7697 fs.insert_tree("/root", json!({ "one": "" })).await;
7698
7699 let project = Project::test(fs, ["root".as_ref()], cx).await;
7700 let (workspace, cx) =
7701 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7702
7703 // When there are no dirty items, there's nothing to do.
7704 let item1 = cx.new(TestItem::new);
7705 workspace.update_in(cx, |w, window, cx| {
7706 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx)
7707 });
7708 let task = workspace.update_in(cx, |w, window, cx| {
7709 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
7710 });
7711 assert!(task.await.unwrap());
7712
7713 // When there are dirty untitled items, prompt to save each one. If the user
7714 // cancels any prompt, then abort.
7715 let item2 = cx.new(|cx| TestItem::new(cx).with_dirty(true));
7716 let item3 = cx.new(|cx| {
7717 TestItem::new(cx)
7718 .with_dirty(true)
7719 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7720 });
7721 workspace.update_in(cx, |w, window, cx| {
7722 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7723 w.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7724 });
7725 let task = workspace.update_in(cx, |w, window, cx| {
7726 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
7727 });
7728 cx.executor().run_until_parked();
7729 cx.simulate_prompt_answer("Cancel"); // cancel save all
7730 cx.executor().run_until_parked();
7731 assert!(!cx.has_pending_prompt());
7732 assert!(!task.await.unwrap());
7733 }
7734
7735 #[gpui::test]
7736 async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
7737 init_test(cx);
7738
7739 // Register TestItem as a serializable item
7740 cx.update(|cx| {
7741 register_serializable_item::<TestItem>(cx);
7742 });
7743
7744 let fs = FakeFs::new(cx.executor());
7745 fs.insert_tree("/root", json!({ "one": "" })).await;
7746
7747 let project = Project::test(fs, ["root".as_ref()], cx).await;
7748 let (workspace, cx) =
7749 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7750
7751 // When there are dirty untitled items, but they can serialize, then there is no prompt.
7752 let item1 = cx.new(|cx| {
7753 TestItem::new(cx)
7754 .with_dirty(true)
7755 .with_serialize(|| Some(Task::ready(Ok(()))))
7756 });
7757 let item2 = cx.new(|cx| {
7758 TestItem::new(cx)
7759 .with_dirty(true)
7760 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7761 .with_serialize(|| Some(Task::ready(Ok(()))))
7762 });
7763 workspace.update_in(cx, |w, window, cx| {
7764 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7765 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7766 });
7767 let task = workspace.update_in(cx, |w, window, cx| {
7768 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
7769 });
7770 assert!(task.await.unwrap());
7771 }
7772
7773 #[gpui::test]
7774 async fn test_close_pane_items(cx: &mut TestAppContext) {
7775 init_test(cx);
7776
7777 let fs = FakeFs::new(cx.executor());
7778
7779 let project = Project::test(fs, None, cx).await;
7780 let (workspace, cx) =
7781 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7782
7783 let item1 = cx.new(|cx| {
7784 TestItem::new(cx)
7785 .with_dirty(true)
7786 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7787 });
7788 let item2 = cx.new(|cx| {
7789 TestItem::new(cx)
7790 .with_dirty(true)
7791 .with_conflict(true)
7792 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7793 });
7794 let item3 = cx.new(|cx| {
7795 TestItem::new(cx)
7796 .with_dirty(true)
7797 .with_conflict(true)
7798 .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
7799 });
7800 let item4 = cx.new(|cx| {
7801 TestItem::new(cx).with_dirty(true).with_project_items(&[{
7802 let project_item = TestProjectItem::new_untitled(cx);
7803 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
7804 project_item
7805 }])
7806 });
7807 let pane = workspace.update_in(cx, |workspace, window, cx| {
7808 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7809 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7810 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7811 workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, window, cx);
7812 workspace.active_pane().clone()
7813 });
7814
7815 let close_items = pane.update_in(cx, |pane, window, cx| {
7816 pane.activate_item(1, true, true, window, cx);
7817 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7818 let item1_id = item1.item_id();
7819 let item3_id = item3.item_id();
7820 let item4_id = item4.item_id();
7821 pane.close_items(window, cx, SaveIntent::Close, move |id| {
7822 [item1_id, item3_id, item4_id].contains(&id)
7823 })
7824 });
7825 cx.executor().run_until_parked();
7826
7827 assert!(cx.has_pending_prompt());
7828 cx.simulate_prompt_answer("Save all");
7829
7830 cx.executor().run_until_parked();
7831
7832 // Item 1 is saved. There's a prompt to save item 3.
7833 pane.update(cx, |pane, cx| {
7834 assert_eq!(item1.read(cx).save_count, 1);
7835 assert_eq!(item1.read(cx).save_as_count, 0);
7836 assert_eq!(item1.read(cx).reload_count, 0);
7837 assert_eq!(pane.items_len(), 3);
7838 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
7839 });
7840 assert!(cx.has_pending_prompt());
7841
7842 // Cancel saving item 3.
7843 cx.simulate_prompt_answer("Discard");
7844 cx.executor().run_until_parked();
7845
7846 // Item 3 is reloaded. There's a prompt to save item 4.
7847 pane.update(cx, |pane, cx| {
7848 assert_eq!(item3.read(cx).save_count, 0);
7849 assert_eq!(item3.read(cx).save_as_count, 0);
7850 assert_eq!(item3.read(cx).reload_count, 1);
7851 assert_eq!(pane.items_len(), 2);
7852 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
7853 });
7854
7855 // There's a prompt for a path for item 4.
7856 cx.simulate_new_path_selection(|_| Some(Default::default()));
7857 close_items.await.unwrap();
7858
7859 // The requested items are closed.
7860 pane.update(cx, |pane, cx| {
7861 assert_eq!(item4.read(cx).save_count, 0);
7862 assert_eq!(item4.read(cx).save_as_count, 1);
7863 assert_eq!(item4.read(cx).reload_count, 0);
7864 assert_eq!(pane.items_len(), 1);
7865 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7866 });
7867 }
7868
7869 #[gpui::test]
7870 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
7871 init_test(cx);
7872
7873 let fs = FakeFs::new(cx.executor());
7874 let project = Project::test(fs, [], cx).await;
7875 let (workspace, cx) =
7876 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7877
7878 // Create several workspace items with single project entries, and two
7879 // workspace items with multiple project entries.
7880 let single_entry_items = (0..=4)
7881 .map(|project_entry_id| {
7882 cx.new(|cx| {
7883 TestItem::new(cx)
7884 .with_dirty(true)
7885 .with_project_items(&[dirty_project_item(
7886 project_entry_id,
7887 &format!("{project_entry_id}.txt"),
7888 cx,
7889 )])
7890 })
7891 })
7892 .collect::<Vec<_>>();
7893 let item_2_3 = cx.new(|cx| {
7894 TestItem::new(cx)
7895 .with_dirty(true)
7896 .with_singleton(false)
7897 .with_project_items(&[
7898 single_entry_items[2].read(cx).project_items[0].clone(),
7899 single_entry_items[3].read(cx).project_items[0].clone(),
7900 ])
7901 });
7902 let item_3_4 = cx.new(|cx| {
7903 TestItem::new(cx)
7904 .with_dirty(true)
7905 .with_singleton(false)
7906 .with_project_items(&[
7907 single_entry_items[3].read(cx).project_items[0].clone(),
7908 single_entry_items[4].read(cx).project_items[0].clone(),
7909 ])
7910 });
7911
7912 // Create two panes that contain the following project entries:
7913 // left pane:
7914 // multi-entry items: (2, 3)
7915 // single-entry items: 0, 2, 3, 4
7916 // right pane:
7917 // single-entry items: 4, 1
7918 // multi-entry items: (3, 4)
7919 let (left_pane, right_pane) = workspace.update_in(cx, |workspace, window, cx| {
7920 let left_pane = workspace.active_pane().clone();
7921 workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, window, cx);
7922 workspace.add_item_to_active_pane(
7923 single_entry_items[0].boxed_clone(),
7924 None,
7925 true,
7926 window,
7927 cx,
7928 );
7929 workspace.add_item_to_active_pane(
7930 single_entry_items[2].boxed_clone(),
7931 None,
7932 true,
7933 window,
7934 cx,
7935 );
7936 workspace.add_item_to_active_pane(
7937 single_entry_items[3].boxed_clone(),
7938 None,
7939 true,
7940 window,
7941 cx,
7942 );
7943 workspace.add_item_to_active_pane(
7944 single_entry_items[4].boxed_clone(),
7945 None,
7946 true,
7947 window,
7948 cx,
7949 );
7950
7951 let right_pane = workspace
7952 .split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx)
7953 .unwrap();
7954
7955 right_pane.update(cx, |pane, cx| {
7956 pane.add_item(
7957 single_entry_items[1].boxed_clone(),
7958 true,
7959 true,
7960 None,
7961 window,
7962 cx,
7963 );
7964 pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx);
7965 });
7966
7967 (left_pane, right_pane)
7968 });
7969
7970 cx.focus(&right_pane);
7971
7972 let mut close = right_pane.update_in(cx, |pane, window, cx| {
7973 pane.close_all_items(&CloseAllItems::default(), window, cx)
7974 .unwrap()
7975 });
7976 cx.executor().run_until_parked();
7977
7978 let msg = cx.pending_prompt().unwrap().0;
7979 assert!(msg.contains("1.txt"));
7980 assert!(!msg.contains("2.txt"));
7981 assert!(!msg.contains("3.txt"));
7982 assert!(!msg.contains("4.txt"));
7983
7984 cx.simulate_prompt_answer("Cancel");
7985 close.await.unwrap();
7986
7987 left_pane
7988 .update_in(cx, |left_pane, window, cx| {
7989 left_pane.close_item_by_id(
7990 single_entry_items[3].entity_id(),
7991 SaveIntent::Skip,
7992 window,
7993 cx,
7994 )
7995 })
7996 .await
7997 .unwrap();
7998
7999 close = right_pane.update_in(cx, |pane, window, cx| {
8000 pane.close_all_items(&CloseAllItems::default(), window, cx)
8001 .unwrap()
8002 });
8003 cx.executor().run_until_parked();
8004
8005 let details = cx.pending_prompt().unwrap().1;
8006 assert!(details.contains("1.txt"));
8007 assert!(!details.contains("2.txt"));
8008 assert!(details.contains("3.txt"));
8009 // ideally this assertion could be made, but today we can only
8010 // save whole items not project items, so the orphaned item 3 causes
8011 // 4 to be saved too.
8012 // assert!(!details.contains("4.txt"));
8013
8014 cx.simulate_prompt_answer("Save all");
8015
8016 cx.executor().run_until_parked();
8017 close.await.unwrap();
8018 right_pane.update(cx, |pane, _| {
8019 assert_eq!(pane.items_len(), 0);
8020 });
8021 }
8022
8023 #[gpui::test]
8024 async fn test_autosave(cx: &mut gpui::TestAppContext) {
8025 init_test(cx);
8026
8027 let fs = FakeFs::new(cx.executor());
8028 let project = Project::test(fs, [], cx).await;
8029 let (workspace, cx) =
8030 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8031 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8032
8033 let item = cx.new(|cx| {
8034 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
8035 });
8036 let item_id = item.entity_id();
8037 workspace.update_in(cx, |workspace, window, cx| {
8038 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
8039 });
8040
8041 // Autosave on window change.
8042 item.update(cx, |item, cx| {
8043 SettingsStore::update_global(cx, |settings, cx| {
8044 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
8045 settings.autosave = Some(AutosaveSetting::OnWindowChange);
8046 })
8047 });
8048 item.is_dirty = true;
8049 });
8050
8051 // Deactivating the window saves the file.
8052 cx.deactivate_window();
8053 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
8054
8055 // Re-activating the window doesn't save the file.
8056 cx.update(|window, _| window.activate_window());
8057 cx.executor().run_until_parked();
8058 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
8059
8060 // Autosave on focus change.
8061 item.update_in(cx, |item, window, cx| {
8062 cx.focus_self(window);
8063 SettingsStore::update_global(cx, |settings, cx| {
8064 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
8065 settings.autosave = Some(AutosaveSetting::OnFocusChange);
8066 })
8067 });
8068 item.is_dirty = true;
8069 });
8070
8071 // Blurring the item saves the file.
8072 item.update_in(cx, |_, window, _| window.blur());
8073 cx.executor().run_until_parked();
8074 item.update(cx, |item, _| assert_eq!(item.save_count, 2));
8075
8076 // Deactivating the window still saves the file.
8077 item.update_in(cx, |item, window, cx| {
8078 cx.focus_self(window);
8079 item.is_dirty = true;
8080 });
8081 cx.deactivate_window();
8082 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
8083
8084 // Autosave after delay.
8085 item.update(cx, |item, cx| {
8086 SettingsStore::update_global(cx, |settings, cx| {
8087 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
8088 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
8089 })
8090 });
8091 item.is_dirty = true;
8092 cx.emit(ItemEvent::Edit);
8093 });
8094
8095 // Delay hasn't fully expired, so the file is still dirty and unsaved.
8096 cx.executor().advance_clock(Duration::from_millis(250));
8097 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
8098
8099 // After delay expires, the file is saved.
8100 cx.executor().advance_clock(Duration::from_millis(250));
8101 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
8102
8103 // Autosave on focus change, ensuring closing the tab counts as such.
8104 item.update(cx, |item, cx| {
8105 SettingsStore::update_global(cx, |settings, cx| {
8106 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
8107 settings.autosave = Some(AutosaveSetting::OnFocusChange);
8108 })
8109 });
8110 item.is_dirty = true;
8111 for project_item in &mut item.project_items {
8112 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
8113 }
8114 });
8115
8116 pane.update_in(cx, |pane, window, cx| {
8117 pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
8118 })
8119 .await
8120 .unwrap();
8121 assert!(!cx.has_pending_prompt());
8122 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
8123
8124 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
8125 workspace.update_in(cx, |workspace, window, cx| {
8126 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
8127 });
8128 item.update_in(cx, |item, window, cx| {
8129 item.project_items[0].update(cx, |item, _| {
8130 item.entry_id = None;
8131 });
8132 item.is_dirty = true;
8133 window.blur();
8134 });
8135 cx.run_until_parked();
8136 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
8137
8138 // Ensure autosave is prevented for deleted files also when closing the buffer.
8139 let _close_items = pane.update_in(cx, |pane, window, cx| {
8140 pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
8141 });
8142 cx.run_until_parked();
8143 assert!(cx.has_pending_prompt());
8144 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
8145 }
8146
8147 #[gpui::test]
8148 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
8149 init_test(cx);
8150
8151 let fs = FakeFs::new(cx.executor());
8152
8153 let project = Project::test(fs, [], cx).await;
8154 let (workspace, cx) =
8155 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8156
8157 let item = cx.new(|cx| {
8158 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
8159 });
8160 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8161 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
8162 let toolbar_notify_count = Rc::new(RefCell::new(0));
8163
8164 workspace.update_in(cx, |workspace, window, cx| {
8165 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
8166 let toolbar_notification_count = toolbar_notify_count.clone();
8167 cx.observe_in(&toolbar, window, move |_, _, _, _| {
8168 *toolbar_notification_count.borrow_mut() += 1
8169 })
8170 .detach();
8171 });
8172
8173 pane.update(cx, |pane, _| {
8174 assert!(!pane.can_navigate_backward());
8175 assert!(!pane.can_navigate_forward());
8176 });
8177
8178 item.update_in(cx, |item, _, cx| {
8179 item.set_state("one".to_string(), cx);
8180 });
8181
8182 // Toolbar must be notified to re-render the navigation buttons
8183 assert_eq!(*toolbar_notify_count.borrow(), 1);
8184
8185 pane.update(cx, |pane, _| {
8186 assert!(pane.can_navigate_backward());
8187 assert!(!pane.can_navigate_forward());
8188 });
8189
8190 workspace
8191 .update_in(cx, |workspace, window, cx| {
8192 workspace.go_back(pane.downgrade(), window, cx)
8193 })
8194 .await
8195 .unwrap();
8196
8197 assert_eq!(*toolbar_notify_count.borrow(), 2);
8198 pane.update(cx, |pane, _| {
8199 assert!(!pane.can_navigate_backward());
8200 assert!(pane.can_navigate_forward());
8201 });
8202 }
8203
8204 #[gpui::test]
8205 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
8206 init_test(cx);
8207 let fs = FakeFs::new(cx.executor());
8208
8209 let project = Project::test(fs, [], cx).await;
8210 let (workspace, cx) =
8211 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8212
8213 let panel = workspace.update_in(cx, |workspace, window, cx| {
8214 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
8215 workspace.add_panel(panel.clone(), window, cx);
8216
8217 workspace
8218 .right_dock()
8219 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
8220
8221 panel
8222 });
8223
8224 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8225 pane.update_in(cx, |pane, window, cx| {
8226 let item = cx.new(TestItem::new);
8227 pane.add_item(Box::new(item), true, true, None, window, cx);
8228 });
8229
8230 // Transfer focus from center to panel
8231 workspace.update_in(cx, |workspace, window, cx| {
8232 workspace.toggle_panel_focus::<TestPanel>(window, cx);
8233 });
8234
8235 workspace.update_in(cx, |workspace, window, cx| {
8236 assert!(workspace.right_dock().read(cx).is_open());
8237 assert!(!panel.is_zoomed(window, cx));
8238 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8239 });
8240
8241 // Transfer focus from panel to center
8242 workspace.update_in(cx, |workspace, window, cx| {
8243 workspace.toggle_panel_focus::<TestPanel>(window, cx);
8244 });
8245
8246 workspace.update_in(cx, |workspace, window, cx| {
8247 assert!(workspace.right_dock().read(cx).is_open());
8248 assert!(!panel.is_zoomed(window, cx));
8249 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8250 });
8251
8252 // Close the dock
8253 workspace.update_in(cx, |workspace, window, cx| {
8254 workspace.toggle_dock(DockPosition::Right, window, cx);
8255 });
8256
8257 workspace.update_in(cx, |workspace, window, cx| {
8258 assert!(!workspace.right_dock().read(cx).is_open());
8259 assert!(!panel.is_zoomed(window, cx));
8260 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8261 });
8262
8263 // Open the dock
8264 workspace.update_in(cx, |workspace, window, cx| {
8265 workspace.toggle_dock(DockPosition::Right, window, cx);
8266 });
8267
8268 workspace.update_in(cx, |workspace, window, cx| {
8269 assert!(workspace.right_dock().read(cx).is_open());
8270 assert!(!panel.is_zoomed(window, cx));
8271 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8272 });
8273
8274 // Focus and zoom panel
8275 panel.update_in(cx, |panel, window, cx| {
8276 cx.focus_self(window);
8277 panel.set_zoomed(true, window, cx)
8278 });
8279
8280 workspace.update_in(cx, |workspace, window, cx| {
8281 assert!(workspace.right_dock().read(cx).is_open());
8282 assert!(panel.is_zoomed(window, cx));
8283 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8284 });
8285
8286 // Transfer focus to the center closes the dock
8287 workspace.update_in(cx, |workspace, window, cx| {
8288 workspace.toggle_panel_focus::<TestPanel>(window, cx);
8289 });
8290
8291 workspace.update_in(cx, |workspace, window, cx| {
8292 assert!(!workspace.right_dock().read(cx).is_open());
8293 assert!(panel.is_zoomed(window, cx));
8294 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8295 });
8296
8297 // Transferring focus back to the panel keeps it zoomed
8298 workspace.update_in(cx, |workspace, window, cx| {
8299 workspace.toggle_panel_focus::<TestPanel>(window, cx);
8300 });
8301
8302 workspace.update_in(cx, |workspace, window, cx| {
8303 assert!(workspace.right_dock().read(cx).is_open());
8304 assert!(panel.is_zoomed(window, cx));
8305 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8306 });
8307
8308 // Close the dock while it is zoomed
8309 workspace.update_in(cx, |workspace, window, cx| {
8310 workspace.toggle_dock(DockPosition::Right, window, cx)
8311 });
8312
8313 workspace.update_in(cx, |workspace, window, cx| {
8314 assert!(!workspace.right_dock().read(cx).is_open());
8315 assert!(panel.is_zoomed(window, cx));
8316 assert!(workspace.zoomed.is_none());
8317 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8318 });
8319
8320 // Opening the dock, when it's zoomed, retains focus
8321 workspace.update_in(cx, |workspace, window, cx| {
8322 workspace.toggle_dock(DockPosition::Right, window, cx)
8323 });
8324
8325 workspace.update_in(cx, |workspace, window, cx| {
8326 assert!(workspace.right_dock().read(cx).is_open());
8327 assert!(panel.is_zoomed(window, cx));
8328 assert!(workspace.zoomed.is_some());
8329 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8330 });
8331
8332 // Unzoom and close the panel, zoom the active pane.
8333 panel.update_in(cx, |panel, window, cx| panel.set_zoomed(false, window, cx));
8334 workspace.update_in(cx, |workspace, window, cx| {
8335 workspace.toggle_dock(DockPosition::Right, window, cx)
8336 });
8337 pane.update_in(cx, |pane, window, cx| {
8338 pane.toggle_zoom(&Default::default(), window, cx)
8339 });
8340
8341 // Opening a dock unzooms the pane.
8342 workspace.update_in(cx, |workspace, window, cx| {
8343 workspace.toggle_dock(DockPosition::Right, window, cx)
8344 });
8345 workspace.update_in(cx, |workspace, window, cx| {
8346 let pane = pane.read(cx);
8347 assert!(!pane.is_zoomed());
8348 assert!(!pane.focus_handle(cx).is_focused(window));
8349 assert!(workspace.right_dock().read(cx).is_open());
8350 assert!(workspace.zoomed.is_none());
8351 });
8352 }
8353
8354 #[gpui::test]
8355 async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
8356 init_test(cx);
8357
8358 let fs = FakeFs::new(cx.executor());
8359
8360 let project = Project::test(fs, None, cx).await;
8361 let (workspace, cx) =
8362 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8363
8364 // Let's arrange the panes like this:
8365 //
8366 // +-----------------------+
8367 // | top |
8368 // +------+--------+-------+
8369 // | left | center | right |
8370 // +------+--------+-------+
8371 // | bottom |
8372 // +-----------------------+
8373
8374 let top_item = cx.new(|cx| {
8375 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
8376 });
8377 let bottom_item = cx.new(|cx| {
8378 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
8379 });
8380 let left_item = cx.new(|cx| {
8381 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
8382 });
8383 let right_item = cx.new(|cx| {
8384 TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
8385 });
8386 let center_item = cx.new(|cx| {
8387 TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
8388 });
8389
8390 let top_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8391 let top_pane_id = workspace.active_pane().entity_id();
8392 workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, window, cx);
8393 workspace.split_pane(
8394 workspace.active_pane().clone(),
8395 SplitDirection::Down,
8396 window,
8397 cx,
8398 );
8399 top_pane_id
8400 });
8401 let bottom_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8402 let bottom_pane_id = workspace.active_pane().entity_id();
8403 workspace.add_item_to_active_pane(
8404 Box::new(bottom_item.clone()),
8405 None,
8406 false,
8407 window,
8408 cx,
8409 );
8410 workspace.split_pane(
8411 workspace.active_pane().clone(),
8412 SplitDirection::Up,
8413 window,
8414 cx,
8415 );
8416 bottom_pane_id
8417 });
8418 let left_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8419 let left_pane_id = workspace.active_pane().entity_id();
8420 workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, window, cx);
8421 workspace.split_pane(
8422 workspace.active_pane().clone(),
8423 SplitDirection::Right,
8424 window,
8425 cx,
8426 );
8427 left_pane_id
8428 });
8429 let right_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8430 let right_pane_id = workspace.active_pane().entity_id();
8431 workspace.add_item_to_active_pane(
8432 Box::new(right_item.clone()),
8433 None,
8434 false,
8435 window,
8436 cx,
8437 );
8438 workspace.split_pane(
8439 workspace.active_pane().clone(),
8440 SplitDirection::Left,
8441 window,
8442 cx,
8443 );
8444 right_pane_id
8445 });
8446 let center_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8447 let center_pane_id = workspace.active_pane().entity_id();
8448 workspace.add_item_to_active_pane(
8449 Box::new(center_item.clone()),
8450 None,
8451 false,
8452 window,
8453 cx,
8454 );
8455 center_pane_id
8456 });
8457 cx.executor().run_until_parked();
8458
8459 workspace.update_in(cx, |workspace, window, cx| {
8460 assert_eq!(center_pane_id, workspace.active_pane().entity_id());
8461
8462 // Join into next from center pane into right
8463 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
8464 });
8465
8466 workspace.update_in(cx, |workspace, window, cx| {
8467 let active_pane = workspace.active_pane();
8468 assert_eq!(right_pane_id, active_pane.entity_id());
8469 assert_eq!(2, active_pane.read(cx).items_len());
8470 let item_ids_in_pane =
8471 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
8472 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
8473 assert!(item_ids_in_pane.contains(&right_item.item_id()));
8474
8475 // Join into next from right pane into bottom
8476 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
8477 });
8478
8479 workspace.update_in(cx, |workspace, window, cx| {
8480 let active_pane = workspace.active_pane();
8481 assert_eq!(bottom_pane_id, active_pane.entity_id());
8482 assert_eq!(3, active_pane.read(cx).items_len());
8483 let item_ids_in_pane =
8484 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
8485 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
8486 assert!(item_ids_in_pane.contains(&right_item.item_id()));
8487 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
8488
8489 // Join into next from bottom pane into left
8490 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
8491 });
8492
8493 workspace.update_in(cx, |workspace, window, cx| {
8494 let active_pane = workspace.active_pane();
8495 assert_eq!(left_pane_id, active_pane.entity_id());
8496 assert_eq!(4, active_pane.read(cx).items_len());
8497 let item_ids_in_pane =
8498 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
8499 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
8500 assert!(item_ids_in_pane.contains(&right_item.item_id()));
8501 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
8502 assert!(item_ids_in_pane.contains(&left_item.item_id()));
8503
8504 // Join into next from left pane into top
8505 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
8506 });
8507
8508 workspace.update_in(cx, |workspace, window, cx| {
8509 let active_pane = workspace.active_pane();
8510 assert_eq!(top_pane_id, active_pane.entity_id());
8511 assert_eq!(5, active_pane.read(cx).items_len());
8512 let item_ids_in_pane =
8513 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
8514 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
8515 assert!(item_ids_in_pane.contains(&right_item.item_id()));
8516 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
8517 assert!(item_ids_in_pane.contains(&left_item.item_id()));
8518 assert!(item_ids_in_pane.contains(&top_item.item_id()));
8519
8520 // Single pane left: no-op
8521 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx)
8522 });
8523
8524 workspace.update(cx, |workspace, _cx| {
8525 let active_pane = workspace.active_pane();
8526 assert_eq!(top_pane_id, active_pane.entity_id());
8527 });
8528 }
8529
8530 fn add_an_item_to_active_pane(
8531 cx: &mut VisualTestContext,
8532 workspace: &Entity<Workspace>,
8533 item_id: u64,
8534 ) -> Entity<TestItem> {
8535 let item = cx.new(|cx| {
8536 TestItem::new(cx).with_project_items(&[TestProjectItem::new(
8537 item_id,
8538 "item{item_id}.txt",
8539 cx,
8540 )])
8541 });
8542 workspace.update_in(cx, |workspace, window, cx| {
8543 workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx);
8544 });
8545 return item;
8546 }
8547
8548 fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> {
8549 return workspace.update_in(cx, |workspace, window, cx| {
8550 let new_pane = workspace.split_pane(
8551 workspace.active_pane().clone(),
8552 SplitDirection::Right,
8553 window,
8554 cx,
8555 );
8556 new_pane
8557 });
8558 }
8559
8560 #[gpui::test]
8561 async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
8562 init_test(cx);
8563 let fs = FakeFs::new(cx.executor());
8564 let project = Project::test(fs, None, cx).await;
8565 let (workspace, cx) =
8566 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8567
8568 add_an_item_to_active_pane(cx, &workspace, 1);
8569 split_pane(cx, &workspace);
8570 add_an_item_to_active_pane(cx, &workspace, 2);
8571 split_pane(cx, &workspace); // empty pane
8572 split_pane(cx, &workspace);
8573 let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
8574
8575 cx.executor().run_until_parked();
8576
8577 workspace.update(cx, |workspace, cx| {
8578 let num_panes = workspace.panes().len();
8579 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
8580 let active_item = workspace
8581 .active_pane()
8582 .read(cx)
8583 .active_item()
8584 .expect("item is in focus");
8585
8586 assert_eq!(num_panes, 4);
8587 assert_eq!(num_items_in_current_pane, 1);
8588 assert_eq!(active_item.item_id(), last_item.item_id());
8589 });
8590
8591 workspace.update_in(cx, |workspace, window, cx| {
8592 workspace.join_all_panes(window, cx);
8593 });
8594
8595 workspace.update(cx, |workspace, cx| {
8596 let num_panes = workspace.panes().len();
8597 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
8598 let active_item = workspace
8599 .active_pane()
8600 .read(cx)
8601 .active_item()
8602 .expect("item is in focus");
8603
8604 assert_eq!(num_panes, 1);
8605 assert_eq!(num_items_in_current_pane, 3);
8606 assert_eq!(active_item.item_id(), last_item.item_id());
8607 });
8608 }
8609 struct TestModal(FocusHandle);
8610
8611 impl TestModal {
8612 fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
8613 Self(cx.focus_handle())
8614 }
8615 }
8616
8617 impl EventEmitter<DismissEvent> for TestModal {}
8618
8619 impl Focusable for TestModal {
8620 fn focus_handle(&self, _cx: &App) -> FocusHandle {
8621 self.0.clone()
8622 }
8623 }
8624
8625 impl ModalView for TestModal {}
8626
8627 impl Render for TestModal {
8628 fn render(
8629 &mut self,
8630 _window: &mut Window,
8631 _cx: &mut Context<TestModal>,
8632 ) -> impl IntoElement {
8633 div().track_focus(&self.0)
8634 }
8635 }
8636
8637 #[gpui::test]
8638 async fn test_panels(cx: &mut gpui::TestAppContext) {
8639 init_test(cx);
8640 let fs = FakeFs::new(cx.executor());
8641
8642 let project = Project::test(fs, [], cx).await;
8643 let (workspace, cx) =
8644 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8645
8646 let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| {
8647 let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, cx));
8648 workspace.add_panel(panel_1.clone(), window, cx);
8649 workspace.toggle_dock(DockPosition::Left, window, cx);
8650 let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
8651 workspace.add_panel(panel_2.clone(), window, cx);
8652 workspace.toggle_dock(DockPosition::Right, window, cx);
8653
8654 let left_dock = workspace.left_dock();
8655 assert_eq!(
8656 left_dock.read(cx).visible_panel().unwrap().panel_id(),
8657 panel_1.panel_id()
8658 );
8659 assert_eq!(
8660 left_dock.read(cx).active_panel_size(window, cx).unwrap(),
8661 panel_1.size(window, cx)
8662 );
8663
8664 left_dock.update(cx, |left_dock, cx| {
8665 left_dock.resize_active_panel(Some(px(1337.)), window, cx)
8666 });
8667 assert_eq!(
8668 workspace
8669 .right_dock()
8670 .read(cx)
8671 .visible_panel()
8672 .unwrap()
8673 .panel_id(),
8674 panel_2.panel_id(),
8675 );
8676
8677 (panel_1, panel_2)
8678 });
8679
8680 // Move panel_1 to the right
8681 panel_1.update_in(cx, |panel_1, window, cx| {
8682 panel_1.set_position(DockPosition::Right, window, cx)
8683 });
8684
8685 workspace.update_in(cx, |workspace, window, cx| {
8686 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
8687 // Since it was the only panel on the left, the left dock should now be closed.
8688 assert!(!workspace.left_dock().read(cx).is_open());
8689 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
8690 let right_dock = workspace.right_dock();
8691 assert_eq!(
8692 right_dock.read(cx).visible_panel().unwrap().panel_id(),
8693 panel_1.panel_id()
8694 );
8695 assert_eq!(
8696 right_dock.read(cx).active_panel_size(window, cx).unwrap(),
8697 px(1337.)
8698 );
8699
8700 // Now we move panel_2 to the left
8701 panel_2.set_position(DockPosition::Left, window, cx);
8702 });
8703
8704 workspace.update(cx, |workspace, cx| {
8705 // Since panel_2 was not visible on the right, we don't open the left dock.
8706 assert!(!workspace.left_dock().read(cx).is_open());
8707 // And the right dock is unaffected in its displaying of panel_1
8708 assert!(workspace.right_dock().read(cx).is_open());
8709 assert_eq!(
8710 workspace
8711 .right_dock()
8712 .read(cx)
8713 .visible_panel()
8714 .unwrap()
8715 .panel_id(),
8716 panel_1.panel_id(),
8717 );
8718 });
8719
8720 // Move panel_1 back to the left
8721 panel_1.update_in(cx, |panel_1, window, cx| {
8722 panel_1.set_position(DockPosition::Left, window, cx)
8723 });
8724
8725 workspace.update_in(cx, |workspace, window, cx| {
8726 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
8727 let left_dock = workspace.left_dock();
8728 assert!(left_dock.read(cx).is_open());
8729 assert_eq!(
8730 left_dock.read(cx).visible_panel().unwrap().panel_id(),
8731 panel_1.panel_id()
8732 );
8733 assert_eq!(
8734 left_dock.read(cx).active_panel_size(window, cx).unwrap(),
8735 px(1337.)
8736 );
8737 // And the right dock should be closed as it no longer has any panels.
8738 assert!(!workspace.right_dock().read(cx).is_open());
8739
8740 // Now we move panel_1 to the bottom
8741 panel_1.set_position(DockPosition::Bottom, window, cx);
8742 });
8743
8744 workspace.update_in(cx, |workspace, window, cx| {
8745 // Since panel_1 was visible on the left, we close the left dock.
8746 assert!(!workspace.left_dock().read(cx).is_open());
8747 // The bottom dock is sized based on the panel's default size,
8748 // since the panel orientation changed from vertical to horizontal.
8749 let bottom_dock = workspace.bottom_dock();
8750 assert_eq!(
8751 bottom_dock.read(cx).active_panel_size(window, cx).unwrap(),
8752 panel_1.size(window, cx),
8753 );
8754 // Close bottom dock and move panel_1 back to the left.
8755 bottom_dock.update(cx, |bottom_dock, cx| {
8756 bottom_dock.set_open(false, window, cx)
8757 });
8758 panel_1.set_position(DockPosition::Left, window, cx);
8759 });
8760
8761 // Emit activated event on panel 1
8762 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
8763
8764 // Now the left dock is open and panel_1 is active and focused.
8765 workspace.update_in(cx, |workspace, window, cx| {
8766 let left_dock = workspace.left_dock();
8767 assert!(left_dock.read(cx).is_open());
8768 assert_eq!(
8769 left_dock.read(cx).visible_panel().unwrap().panel_id(),
8770 panel_1.panel_id(),
8771 );
8772 assert!(panel_1.focus_handle(cx).is_focused(window));
8773 });
8774
8775 // Emit closed event on panel 2, which is not active
8776 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
8777
8778 // Wo don't close the left dock, because panel_2 wasn't the active panel
8779 workspace.update(cx, |workspace, cx| {
8780 let left_dock = workspace.left_dock();
8781 assert!(left_dock.read(cx).is_open());
8782 assert_eq!(
8783 left_dock.read(cx).visible_panel().unwrap().panel_id(),
8784 panel_1.panel_id(),
8785 );
8786 });
8787
8788 // Emitting a ZoomIn event shows the panel as zoomed.
8789 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
8790 workspace.update(cx, |workspace, _| {
8791 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8792 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
8793 });
8794
8795 // Move panel to another dock while it is zoomed
8796 panel_1.update_in(cx, |panel, window, cx| {
8797 panel.set_position(DockPosition::Right, window, cx)
8798 });
8799 workspace.update(cx, |workspace, _| {
8800 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8801
8802 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8803 });
8804
8805 // This is a helper for getting a:
8806 // - valid focus on an element,
8807 // - that isn't a part of the panes and panels system of the Workspace,
8808 // - and doesn't trigger the 'on_focus_lost' API.
8809 let focus_other_view = {
8810 let workspace = workspace.clone();
8811 move |cx: &mut VisualTestContext| {
8812 workspace.update_in(cx, |workspace, window, cx| {
8813 if let Some(_) = workspace.active_modal::<TestModal>(cx) {
8814 workspace.toggle_modal(window, cx, TestModal::new);
8815 workspace.toggle_modal(window, cx, TestModal::new);
8816 } else {
8817 workspace.toggle_modal(window, cx, TestModal::new);
8818 }
8819 })
8820 }
8821 };
8822
8823 // If focus is transferred to another view that's not a panel or another pane, we still show
8824 // the panel as zoomed.
8825 focus_other_view(cx);
8826 workspace.update(cx, |workspace, _| {
8827 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8828 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8829 });
8830
8831 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
8832 workspace.update_in(cx, |_workspace, window, cx| {
8833 cx.focus_self(window);
8834 });
8835 workspace.update(cx, |workspace, _| {
8836 assert_eq!(workspace.zoomed, None);
8837 assert_eq!(workspace.zoomed_position, None);
8838 });
8839
8840 // If focus is transferred again to another view that's not a panel or a pane, we won't
8841 // show the panel as zoomed because it wasn't zoomed before.
8842 focus_other_view(cx);
8843 workspace.update(cx, |workspace, _| {
8844 assert_eq!(workspace.zoomed, None);
8845 assert_eq!(workspace.zoomed_position, None);
8846 });
8847
8848 // When the panel is activated, it is zoomed again.
8849 cx.dispatch_action(ToggleRightDock);
8850 workspace.update(cx, |workspace, _| {
8851 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8852 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8853 });
8854
8855 // Emitting a ZoomOut event unzooms the panel.
8856 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
8857 workspace.update(cx, |workspace, _| {
8858 assert_eq!(workspace.zoomed, None);
8859 assert_eq!(workspace.zoomed_position, None);
8860 });
8861
8862 // Emit closed event on panel 1, which is active
8863 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
8864
8865 // Now the left dock is closed, because panel_1 was the active panel
8866 workspace.update(cx, |workspace, cx| {
8867 let right_dock = workspace.right_dock();
8868 assert!(!right_dock.read(cx).is_open());
8869 });
8870 }
8871
8872 #[gpui::test]
8873 async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
8874 init_test(cx);
8875
8876 let fs = FakeFs::new(cx.background_executor.clone());
8877 let project = Project::test(fs, [], cx).await;
8878 let (workspace, cx) =
8879 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8880 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8881
8882 let dirty_regular_buffer = cx.new(|cx| {
8883 TestItem::new(cx)
8884 .with_dirty(true)
8885 .with_label("1.txt")
8886 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8887 });
8888 let dirty_regular_buffer_2 = cx.new(|cx| {
8889 TestItem::new(cx)
8890 .with_dirty(true)
8891 .with_label("2.txt")
8892 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8893 });
8894 let dirty_multi_buffer_with_both = cx.new(|cx| {
8895 TestItem::new(cx)
8896 .with_dirty(true)
8897 .with_singleton(false)
8898 .with_label("Fake Project Search")
8899 .with_project_items(&[
8900 dirty_regular_buffer.read(cx).project_items[0].clone(),
8901 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8902 ])
8903 });
8904 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
8905 workspace.update_in(cx, |workspace, window, cx| {
8906 workspace.add_item(
8907 pane.clone(),
8908 Box::new(dirty_regular_buffer.clone()),
8909 None,
8910 false,
8911 false,
8912 window,
8913 cx,
8914 );
8915 workspace.add_item(
8916 pane.clone(),
8917 Box::new(dirty_regular_buffer_2.clone()),
8918 None,
8919 false,
8920 false,
8921 window,
8922 cx,
8923 );
8924 workspace.add_item(
8925 pane.clone(),
8926 Box::new(dirty_multi_buffer_with_both.clone()),
8927 None,
8928 false,
8929 false,
8930 window,
8931 cx,
8932 );
8933 });
8934
8935 pane.update_in(cx, |pane, window, cx| {
8936 pane.activate_item(2, true, true, window, cx);
8937 assert_eq!(
8938 pane.active_item().unwrap().item_id(),
8939 multi_buffer_with_both_files_id,
8940 "Should select the multi buffer in the pane"
8941 );
8942 });
8943 let close_all_but_multi_buffer_task = pane
8944 .update_in(cx, |pane, window, cx| {
8945 pane.close_inactive_items(
8946 &CloseInactiveItems {
8947 save_intent: Some(SaveIntent::Save),
8948 close_pinned: true,
8949 },
8950 window,
8951 cx,
8952 )
8953 })
8954 .expect("should have inactive files to close");
8955 cx.background_executor.run_until_parked();
8956 assert!(!cx.has_pending_prompt());
8957 close_all_but_multi_buffer_task
8958 .await
8959 .expect("Closing all buffers but the multi buffer failed");
8960 pane.update(cx, |pane, cx| {
8961 assert_eq!(dirty_regular_buffer.read(cx).save_count, 1);
8962 assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
8963 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 1);
8964 assert_eq!(pane.items_len(), 1);
8965 assert_eq!(
8966 pane.active_item().unwrap().item_id(),
8967 multi_buffer_with_both_files_id,
8968 "Should have only the multi buffer left in the pane"
8969 );
8970 assert!(
8971 dirty_multi_buffer_with_both.read(cx).is_dirty,
8972 "The multi buffer containing the unsaved buffer should still be dirty"
8973 );
8974 });
8975
8976 dirty_regular_buffer.update(cx, |buffer, cx| {
8977 buffer.project_items[0].update(cx, |pi, _| pi.is_dirty = true)
8978 });
8979
8980 let close_multi_buffer_task = pane
8981 .update_in(cx, |pane, window, cx| {
8982 pane.close_active_item(
8983 &CloseActiveItem {
8984 save_intent: Some(SaveIntent::Close),
8985 close_pinned: false,
8986 },
8987 window,
8988 cx,
8989 )
8990 })
8991 .expect("should have the multi buffer to close");
8992 cx.background_executor.run_until_parked();
8993 assert!(
8994 cx.has_pending_prompt(),
8995 "Dirty multi buffer should prompt a save dialog"
8996 );
8997 cx.simulate_prompt_answer("Save");
8998 cx.background_executor.run_until_parked();
8999 close_multi_buffer_task
9000 .await
9001 .expect("Closing the multi buffer failed");
9002 pane.update(cx, |pane, cx| {
9003 assert_eq!(
9004 dirty_multi_buffer_with_both.read(cx).save_count,
9005 1,
9006 "Multi buffer item should get be saved"
9007 );
9008 // Test impl does not save inner items, so we do not assert them
9009 assert_eq!(
9010 pane.items_len(),
9011 0,
9012 "No more items should be left in the pane"
9013 );
9014 assert!(pane.active_item().is_none());
9015 });
9016 }
9017
9018 #[gpui::test]
9019 async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
9020 cx: &mut TestAppContext,
9021 ) {
9022 init_test(cx);
9023
9024 let fs = FakeFs::new(cx.background_executor.clone());
9025 let project = Project::test(fs, [], cx).await;
9026 let (workspace, cx) =
9027 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
9028 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
9029
9030 let dirty_regular_buffer = cx.new(|cx| {
9031 TestItem::new(cx)
9032 .with_dirty(true)
9033 .with_label("1.txt")
9034 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
9035 });
9036 let dirty_regular_buffer_2 = cx.new(|cx| {
9037 TestItem::new(cx)
9038 .with_dirty(true)
9039 .with_label("2.txt")
9040 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
9041 });
9042 let clear_regular_buffer = cx.new(|cx| {
9043 TestItem::new(cx)
9044 .with_label("3.txt")
9045 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
9046 });
9047
9048 let dirty_multi_buffer_with_both = cx.new(|cx| {
9049 TestItem::new(cx)
9050 .with_dirty(true)
9051 .with_singleton(false)
9052 .with_label("Fake Project Search")
9053 .with_project_items(&[
9054 dirty_regular_buffer.read(cx).project_items[0].clone(),
9055 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
9056 clear_regular_buffer.read(cx).project_items[0].clone(),
9057 ])
9058 });
9059 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
9060 workspace.update_in(cx, |workspace, window, cx| {
9061 workspace.add_item(
9062 pane.clone(),
9063 Box::new(dirty_regular_buffer.clone()),
9064 None,
9065 false,
9066 false,
9067 window,
9068 cx,
9069 );
9070 workspace.add_item(
9071 pane.clone(),
9072 Box::new(dirty_multi_buffer_with_both.clone()),
9073 None,
9074 false,
9075 false,
9076 window,
9077 cx,
9078 );
9079 });
9080
9081 pane.update_in(cx, |pane, window, cx| {
9082 pane.activate_item(1, true, true, window, cx);
9083 assert_eq!(
9084 pane.active_item().unwrap().item_id(),
9085 multi_buffer_with_both_files_id,
9086 "Should select the multi buffer in the pane"
9087 );
9088 });
9089 let _close_multi_buffer_task = pane
9090 .update_in(cx, |pane, window, cx| {
9091 pane.close_active_item(
9092 &CloseActiveItem {
9093 save_intent: None,
9094 close_pinned: false,
9095 },
9096 window,
9097 cx,
9098 )
9099 })
9100 .expect("should have active multi buffer to close");
9101 cx.background_executor.run_until_parked();
9102 assert!(
9103 cx.has_pending_prompt(),
9104 "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
9105 );
9106 }
9107
9108 #[gpui::test]
9109 async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
9110 cx: &mut TestAppContext,
9111 ) {
9112 init_test(cx);
9113
9114 let fs = FakeFs::new(cx.background_executor.clone());
9115 let project = Project::test(fs, [], cx).await;
9116 let (workspace, cx) =
9117 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
9118 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
9119
9120 let dirty_regular_buffer = cx.new(|cx| {
9121 TestItem::new(cx)
9122 .with_dirty(true)
9123 .with_label("1.txt")
9124 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
9125 });
9126 let dirty_regular_buffer_2 = cx.new(|cx| {
9127 TestItem::new(cx)
9128 .with_dirty(true)
9129 .with_label("2.txt")
9130 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
9131 });
9132 let clear_regular_buffer = cx.new(|cx| {
9133 TestItem::new(cx)
9134 .with_label("3.txt")
9135 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
9136 });
9137
9138 let dirty_multi_buffer = cx.new(|cx| {
9139 TestItem::new(cx)
9140 .with_dirty(true)
9141 .with_singleton(false)
9142 .with_label("Fake Project Search")
9143 .with_project_items(&[
9144 dirty_regular_buffer.read(cx).project_items[0].clone(),
9145 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
9146 clear_regular_buffer.read(cx).project_items[0].clone(),
9147 ])
9148 });
9149 workspace.update_in(cx, |workspace, window, cx| {
9150 workspace.add_item(
9151 pane.clone(),
9152 Box::new(dirty_regular_buffer.clone()),
9153 None,
9154 false,
9155 false,
9156 window,
9157 cx,
9158 );
9159 workspace.add_item(
9160 pane.clone(),
9161 Box::new(dirty_regular_buffer_2.clone()),
9162 None,
9163 false,
9164 false,
9165 window,
9166 cx,
9167 );
9168 workspace.add_item(
9169 pane.clone(),
9170 Box::new(dirty_multi_buffer.clone()),
9171 None,
9172 false,
9173 false,
9174 window,
9175 cx,
9176 );
9177 });
9178
9179 pane.update_in(cx, |pane, window, cx| {
9180 pane.activate_item(2, true, true, window, cx);
9181 assert_eq!(
9182 pane.active_item().unwrap().item_id(),
9183 dirty_multi_buffer.item_id(),
9184 "Should select the multi buffer in the pane"
9185 );
9186 });
9187 let close_multi_buffer_task = pane
9188 .update_in(cx, |pane, window, cx| {
9189 pane.close_active_item(
9190 &CloseActiveItem {
9191 save_intent: None,
9192 close_pinned: false,
9193 },
9194 window,
9195 cx,
9196 )
9197 })
9198 .expect("should have active multi buffer to close");
9199 cx.background_executor.run_until_parked();
9200 assert!(
9201 !cx.has_pending_prompt(),
9202 "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
9203 );
9204 close_multi_buffer_task
9205 .await
9206 .expect("Closing multi buffer failed");
9207 pane.update(cx, |pane, cx| {
9208 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
9209 assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
9210 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
9211 assert_eq!(
9212 pane.items()
9213 .map(|item| item.item_id())
9214 .sorted()
9215 .collect::<Vec<_>>(),
9216 vec![
9217 dirty_regular_buffer.item_id(),
9218 dirty_regular_buffer_2.item_id(),
9219 ],
9220 "Should have no multi buffer left in the pane"
9221 );
9222 assert!(dirty_regular_buffer.read(cx).is_dirty);
9223 assert!(dirty_regular_buffer_2.read(cx).is_dirty);
9224 });
9225 }
9226
9227 #[gpui::test]
9228 async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) {
9229 init_test(cx);
9230 let fs = FakeFs::new(cx.executor());
9231 let project = Project::test(fs, [], cx).await;
9232 let (workspace, cx) =
9233 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
9234
9235 // Add a new panel to the right dock, opening the dock and setting the
9236 // focus to the new panel.
9237 let panel = workspace.update_in(cx, |workspace, window, cx| {
9238 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
9239 workspace.add_panel(panel.clone(), window, cx);
9240
9241 workspace
9242 .right_dock()
9243 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
9244
9245 workspace.toggle_panel_focus::<TestPanel>(window, cx);
9246
9247 panel
9248 });
9249
9250 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
9251 // panel to the next valid position which, in this case, is the left
9252 // dock.
9253 cx.dispatch_action(MoveFocusedPanelToNextPosition);
9254 workspace.update(cx, |workspace, cx| {
9255 assert!(workspace.left_dock().read(cx).is_open());
9256 assert_eq!(panel.read(cx).position, DockPosition::Left);
9257 });
9258
9259 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
9260 // panel to the next valid position which, in this case, is the bottom
9261 // dock.
9262 cx.dispatch_action(MoveFocusedPanelToNextPosition);
9263 workspace.update(cx, |workspace, cx| {
9264 assert!(workspace.bottom_dock().read(cx).is_open());
9265 assert_eq!(panel.read(cx).position, DockPosition::Bottom);
9266 });
9267
9268 // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time
9269 // around moving the panel to its initial position, the right dock.
9270 cx.dispatch_action(MoveFocusedPanelToNextPosition);
9271 workspace.update(cx, |workspace, cx| {
9272 assert!(workspace.right_dock().read(cx).is_open());
9273 assert_eq!(panel.read(cx).position, DockPosition::Right);
9274 });
9275
9276 // Remove focus from the panel, ensuring that, if the panel is not
9277 // focused, the `MoveFocusedPanelToNextPosition` action does not update
9278 // the panel's position, so the panel is still in the right dock.
9279 workspace.update_in(cx, |workspace, window, cx| {
9280 workspace.toggle_panel_focus::<TestPanel>(window, cx);
9281 });
9282
9283 cx.dispatch_action(MoveFocusedPanelToNextPosition);
9284 workspace.update(cx, |workspace, cx| {
9285 assert!(workspace.right_dock().read(cx).is_open());
9286 assert_eq!(panel.read(cx).position, DockPosition::Right);
9287 });
9288 }
9289
9290 mod register_project_item_tests {
9291
9292 use super::*;
9293
9294 // View
9295 struct TestPngItemView {
9296 focus_handle: FocusHandle,
9297 }
9298 // Model
9299 struct TestPngItem {}
9300
9301 impl project::ProjectItem for TestPngItem {
9302 fn try_open(
9303 _project: &Entity<Project>,
9304 path: &ProjectPath,
9305 cx: &mut App,
9306 ) -> Option<Task<gpui::Result<Entity<Self>>>> {
9307 if path.path.extension().unwrap() == "png" {
9308 Some(cx.spawn(async move |cx| cx.new(|_| TestPngItem {})))
9309 } else {
9310 None
9311 }
9312 }
9313
9314 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
9315 None
9316 }
9317
9318 fn project_path(&self, _: &App) -> Option<ProjectPath> {
9319 None
9320 }
9321
9322 fn is_dirty(&self) -> bool {
9323 false
9324 }
9325 }
9326
9327 impl Item for TestPngItemView {
9328 type Event = ();
9329 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
9330 "".into()
9331 }
9332 }
9333 impl EventEmitter<()> for TestPngItemView {}
9334 impl Focusable for TestPngItemView {
9335 fn focus_handle(&self, _cx: &App) -> FocusHandle {
9336 self.focus_handle.clone()
9337 }
9338 }
9339
9340 impl Render for TestPngItemView {
9341 fn render(
9342 &mut self,
9343 _window: &mut Window,
9344 _cx: &mut Context<Self>,
9345 ) -> impl IntoElement {
9346 Empty
9347 }
9348 }
9349
9350 impl ProjectItem for TestPngItemView {
9351 type Item = TestPngItem;
9352
9353 fn for_project_item(
9354 _project: Entity<Project>,
9355 _pane: Option<&Pane>,
9356 _item: Entity<Self::Item>,
9357 _: &mut Window,
9358 cx: &mut Context<Self>,
9359 ) -> Self
9360 where
9361 Self: Sized,
9362 {
9363 Self {
9364 focus_handle: cx.focus_handle(),
9365 }
9366 }
9367 }
9368
9369 // View
9370 struct TestIpynbItemView {
9371 focus_handle: FocusHandle,
9372 }
9373 // Model
9374 struct TestIpynbItem {}
9375
9376 impl project::ProjectItem for TestIpynbItem {
9377 fn try_open(
9378 _project: &Entity<Project>,
9379 path: &ProjectPath,
9380 cx: &mut App,
9381 ) -> Option<Task<gpui::Result<Entity<Self>>>> {
9382 if path.path.extension().unwrap() == "ipynb" {
9383 Some(cx.spawn(async move |cx| cx.new(|_| TestIpynbItem {})))
9384 } else {
9385 None
9386 }
9387 }
9388
9389 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
9390 None
9391 }
9392
9393 fn project_path(&self, _: &App) -> Option<ProjectPath> {
9394 None
9395 }
9396
9397 fn is_dirty(&self) -> bool {
9398 false
9399 }
9400 }
9401
9402 impl Item for TestIpynbItemView {
9403 type Event = ();
9404 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
9405 "".into()
9406 }
9407 }
9408 impl EventEmitter<()> for TestIpynbItemView {}
9409 impl Focusable for TestIpynbItemView {
9410 fn focus_handle(&self, _cx: &App) -> FocusHandle {
9411 self.focus_handle.clone()
9412 }
9413 }
9414
9415 impl Render for TestIpynbItemView {
9416 fn render(
9417 &mut self,
9418 _window: &mut Window,
9419 _cx: &mut Context<Self>,
9420 ) -> impl IntoElement {
9421 Empty
9422 }
9423 }
9424
9425 impl ProjectItem for TestIpynbItemView {
9426 type Item = TestIpynbItem;
9427
9428 fn for_project_item(
9429 _project: Entity<Project>,
9430 _pane: Option<&Pane>,
9431 _item: Entity<Self::Item>,
9432 _: &mut Window,
9433 cx: &mut Context<Self>,
9434 ) -> Self
9435 where
9436 Self: Sized,
9437 {
9438 Self {
9439 focus_handle: cx.focus_handle(),
9440 }
9441 }
9442 }
9443
9444 struct TestAlternatePngItemView {
9445 focus_handle: FocusHandle,
9446 }
9447
9448 impl Item for TestAlternatePngItemView {
9449 type Event = ();
9450 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
9451 "".into()
9452 }
9453 }
9454
9455 impl EventEmitter<()> for TestAlternatePngItemView {}
9456 impl Focusable for TestAlternatePngItemView {
9457 fn focus_handle(&self, _cx: &App) -> FocusHandle {
9458 self.focus_handle.clone()
9459 }
9460 }
9461
9462 impl Render for TestAlternatePngItemView {
9463 fn render(
9464 &mut self,
9465 _window: &mut Window,
9466 _cx: &mut Context<Self>,
9467 ) -> impl IntoElement {
9468 Empty
9469 }
9470 }
9471
9472 impl ProjectItem for TestAlternatePngItemView {
9473 type Item = TestPngItem;
9474
9475 fn for_project_item(
9476 _project: Entity<Project>,
9477 _pane: Option<&Pane>,
9478 _item: Entity<Self::Item>,
9479 _: &mut Window,
9480 cx: &mut Context<Self>,
9481 ) -> Self
9482 where
9483 Self: Sized,
9484 {
9485 Self {
9486 focus_handle: cx.focus_handle(),
9487 }
9488 }
9489 }
9490
9491 #[gpui::test]
9492 async fn test_register_project_item(cx: &mut TestAppContext) {
9493 init_test(cx);
9494
9495 cx.update(|cx| {
9496 register_project_item::<TestPngItemView>(cx);
9497 register_project_item::<TestIpynbItemView>(cx);
9498 });
9499
9500 let fs = FakeFs::new(cx.executor());
9501 fs.insert_tree(
9502 "/root1",
9503 json!({
9504 "one.png": "BINARYDATAHERE",
9505 "two.ipynb": "{ totally a notebook }",
9506 "three.txt": "editing text, sure why not?"
9507 }),
9508 )
9509 .await;
9510
9511 let project = Project::test(fs, ["root1".as_ref()], cx).await;
9512 let (workspace, cx) =
9513 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
9514
9515 let worktree_id = project.update(cx, |project, cx| {
9516 project.worktrees(cx).next().unwrap().read(cx).id()
9517 });
9518
9519 let handle = workspace
9520 .update_in(cx, |workspace, window, cx| {
9521 let project_path = (worktree_id, "one.png");
9522 workspace.open_path(project_path, None, true, window, cx)
9523 })
9524 .await
9525 .unwrap();
9526
9527 // Now we can check if the handle we got back errored or not
9528 assert_eq!(
9529 handle.to_any().entity_type(),
9530 TypeId::of::<TestPngItemView>()
9531 );
9532
9533 let handle = workspace
9534 .update_in(cx, |workspace, window, cx| {
9535 let project_path = (worktree_id, "two.ipynb");
9536 workspace.open_path(project_path, None, true, window, cx)
9537 })
9538 .await
9539 .unwrap();
9540
9541 assert_eq!(
9542 handle.to_any().entity_type(),
9543 TypeId::of::<TestIpynbItemView>()
9544 );
9545
9546 let handle = workspace
9547 .update_in(cx, |workspace, window, cx| {
9548 let project_path = (worktree_id, "three.txt");
9549 workspace.open_path(project_path, None, true, window, cx)
9550 })
9551 .await;
9552 assert!(handle.is_err());
9553 }
9554
9555 #[gpui::test]
9556 async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
9557 init_test(cx);
9558
9559 cx.update(|cx| {
9560 register_project_item::<TestPngItemView>(cx);
9561 register_project_item::<TestAlternatePngItemView>(cx);
9562 });
9563
9564 let fs = FakeFs::new(cx.executor());
9565 fs.insert_tree(
9566 "/root1",
9567 json!({
9568 "one.png": "BINARYDATAHERE",
9569 "two.ipynb": "{ totally a notebook }",
9570 "three.txt": "editing text, sure why not?"
9571 }),
9572 )
9573 .await;
9574 let project = Project::test(fs, ["root1".as_ref()], cx).await;
9575 let (workspace, cx) =
9576 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
9577 let worktree_id = project.update(cx, |project, cx| {
9578 project.worktrees(cx).next().unwrap().read(cx).id()
9579 });
9580
9581 let handle = workspace
9582 .update_in(cx, |workspace, window, cx| {
9583 let project_path = (worktree_id, "one.png");
9584 workspace.open_path(project_path, None, true, window, cx)
9585 })
9586 .await
9587 .unwrap();
9588
9589 // This _must_ be the second item registered
9590 assert_eq!(
9591 handle.to_any().entity_type(),
9592 TypeId::of::<TestAlternatePngItemView>()
9593 );
9594
9595 let handle = workspace
9596 .update_in(cx, |workspace, window, cx| {
9597 let project_path = (worktree_id, "three.txt");
9598 workspace.open_path(project_path, None, true, window, cx)
9599 })
9600 .await;
9601 assert!(handle.is_err());
9602 }
9603 }
9604
9605 pub fn init_test(cx: &mut TestAppContext) {
9606 cx.update(|cx| {
9607 let settings_store = SettingsStore::test(cx);
9608 cx.set_global(settings_store);
9609 theme::init(theme::LoadThemes::JustBase, cx);
9610 language::init(cx);
9611 crate::init_settings(cx);
9612 Project::init_settings(cx);
9613 });
9614 }
9615
9616 fn dirty_project_item(id: u64, path: &str, cx: &mut App) -> Entity<TestProjectItem> {
9617 let item = TestProjectItem::new(id, path, cx);
9618 item.update(cx, |item, _| {
9619 item.is_dirty = true;
9620 });
9621 item
9622 }
9623}