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