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