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