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 let pane = pane.read(cx);
3506 if let Some(item) = pane.active_item() {
3507 item.item_focus_handle(cx).focus(window);
3508 } else {
3509 log::error!(
3510 "Could not find a focus target when in switching focus in {direction} direction for a pane",
3511 );
3512 }
3513 }
3514 Some(ActivateInDirectionTarget::Dock(dock)) => {
3515 // Defer this to avoid a panic when the dock's active panel is already on the stack.
3516 window.defer(cx, move |window, cx| {
3517 let dock = dock.read(cx);
3518 if let Some(panel) = dock.active_panel() {
3519 panel.panel_focus_handle(cx).focus(window);
3520 } else {
3521 log::error!("Could not find a focus target when in switching focus in {direction} direction for a {:?} dock", dock.position());
3522 }
3523 })
3524 }
3525 None => {}
3526 }
3527 }
3528
3529 pub fn move_item_to_pane_in_direction(
3530 &mut self,
3531 action: &MoveItemToPaneInDirection,
3532 window: &mut Window,
3533 cx: &mut Context<Self>,
3534 ) {
3535 let destination = match self.find_pane_in_direction(action.direction, cx) {
3536 Some(destination) => destination,
3537 None => {
3538 if self.active_pane.read(cx).items_len() < 2 {
3539 return;
3540 }
3541 let new_pane = self.add_pane(window, cx);
3542 if self
3543 .center
3544 .split(&self.active_pane, &new_pane, action.direction)
3545 .log_err()
3546 .is_none()
3547 {
3548 return;
3549 };
3550 new_pane
3551 }
3552 };
3553
3554 move_active_item(
3555 &self.active_pane,
3556 &destination,
3557 action.focus,
3558 true,
3559 window,
3560 cx,
3561 );
3562 }
3563
3564 pub fn bounding_box_for_pane(&self, pane: &Entity<Pane>) -> Option<Bounds<Pixels>> {
3565 self.center.bounding_box_for_pane(pane)
3566 }
3567
3568 pub fn find_pane_in_direction(
3569 &mut self,
3570 direction: SplitDirection,
3571 cx: &App,
3572 ) -> Option<Entity<Pane>> {
3573 self.center
3574 .find_pane_in_direction(&self.active_pane, direction, cx)
3575 .cloned()
3576 }
3577
3578 pub fn swap_pane_in_direction(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
3579 if let Some(to) = self.find_pane_in_direction(direction, cx) {
3580 self.center.swap(&self.active_pane, &to);
3581 cx.notify();
3582 }
3583 }
3584
3585 pub fn resize_pane(
3586 &mut self,
3587 axis: gpui::Axis,
3588 amount: Pixels,
3589 window: &mut Window,
3590 cx: &mut Context<Self>,
3591 ) {
3592 let docks = self.all_docks();
3593 let active_dock = docks
3594 .into_iter()
3595 .find(|dock| dock.focus_handle(cx).contains_focused(window, cx));
3596
3597 if let Some(dock) = active_dock {
3598 let Some(panel_size) = dock.read(cx).active_panel_size(window, cx) else {
3599 return;
3600 };
3601 match dock.read(cx).position() {
3602 DockPosition::Left => resize_left_dock(panel_size + amount, self, window, cx),
3603 DockPosition::Bottom => resize_bottom_dock(panel_size + amount, self, window, cx),
3604 DockPosition::Right => resize_right_dock(panel_size + amount, self, window, cx),
3605 }
3606 } else {
3607 self.center
3608 .resize(&self.active_pane, axis, amount, &self.bounds);
3609 }
3610 cx.notify();
3611 }
3612
3613 pub fn reset_pane_sizes(&mut self, cx: &mut Context<Self>) {
3614 self.center.reset_pane_sizes();
3615 cx.notify();
3616 }
3617
3618 fn handle_pane_focused(
3619 &mut self,
3620 pane: Entity<Pane>,
3621 window: &mut Window,
3622 cx: &mut Context<Self>,
3623 ) {
3624 // This is explicitly hoisted out of the following check for pane identity as
3625 // terminal panel panes are not registered as a center panes.
3626 self.status_bar.update(cx, |status_bar, cx| {
3627 status_bar.set_active_pane(&pane, window, cx);
3628 });
3629 if self.active_pane != pane {
3630 self.set_active_pane(&pane, window, cx);
3631 }
3632
3633 if self.last_active_center_pane.is_none() {
3634 self.last_active_center_pane = Some(pane.downgrade());
3635 }
3636
3637 self.dismiss_zoomed_items_to_reveal(None, window, cx);
3638 if pane.read(cx).is_zoomed() {
3639 self.zoomed = Some(pane.downgrade().into());
3640 } else {
3641 self.zoomed = None;
3642 }
3643 self.zoomed_position = None;
3644 cx.emit(Event::ZoomChanged);
3645 self.update_active_view_for_followers(window, cx);
3646 pane.update(cx, |pane, _| {
3647 pane.track_alternate_file_items();
3648 });
3649
3650 cx.notify();
3651 }
3652
3653 fn set_active_pane(
3654 &mut self,
3655 pane: &Entity<Pane>,
3656 window: &mut Window,
3657 cx: &mut Context<Self>,
3658 ) {
3659 self.active_pane = pane.clone();
3660 self.active_item_path_changed(window, cx);
3661 self.last_active_center_pane = Some(pane.downgrade());
3662 }
3663
3664 fn handle_panel_focused(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3665 self.update_active_view_for_followers(window, cx);
3666 }
3667
3668 fn handle_pane_event(
3669 &mut self,
3670 pane: &Entity<Pane>,
3671 event: &pane::Event,
3672 window: &mut Window,
3673 cx: &mut Context<Self>,
3674 ) {
3675 let mut serialize_workspace = true;
3676 match event {
3677 pane::Event::AddItem { item } => {
3678 item.added_to_pane(self, pane.clone(), window, cx);
3679 cx.emit(Event::ItemAdded {
3680 item: item.boxed_clone(),
3681 });
3682 }
3683 pane::Event::Split(direction) => {
3684 self.split_and_clone(pane.clone(), *direction, window, cx);
3685 }
3686 pane::Event::JoinIntoNext => {
3687 self.join_pane_into_next(pane.clone(), window, cx);
3688 }
3689 pane::Event::JoinAll => {
3690 self.join_all_panes(window, cx);
3691 }
3692 pane::Event::Remove { focus_on_pane } => {
3693 self.remove_pane(pane.clone(), focus_on_pane.clone(), window, cx);
3694 }
3695 pane::Event::ActivateItem {
3696 local,
3697 focus_changed,
3698 } => {
3699 cx.on_next_frame(window, |_, window, _| {
3700 window.invalidate_character_coordinates();
3701 });
3702
3703 pane.update(cx, |pane, _| {
3704 pane.track_alternate_file_items();
3705 });
3706 if *local {
3707 self.unfollow_in_pane(&pane, window, cx);
3708 }
3709 if pane == self.active_pane() {
3710 self.active_item_path_changed(window, cx);
3711 self.update_active_view_for_followers(window, cx);
3712 }
3713 serialize_workspace = *focus_changed || pane != self.active_pane();
3714 }
3715 pane::Event::UserSavedItem { item, save_intent } => {
3716 cx.emit(Event::UserSavedItem {
3717 pane: pane.downgrade(),
3718 item: item.boxed_clone(),
3719 save_intent: *save_intent,
3720 });
3721 serialize_workspace = false;
3722 }
3723 pane::Event::ChangeItemTitle => {
3724 if *pane == self.active_pane {
3725 self.active_item_path_changed(window, cx);
3726 }
3727 serialize_workspace = false;
3728 }
3729 pane::Event::RemoveItem { .. } => {}
3730 pane::Event::RemovedItem { item } => {
3731 cx.emit(Event::ActiveItemChanged);
3732 self.update_window_edited(window, cx);
3733 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(item.item_id()) {
3734 if entry.get().entity_id() == pane.entity_id() {
3735 entry.remove();
3736 }
3737 }
3738 }
3739 pane::Event::Focus => {
3740 cx.on_next_frame(window, |_, window, _| {
3741 window.invalidate_character_coordinates();
3742 });
3743 self.handle_pane_focused(pane.clone(), window, cx);
3744 }
3745 pane::Event::ZoomIn => {
3746 if *pane == self.active_pane {
3747 pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
3748 if pane.read(cx).has_focus(window, cx) {
3749 self.zoomed = Some(pane.downgrade().into());
3750 self.zoomed_position = None;
3751 cx.emit(Event::ZoomChanged);
3752 }
3753 cx.notify();
3754 }
3755 }
3756 pane::Event::ZoomOut => {
3757 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
3758 if self.zoomed_position.is_none() {
3759 self.zoomed = None;
3760 cx.emit(Event::ZoomChanged);
3761 }
3762 cx.notify();
3763 }
3764 pane::Event::ItemPinned | pane::Event::ItemUnpinned => {}
3765 }
3766
3767 if serialize_workspace {
3768 self.serialize_workspace(window, cx);
3769 }
3770 }
3771
3772 pub fn unfollow_in_pane(
3773 &mut self,
3774 pane: &Entity<Pane>,
3775 window: &mut Window,
3776 cx: &mut Context<Workspace>,
3777 ) -> Option<CollaboratorId> {
3778 let leader_id = self.leader_for_pane(pane)?;
3779 self.unfollow(leader_id, window, cx);
3780 Some(leader_id)
3781 }
3782
3783 pub fn split_pane(
3784 &mut self,
3785 pane_to_split: Entity<Pane>,
3786 split_direction: SplitDirection,
3787 window: &mut Window,
3788 cx: &mut Context<Self>,
3789 ) -> Entity<Pane> {
3790 let new_pane = self.add_pane(window, cx);
3791 self.center
3792 .split(&pane_to_split, &new_pane, split_direction)
3793 .unwrap();
3794 cx.notify();
3795 new_pane
3796 }
3797
3798 pub fn split_and_clone(
3799 &mut self,
3800 pane: Entity<Pane>,
3801 direction: SplitDirection,
3802 window: &mut Window,
3803 cx: &mut Context<Self>,
3804 ) -> Option<Entity<Pane>> {
3805 let item = pane.read(cx).active_item()?;
3806 let maybe_pane_handle =
3807 if let Some(clone) = item.clone_on_split(self.database_id(), window, cx) {
3808 let new_pane = self.add_pane(window, cx);
3809 new_pane.update(cx, |pane, cx| {
3810 pane.add_item(clone, true, true, None, window, cx)
3811 });
3812 self.center.split(&pane, &new_pane, direction).unwrap();
3813 Some(new_pane)
3814 } else {
3815 None
3816 };
3817 cx.notify();
3818 maybe_pane_handle
3819 }
3820
3821 pub fn split_pane_with_item(
3822 &mut self,
3823 pane_to_split: WeakEntity<Pane>,
3824 split_direction: SplitDirection,
3825 from: WeakEntity<Pane>,
3826 item_id_to_move: EntityId,
3827 window: &mut Window,
3828 cx: &mut Context<Self>,
3829 ) {
3830 let Some(pane_to_split) = pane_to_split.upgrade() else {
3831 return;
3832 };
3833 let Some(from) = from.upgrade() else {
3834 return;
3835 };
3836
3837 let new_pane = self.add_pane(window, cx);
3838 move_item(&from, &new_pane, item_id_to_move, 0, window, cx);
3839 self.center
3840 .split(&pane_to_split, &new_pane, split_direction)
3841 .unwrap();
3842 cx.notify();
3843 }
3844
3845 pub fn split_pane_with_project_entry(
3846 &mut self,
3847 pane_to_split: WeakEntity<Pane>,
3848 split_direction: SplitDirection,
3849 project_entry: ProjectEntryId,
3850 window: &mut Window,
3851 cx: &mut Context<Self>,
3852 ) -> Option<Task<Result<()>>> {
3853 let pane_to_split = pane_to_split.upgrade()?;
3854 let new_pane = self.add_pane(window, cx);
3855 self.center
3856 .split(&pane_to_split, &new_pane, split_direction)
3857 .unwrap();
3858
3859 let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
3860 let task = self.open_path(path, Some(new_pane.downgrade()), true, window, cx);
3861 Some(cx.foreground_executor().spawn(async move {
3862 task.await?;
3863 Ok(())
3864 }))
3865 }
3866
3867 pub fn join_all_panes(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3868 let active_item = self.active_pane.read(cx).active_item();
3869 for pane in &self.panes {
3870 join_pane_into_active(&self.active_pane, pane, window, cx);
3871 }
3872 if let Some(active_item) = active_item {
3873 self.activate_item(active_item.as_ref(), true, true, window, cx);
3874 }
3875 cx.notify();
3876 }
3877
3878 pub fn join_pane_into_next(
3879 &mut self,
3880 pane: Entity<Pane>,
3881 window: &mut Window,
3882 cx: &mut Context<Self>,
3883 ) {
3884 let next_pane = self
3885 .find_pane_in_direction(SplitDirection::Right, cx)
3886 .or_else(|| self.find_pane_in_direction(SplitDirection::Down, cx))
3887 .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
3888 .or_else(|| self.find_pane_in_direction(SplitDirection::Up, cx));
3889 let Some(next_pane) = next_pane else {
3890 return;
3891 };
3892 move_all_items(&pane, &next_pane, window, cx);
3893 cx.notify();
3894 }
3895
3896 fn remove_pane(
3897 &mut self,
3898 pane: Entity<Pane>,
3899 focus_on: Option<Entity<Pane>>,
3900 window: &mut Window,
3901 cx: &mut Context<Self>,
3902 ) {
3903 if self.center.remove(&pane).unwrap() {
3904 self.force_remove_pane(&pane, &focus_on, window, cx);
3905 self.unfollow_in_pane(&pane, window, cx);
3906 self.last_leaders_by_pane.remove(&pane.downgrade());
3907 for removed_item in pane.read(cx).items() {
3908 self.panes_by_item.remove(&removed_item.item_id());
3909 }
3910
3911 cx.notify();
3912 } else {
3913 self.active_item_path_changed(window, cx);
3914 }
3915 cx.emit(Event::PaneRemoved);
3916 }
3917
3918 pub fn panes(&self) -> &[Entity<Pane>] {
3919 &self.panes
3920 }
3921
3922 pub fn active_pane(&self) -> &Entity<Pane> {
3923 &self.active_pane
3924 }
3925
3926 pub fn focused_pane(&self, window: &Window, cx: &App) -> Entity<Pane> {
3927 for dock in self.all_docks() {
3928 if dock.focus_handle(cx).contains_focused(window, cx) {
3929 if let Some(pane) = dock
3930 .read(cx)
3931 .active_panel()
3932 .and_then(|panel| panel.pane(cx))
3933 {
3934 return pane;
3935 }
3936 }
3937 }
3938 self.active_pane().clone()
3939 }
3940
3941 pub fn adjacent_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<Pane> {
3942 self.find_pane_in_direction(SplitDirection::Right, cx)
3943 .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
3944 .unwrap_or_else(|| {
3945 self.split_pane(self.active_pane.clone(), SplitDirection::Right, window, cx)
3946 })
3947 .clone()
3948 }
3949
3950 pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<Entity<Pane>> {
3951 let weak_pane = self.panes_by_item.get(&handle.item_id())?;
3952 weak_pane.upgrade()
3953 }
3954
3955 fn collaborator_left(&mut self, peer_id: PeerId, window: &mut Window, cx: &mut Context<Self>) {
3956 self.follower_states.retain(|leader_id, state| {
3957 if *leader_id == CollaboratorId::PeerId(peer_id) {
3958 for item in state.items_by_leader_view_id.values() {
3959 item.view.set_leader_id(None, window, cx);
3960 }
3961 false
3962 } else {
3963 true
3964 }
3965 });
3966 cx.notify();
3967 }
3968
3969 pub fn start_following(
3970 &mut self,
3971 leader_id: impl Into<CollaboratorId>,
3972 window: &mut Window,
3973 cx: &mut Context<Self>,
3974 ) -> Option<Task<Result<()>>> {
3975 let leader_id = leader_id.into();
3976 let pane = self.active_pane().clone();
3977
3978 self.last_leaders_by_pane
3979 .insert(pane.downgrade(), leader_id);
3980 self.unfollow(leader_id, window, cx);
3981 self.unfollow_in_pane(&pane, window, cx);
3982 self.follower_states.insert(
3983 leader_id,
3984 FollowerState {
3985 center_pane: pane.clone(),
3986 dock_pane: None,
3987 active_view_id: None,
3988 items_by_leader_view_id: Default::default(),
3989 },
3990 );
3991 cx.notify();
3992
3993 match leader_id {
3994 CollaboratorId::PeerId(leader_peer_id) => {
3995 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
3996 let project_id = self.project.read(cx).remote_id();
3997 let request = self.app_state.client.request(proto::Follow {
3998 room_id,
3999 project_id,
4000 leader_id: Some(leader_peer_id),
4001 });
4002
4003 Some(cx.spawn_in(window, async move |this, cx| {
4004 let response = request.await?;
4005 this.update(cx, |this, _| {
4006 let state = this
4007 .follower_states
4008 .get_mut(&leader_id)
4009 .context("following interrupted")?;
4010 state.active_view_id = response
4011 .active_view
4012 .as_ref()
4013 .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
4014 anyhow::Ok(())
4015 })??;
4016 if let Some(view) = response.active_view {
4017 Self::add_view_from_leader(this.clone(), leader_peer_id, &view, cx).await?;
4018 }
4019 this.update_in(cx, |this, window, cx| {
4020 this.leader_updated(leader_id, window, cx)
4021 })?;
4022 Ok(())
4023 }))
4024 }
4025 CollaboratorId::Agent => {
4026 self.leader_updated(leader_id, window, cx)?;
4027 Some(Task::ready(Ok(())))
4028 }
4029 }
4030 }
4031
4032 pub fn follow_next_collaborator(
4033 &mut self,
4034 _: &FollowNextCollaborator,
4035 window: &mut Window,
4036 cx: &mut Context<Self>,
4037 ) {
4038 let collaborators = self.project.read(cx).collaborators();
4039 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
4040 let mut collaborators = collaborators.keys().copied();
4041 for peer_id in collaborators.by_ref() {
4042 if CollaboratorId::PeerId(peer_id) == leader_id {
4043 break;
4044 }
4045 }
4046 collaborators.next().map(CollaboratorId::PeerId)
4047 } else if let Some(last_leader_id) =
4048 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
4049 {
4050 match last_leader_id {
4051 CollaboratorId::PeerId(peer_id) => {
4052 if collaborators.contains_key(peer_id) {
4053 Some(*last_leader_id)
4054 } else {
4055 None
4056 }
4057 }
4058 CollaboratorId::Agent => Some(CollaboratorId::Agent),
4059 }
4060 } else {
4061 None
4062 };
4063
4064 let pane = self.active_pane.clone();
4065 let Some(leader_id) = next_leader_id.or_else(|| {
4066 Some(CollaboratorId::PeerId(
4067 collaborators.keys().copied().next()?,
4068 ))
4069 }) else {
4070 return;
4071 };
4072 if self.unfollow_in_pane(&pane, window, cx) == Some(leader_id) {
4073 return;
4074 }
4075 if let Some(task) = self.start_following(leader_id, window, cx) {
4076 task.detach_and_log_err(cx)
4077 }
4078 }
4079
4080 pub fn follow(
4081 &mut self,
4082 leader_id: impl Into<CollaboratorId>,
4083 window: &mut Window,
4084 cx: &mut Context<Self>,
4085 ) {
4086 let leader_id = leader_id.into();
4087
4088 if let CollaboratorId::PeerId(peer_id) = leader_id {
4089 let Some(room) = ActiveCall::global(cx).read(cx).room() else {
4090 return;
4091 };
4092 let room = room.read(cx);
4093 let Some(remote_participant) = room.remote_participant_for_peer_id(peer_id) else {
4094 return;
4095 };
4096
4097 let project = self.project.read(cx);
4098
4099 let other_project_id = match remote_participant.location {
4100 call::ParticipantLocation::External => None,
4101 call::ParticipantLocation::UnsharedProject => None,
4102 call::ParticipantLocation::SharedProject { project_id } => {
4103 if Some(project_id) == project.remote_id() {
4104 None
4105 } else {
4106 Some(project_id)
4107 }
4108 }
4109 };
4110
4111 // if they are active in another project, follow there.
4112 if let Some(project_id) = other_project_id {
4113 let app_state = self.app_state.clone();
4114 crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx)
4115 .detach_and_log_err(cx);
4116 }
4117 }
4118
4119 // if you're already following, find the right pane and focus it.
4120 if let Some(follower_state) = self.follower_states.get(&leader_id) {
4121 window.focus(&follower_state.pane().focus_handle(cx));
4122
4123 return;
4124 }
4125
4126 // Otherwise, follow.
4127 if let Some(task) = self.start_following(leader_id, window, cx) {
4128 task.detach_and_log_err(cx)
4129 }
4130 }
4131
4132 pub fn unfollow(
4133 &mut self,
4134 leader_id: impl Into<CollaboratorId>,
4135 window: &mut Window,
4136 cx: &mut Context<Self>,
4137 ) -> Option<()> {
4138 cx.notify();
4139
4140 let leader_id = leader_id.into();
4141 let state = self.follower_states.remove(&leader_id)?;
4142 for (_, item) in state.items_by_leader_view_id {
4143 item.view.set_leader_id(None, window, cx);
4144 }
4145
4146 if let CollaboratorId::PeerId(leader_peer_id) = leader_id {
4147 let project_id = self.project.read(cx).remote_id();
4148 let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
4149 self.app_state
4150 .client
4151 .send(proto::Unfollow {
4152 room_id,
4153 project_id,
4154 leader_id: Some(leader_peer_id),
4155 })
4156 .log_err();
4157 }
4158
4159 Some(())
4160 }
4161
4162 pub fn is_being_followed(&self, id: impl Into<CollaboratorId>) -> bool {
4163 self.follower_states.contains_key(&id.into())
4164 }
4165
4166 fn active_item_path_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4167 cx.emit(Event::ActiveItemChanged);
4168 let active_entry = self.active_project_path(cx);
4169 self.project
4170 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
4171
4172 self.update_window_title(window, cx);
4173 }
4174
4175 fn update_window_title(&mut self, window: &mut Window, cx: &mut App) {
4176 let project = self.project().read(cx);
4177 let mut title = String::new();
4178
4179 for (i, name) in project.worktree_root_names(cx).enumerate() {
4180 if i > 0 {
4181 title.push_str(", ");
4182 }
4183 title.push_str(name);
4184 }
4185
4186 if title.is_empty() {
4187 title = "empty project".to_string();
4188 }
4189
4190 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
4191 let filename = path
4192 .path
4193 .file_name()
4194 .map(|s| s.to_string_lossy())
4195 .or_else(|| {
4196 Some(Cow::Borrowed(
4197 project
4198 .worktree_for_id(path.worktree_id, cx)?
4199 .read(cx)
4200 .root_name(),
4201 ))
4202 });
4203
4204 if let Some(filename) = filename {
4205 title.push_str(" — ");
4206 title.push_str(filename.as_ref());
4207 }
4208 }
4209
4210 if project.is_via_collab() {
4211 title.push_str(" ↙");
4212 } else if project.is_shared() {
4213 title.push_str(" ↗");
4214 }
4215
4216 window.set_window_title(&title);
4217 }
4218
4219 fn update_window_edited(&mut self, window: &mut Window, cx: &mut App) {
4220 let is_edited = !self.project.read(cx).is_disconnected(cx) && !self.dirty_items.is_empty();
4221 if is_edited != self.window_edited {
4222 self.window_edited = is_edited;
4223 window.set_window_edited(self.window_edited)
4224 }
4225 }
4226
4227 fn update_item_dirty_state(
4228 &mut self,
4229 item: &dyn ItemHandle,
4230 window: &mut Window,
4231 cx: &mut App,
4232 ) {
4233 let is_dirty = item.is_dirty(cx);
4234 let item_id = item.item_id();
4235 let was_dirty = self.dirty_items.contains_key(&item_id);
4236 if is_dirty == was_dirty {
4237 return;
4238 }
4239 if was_dirty {
4240 self.dirty_items.remove(&item_id);
4241 self.update_window_edited(window, cx);
4242 return;
4243 }
4244 if let Some(window_handle) = window.window_handle().downcast::<Self>() {
4245 let s = item.on_release(
4246 cx,
4247 Box::new(move |cx| {
4248 window_handle
4249 .update(cx, |this, window, cx| {
4250 this.dirty_items.remove(&item_id);
4251 this.update_window_edited(window, cx)
4252 })
4253 .ok();
4254 }),
4255 );
4256 self.dirty_items.insert(item_id, s);
4257 self.update_window_edited(window, cx);
4258 }
4259 }
4260
4261 fn render_notifications(&self, _window: &mut Window, _cx: &mut Context<Self>) -> Option<Div> {
4262 if self.notifications.is_empty() {
4263 None
4264 } else {
4265 Some(
4266 div()
4267 .absolute()
4268 .right_3()
4269 .bottom_3()
4270 .w_112()
4271 .h_full()
4272 .flex()
4273 .flex_col()
4274 .justify_end()
4275 .gap_2()
4276 .children(
4277 self.notifications
4278 .iter()
4279 .map(|(_, notification)| notification.clone().into_any()),
4280 ),
4281 )
4282 }
4283 }
4284
4285 // RPC handlers
4286
4287 fn active_view_for_follower(
4288 &self,
4289 follower_project_id: Option<u64>,
4290 window: &mut Window,
4291 cx: &mut Context<Self>,
4292 ) -> Option<proto::View> {
4293 let (item, panel_id) = self.active_item_for_followers(window, cx);
4294 let item = item?;
4295 let leader_id = self
4296 .pane_for(&*item)
4297 .and_then(|pane| self.leader_for_pane(&pane));
4298 let leader_peer_id = match leader_id {
4299 Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
4300 Some(CollaboratorId::Agent) | None => None,
4301 };
4302
4303 let item_handle = item.to_followable_item_handle(cx)?;
4304 let id = item_handle.remote_id(&self.app_state.client, window, cx)?;
4305 let variant = item_handle.to_state_proto(window, cx)?;
4306
4307 if item_handle.is_project_item(window, cx)
4308 && (follower_project_id.is_none()
4309 || follower_project_id != self.project.read(cx).remote_id())
4310 {
4311 return None;
4312 }
4313
4314 Some(proto::View {
4315 id: id.to_proto(),
4316 leader_id: leader_peer_id,
4317 variant: Some(variant),
4318 panel_id: panel_id.map(|id| id as i32),
4319 })
4320 }
4321
4322 fn handle_follow(
4323 &mut self,
4324 follower_project_id: Option<u64>,
4325 window: &mut Window,
4326 cx: &mut Context<Self>,
4327 ) -> proto::FollowResponse {
4328 let active_view = self.active_view_for_follower(follower_project_id, window, cx);
4329
4330 cx.notify();
4331 proto::FollowResponse {
4332 // TODO: Remove after version 0.145.x stabilizes.
4333 active_view_id: active_view.as_ref().and_then(|view| view.id.clone()),
4334 views: active_view.iter().cloned().collect(),
4335 active_view,
4336 }
4337 }
4338
4339 fn handle_update_followers(
4340 &mut self,
4341 leader_id: PeerId,
4342 message: proto::UpdateFollowers,
4343 _window: &mut Window,
4344 _cx: &mut Context<Self>,
4345 ) {
4346 self.leader_updates_tx
4347 .unbounded_send((leader_id, message))
4348 .ok();
4349 }
4350
4351 async fn process_leader_update(
4352 this: &WeakEntity<Self>,
4353 leader_id: PeerId,
4354 update: proto::UpdateFollowers,
4355 cx: &mut AsyncWindowContext,
4356 ) -> Result<()> {
4357 match update.variant.context("invalid update")? {
4358 proto::update_followers::Variant::CreateView(view) => {
4359 let view_id = ViewId::from_proto(view.id.clone().context("invalid view id")?)?;
4360 let should_add_view = this.update(cx, |this, _| {
4361 if let Some(state) = this.follower_states.get_mut(&leader_id.into()) {
4362 anyhow::Ok(!state.items_by_leader_view_id.contains_key(&view_id))
4363 } else {
4364 anyhow::Ok(false)
4365 }
4366 })??;
4367
4368 if should_add_view {
4369 Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
4370 }
4371 }
4372 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
4373 let should_add_view = this.update(cx, |this, _| {
4374 if let Some(state) = this.follower_states.get_mut(&leader_id.into()) {
4375 state.active_view_id = update_active_view
4376 .view
4377 .as_ref()
4378 .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
4379
4380 if state.active_view_id.is_some_and(|view_id| {
4381 !state.items_by_leader_view_id.contains_key(&view_id)
4382 }) {
4383 anyhow::Ok(true)
4384 } else {
4385 anyhow::Ok(false)
4386 }
4387 } else {
4388 anyhow::Ok(false)
4389 }
4390 })??;
4391
4392 if should_add_view {
4393 if let Some(view) = update_active_view.view {
4394 Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
4395 }
4396 }
4397 }
4398 proto::update_followers::Variant::UpdateView(update_view) => {
4399 let variant = update_view.variant.context("missing update view variant")?;
4400 let id = update_view.id.context("missing update view id")?;
4401 let mut tasks = Vec::new();
4402 this.update_in(cx, |this, window, cx| {
4403 let project = this.project.clone();
4404 if let Some(state) = this.follower_states.get(&leader_id.into()) {
4405 let view_id = ViewId::from_proto(id.clone())?;
4406 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
4407 tasks.push(item.view.apply_update_proto(
4408 &project,
4409 variant.clone(),
4410 window,
4411 cx,
4412 ));
4413 }
4414 }
4415 anyhow::Ok(())
4416 })??;
4417 try_join_all(tasks).await.log_err();
4418 }
4419 }
4420 this.update_in(cx, |this, window, cx| {
4421 this.leader_updated(leader_id, window, cx)
4422 })?;
4423 Ok(())
4424 }
4425
4426 async fn add_view_from_leader(
4427 this: WeakEntity<Self>,
4428 leader_id: PeerId,
4429 view: &proto::View,
4430 cx: &mut AsyncWindowContext,
4431 ) -> Result<()> {
4432 let this = this.upgrade().context("workspace dropped")?;
4433
4434 let Some(id) = view.id.clone() else {
4435 anyhow::bail!("no id for view");
4436 };
4437 let id = ViewId::from_proto(id)?;
4438 let panel_id = view.panel_id.and_then(proto::PanelId::from_i32);
4439
4440 let pane = this.update(cx, |this, _cx| {
4441 let state = this
4442 .follower_states
4443 .get(&leader_id.into())
4444 .context("stopped following")?;
4445 anyhow::Ok(state.pane().clone())
4446 })??;
4447 let existing_item = pane.update_in(cx, |pane, window, cx| {
4448 let client = this.read(cx).client().clone();
4449 pane.items().find_map(|item| {
4450 let item = item.to_followable_item_handle(cx)?;
4451 if item.remote_id(&client, window, cx) == Some(id) {
4452 Some(item)
4453 } else {
4454 None
4455 }
4456 })
4457 })?;
4458 let item = if let Some(existing_item) = existing_item {
4459 existing_item
4460 } else {
4461 let variant = view.variant.clone();
4462 anyhow::ensure!(variant.is_some(), "missing view variant");
4463
4464 let task = cx.update(|window, cx| {
4465 FollowableViewRegistry::from_state_proto(this.clone(), id, variant, window, cx)
4466 })?;
4467
4468 let Some(task) = task else {
4469 anyhow::bail!(
4470 "failed to construct view from leader (maybe from a different version of zed?)"
4471 );
4472 };
4473
4474 let mut new_item = task.await?;
4475 pane.update_in(cx, |pane, window, cx| {
4476 let mut item_to_remove = None;
4477 for (ix, item) in pane.items().enumerate() {
4478 if let Some(item) = item.to_followable_item_handle(cx) {
4479 match new_item.dedup(item.as_ref(), window, cx) {
4480 Some(item::Dedup::KeepExisting) => {
4481 new_item =
4482 item.boxed_clone().to_followable_item_handle(cx).unwrap();
4483 break;
4484 }
4485 Some(item::Dedup::ReplaceExisting) => {
4486 item_to_remove = Some((ix, item.item_id()));
4487 break;
4488 }
4489 None => {}
4490 }
4491 }
4492 }
4493
4494 if let Some((ix, id)) = item_to_remove {
4495 pane.remove_item(id, false, false, window, cx);
4496 pane.add_item(new_item.boxed_clone(), false, false, Some(ix), window, cx);
4497 }
4498 })?;
4499
4500 new_item
4501 };
4502
4503 this.update_in(cx, |this, window, cx| {
4504 let state = this.follower_states.get_mut(&leader_id.into())?;
4505 item.set_leader_id(Some(leader_id.into()), window, cx);
4506 state.items_by_leader_view_id.insert(
4507 id,
4508 FollowerView {
4509 view: item,
4510 location: panel_id,
4511 },
4512 );
4513
4514 Some(())
4515 })?;
4516
4517 Ok(())
4518 }
4519
4520 fn handle_agent_location_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4521 let Some(follower_state) = self.follower_states.get_mut(&CollaboratorId::Agent) else {
4522 return;
4523 };
4524
4525 if let Some(agent_location) = self.project.read(cx).agent_location() {
4526 let buffer_entity_id = agent_location.buffer.entity_id();
4527 let view_id = ViewId {
4528 creator: CollaboratorId::Agent,
4529 id: buffer_entity_id.as_u64(),
4530 };
4531 follower_state.active_view_id = Some(view_id);
4532
4533 let item = match follower_state.items_by_leader_view_id.entry(view_id) {
4534 hash_map::Entry::Occupied(entry) => Some(entry.into_mut()),
4535 hash_map::Entry::Vacant(entry) => {
4536 let existing_view =
4537 follower_state
4538 .center_pane
4539 .read(cx)
4540 .items()
4541 .find_map(|item| {
4542 let item = item.to_followable_item_handle(cx)?;
4543 if item.is_singleton(cx)
4544 && item.project_item_model_ids(cx).as_slice()
4545 == [buffer_entity_id]
4546 {
4547 Some(item)
4548 } else {
4549 None
4550 }
4551 });
4552 let view = existing_view.or_else(|| {
4553 agent_location.buffer.upgrade().and_then(|buffer| {
4554 cx.update_default_global(|registry: &mut ProjectItemRegistry, cx| {
4555 registry.build_item(buffer, self.project.clone(), None, window, cx)
4556 })?
4557 .to_followable_item_handle(cx)
4558 })
4559 });
4560
4561 if let Some(view) = view {
4562 Some(entry.insert(FollowerView {
4563 view,
4564 location: None,
4565 }))
4566 } else {
4567 None
4568 }
4569 }
4570 };
4571
4572 if let Some(item) = item {
4573 item.view
4574 .set_leader_id(Some(CollaboratorId::Agent), window, cx);
4575 item.view
4576 .update_agent_location(agent_location.position, window, cx);
4577 }
4578 } else {
4579 follower_state.active_view_id = None;
4580 }
4581
4582 self.leader_updated(CollaboratorId::Agent, window, cx);
4583 }
4584
4585 pub fn update_active_view_for_followers(&mut self, window: &mut Window, cx: &mut App) {
4586 let mut is_project_item = true;
4587 let mut update = proto::UpdateActiveView::default();
4588 if window.is_window_active() {
4589 let (active_item, panel_id) = self.active_item_for_followers(window, cx);
4590
4591 if let Some(item) = active_item {
4592 if item.item_focus_handle(cx).contains_focused(window, cx) {
4593 let leader_id = self
4594 .pane_for(&*item)
4595 .and_then(|pane| self.leader_for_pane(&pane));
4596 let leader_peer_id = match leader_id {
4597 Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
4598 Some(CollaboratorId::Agent) | None => None,
4599 };
4600
4601 if let Some(item) = item.to_followable_item_handle(cx) {
4602 let id = item
4603 .remote_id(&self.app_state.client, window, cx)
4604 .map(|id| id.to_proto());
4605
4606 if let Some(id) = id.clone() {
4607 if let Some(variant) = item.to_state_proto(window, cx) {
4608 let view = Some(proto::View {
4609 id: id.clone(),
4610 leader_id: leader_peer_id,
4611 variant: Some(variant),
4612 panel_id: panel_id.map(|id| id as i32),
4613 });
4614
4615 is_project_item = item.is_project_item(window, cx);
4616 update = proto::UpdateActiveView {
4617 view,
4618 // TODO: Remove after version 0.145.x stabilizes.
4619 id: id.clone(),
4620 leader_id: leader_peer_id,
4621 };
4622 }
4623 };
4624 }
4625 }
4626 }
4627 }
4628
4629 let active_view_id = update.view.as_ref().and_then(|view| view.id.as_ref());
4630 if active_view_id != self.last_active_view_id.as_ref() {
4631 self.last_active_view_id = active_view_id.cloned();
4632 self.update_followers(
4633 is_project_item,
4634 proto::update_followers::Variant::UpdateActiveView(update),
4635 window,
4636 cx,
4637 );
4638 }
4639 }
4640
4641 fn active_item_for_followers(
4642 &self,
4643 window: &mut Window,
4644 cx: &mut App,
4645 ) -> (Option<Box<dyn ItemHandle>>, Option<proto::PanelId>) {
4646 let mut active_item = None;
4647 let mut panel_id = None;
4648 for dock in self.all_docks() {
4649 if dock.focus_handle(cx).contains_focused(window, cx) {
4650 if let Some(panel) = dock.read(cx).active_panel() {
4651 if let Some(pane) = panel.pane(cx) {
4652 if let Some(item) = pane.read(cx).active_item() {
4653 active_item = Some(item);
4654 panel_id = panel.remote_id();
4655 break;
4656 }
4657 }
4658 }
4659 }
4660 }
4661
4662 if active_item.is_none() {
4663 active_item = self.active_pane().read(cx).active_item();
4664 }
4665 (active_item, panel_id)
4666 }
4667
4668 fn update_followers(
4669 &self,
4670 project_only: bool,
4671 update: proto::update_followers::Variant,
4672 _: &mut Window,
4673 cx: &mut App,
4674 ) -> Option<()> {
4675 // If this update only applies to for followers in the current project,
4676 // then skip it unless this project is shared. If it applies to all
4677 // followers, regardless of project, then set `project_id` to none,
4678 // indicating that it goes to all followers.
4679 let project_id = if project_only {
4680 Some(self.project.read(cx).remote_id()?)
4681 } else {
4682 None
4683 };
4684 self.app_state().workspace_store.update(cx, |store, cx| {
4685 store.update_followers(project_id, update, cx)
4686 })
4687 }
4688
4689 pub fn leader_for_pane(&self, pane: &Entity<Pane>) -> Option<CollaboratorId> {
4690 self.follower_states.iter().find_map(|(leader_id, state)| {
4691 if state.center_pane == *pane || state.dock_pane.as_ref() == Some(pane) {
4692 Some(*leader_id)
4693 } else {
4694 None
4695 }
4696 })
4697 }
4698
4699 fn leader_updated(
4700 &mut self,
4701 leader_id: impl Into<CollaboratorId>,
4702 window: &mut Window,
4703 cx: &mut Context<Self>,
4704 ) -> Option<Box<dyn ItemHandle>> {
4705 cx.notify();
4706
4707 let leader_id = leader_id.into();
4708 let (panel_id, item) = match leader_id {
4709 CollaboratorId::PeerId(peer_id) => self.active_item_for_peer(peer_id, window, cx)?,
4710 CollaboratorId::Agent => (None, self.active_item_for_agent()?),
4711 };
4712
4713 let state = self.follower_states.get(&leader_id)?;
4714 let mut transfer_focus = state.center_pane.read(cx).has_focus(window, cx);
4715 let pane;
4716 if let Some(panel_id) = panel_id {
4717 pane = self
4718 .activate_panel_for_proto_id(panel_id, window, cx)?
4719 .pane(cx)?;
4720 let state = self.follower_states.get_mut(&leader_id)?;
4721 state.dock_pane = Some(pane.clone());
4722 } else {
4723 pane = state.center_pane.clone();
4724 let state = self.follower_states.get_mut(&leader_id)?;
4725 if let Some(dock_pane) = state.dock_pane.take() {
4726 transfer_focus |= dock_pane.focus_handle(cx).contains_focused(window, cx);
4727 }
4728 }
4729
4730 pane.update(cx, |pane, cx| {
4731 let focus_active_item = pane.has_focus(window, cx) || transfer_focus;
4732 if let Some(index) = pane.index_for_item(item.as_ref()) {
4733 pane.activate_item(index, false, false, window, cx);
4734 } else {
4735 pane.add_item(item.boxed_clone(), false, false, None, window, cx)
4736 }
4737
4738 if focus_active_item {
4739 pane.focus_active_item(window, cx)
4740 }
4741 });
4742
4743 Some(item)
4744 }
4745
4746 fn active_item_for_agent(&self) -> Option<Box<dyn ItemHandle>> {
4747 let state = self.follower_states.get(&CollaboratorId::Agent)?;
4748 let active_view_id = state.active_view_id?;
4749 Some(
4750 state
4751 .items_by_leader_view_id
4752 .get(&active_view_id)?
4753 .view
4754 .boxed_clone(),
4755 )
4756 }
4757
4758 fn active_item_for_peer(
4759 &self,
4760 peer_id: PeerId,
4761 window: &mut Window,
4762 cx: &mut Context<Self>,
4763 ) -> Option<(Option<PanelId>, Box<dyn ItemHandle>)> {
4764 let call = self.active_call()?;
4765 let room = call.read(cx).room()?.read(cx);
4766 let participant = room.remote_participant_for_peer_id(peer_id)?;
4767 let leader_in_this_app;
4768 let leader_in_this_project;
4769 match participant.location {
4770 call::ParticipantLocation::SharedProject { project_id } => {
4771 leader_in_this_app = true;
4772 leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
4773 }
4774 call::ParticipantLocation::UnsharedProject => {
4775 leader_in_this_app = true;
4776 leader_in_this_project = false;
4777 }
4778 call::ParticipantLocation::External => {
4779 leader_in_this_app = false;
4780 leader_in_this_project = false;
4781 }
4782 };
4783 let state = self.follower_states.get(&peer_id.into())?;
4784 let mut item_to_activate = None;
4785 if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
4786 if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
4787 if leader_in_this_project || !item.view.is_project_item(window, cx) {
4788 item_to_activate = Some((item.location, item.view.boxed_clone()));
4789 }
4790 }
4791 } else if let Some(shared_screen) =
4792 self.shared_screen_for_peer(peer_id, &state.center_pane, window, cx)
4793 {
4794 item_to_activate = Some((None, Box::new(shared_screen)));
4795 }
4796 item_to_activate
4797 }
4798
4799 fn shared_screen_for_peer(
4800 &self,
4801 peer_id: PeerId,
4802 pane: &Entity<Pane>,
4803 window: &mut Window,
4804 cx: &mut App,
4805 ) -> Option<Entity<SharedScreen>> {
4806 let call = self.active_call()?;
4807 let room = call.read(cx).room()?.clone();
4808 let participant = room.read(cx).remote_participant_for_peer_id(peer_id)?;
4809 let track = participant.video_tracks.values().next()?.clone();
4810 let user = participant.user.clone();
4811
4812 for item in pane.read(cx).items_of_type::<SharedScreen>() {
4813 if item.read(cx).peer_id == peer_id {
4814 return Some(item);
4815 }
4816 }
4817
4818 Some(cx.new(|cx| SharedScreen::new(track, peer_id, user.clone(), room.clone(), window, cx)))
4819 }
4820
4821 pub fn on_window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4822 if window.is_window_active() {
4823 self.update_active_view_for_followers(window, cx);
4824
4825 if let Some(database_id) = self.database_id {
4826 cx.background_spawn(persistence::DB.update_timestamp(database_id))
4827 .detach();
4828 }
4829 } else {
4830 for pane in &self.panes {
4831 pane.update(cx, |pane, cx| {
4832 if let Some(item) = pane.active_item() {
4833 item.workspace_deactivated(window, cx);
4834 }
4835 for item in pane.items() {
4836 if matches!(
4837 item.workspace_settings(cx).autosave,
4838 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
4839 ) {
4840 Pane::autosave_item(item.as_ref(), self.project.clone(), window, cx)
4841 .detach_and_log_err(cx);
4842 }
4843 }
4844 });
4845 }
4846 }
4847 }
4848
4849 pub fn active_call(&self) -> Option<&Entity<ActiveCall>> {
4850 self.active_call.as_ref().map(|(call, _)| call)
4851 }
4852
4853 fn on_active_call_event(
4854 &mut self,
4855 _: &Entity<ActiveCall>,
4856 event: &call::room::Event,
4857 window: &mut Window,
4858 cx: &mut Context<Self>,
4859 ) {
4860 match event {
4861 call::room::Event::ParticipantLocationChanged { participant_id }
4862 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
4863 self.leader_updated(participant_id, window, cx);
4864 }
4865 _ => {}
4866 }
4867 }
4868
4869 pub fn database_id(&self) -> Option<WorkspaceId> {
4870 self.database_id
4871 }
4872
4873 pub fn session_id(&self) -> Option<String> {
4874 self.session_id.clone()
4875 }
4876
4877 fn local_paths(&self, cx: &App) -> Option<Vec<Arc<Path>>> {
4878 let project = self.project().read(cx);
4879
4880 if project.is_local() {
4881 Some(
4882 project
4883 .visible_worktrees(cx)
4884 .map(|worktree| worktree.read(cx).abs_path())
4885 .collect::<Vec<_>>(),
4886 )
4887 } else {
4888 None
4889 }
4890 }
4891
4892 fn remove_panes(&mut self, member: Member, window: &mut Window, cx: &mut Context<Workspace>) {
4893 match member {
4894 Member::Axis(PaneAxis { members, .. }) => {
4895 for child in members.iter() {
4896 self.remove_panes(child.clone(), window, cx)
4897 }
4898 }
4899 Member::Pane(pane) => {
4900 self.force_remove_pane(&pane, &None, window, cx);
4901 }
4902 }
4903 }
4904
4905 fn remove_from_session(&mut self, window: &mut Window, cx: &mut App) -> Task<()> {
4906 self.session_id.take();
4907 self.serialize_workspace_internal(window, cx)
4908 }
4909
4910 fn force_remove_pane(
4911 &mut self,
4912 pane: &Entity<Pane>,
4913 focus_on: &Option<Entity<Pane>>,
4914 window: &mut Window,
4915 cx: &mut Context<Workspace>,
4916 ) {
4917 self.panes.retain(|p| p != pane);
4918 if let Some(focus_on) = focus_on {
4919 focus_on.update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
4920 } else {
4921 if self.active_pane() == pane {
4922 self.panes
4923 .last()
4924 .unwrap()
4925 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
4926 }
4927 }
4928 if self.last_active_center_pane == Some(pane.downgrade()) {
4929 self.last_active_center_pane = None;
4930 }
4931 cx.notify();
4932 }
4933
4934 fn serialize_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4935 if self._schedule_serialize.is_none() {
4936 self._schedule_serialize = Some(cx.spawn_in(window, async move |this, cx| {
4937 cx.background_executor()
4938 .timer(Duration::from_millis(100))
4939 .await;
4940 this.update_in(cx, |this, window, cx| {
4941 this.serialize_workspace_internal(window, cx).detach();
4942 this._schedule_serialize.take();
4943 })
4944 .log_err();
4945 }));
4946 }
4947 }
4948
4949 fn serialize_workspace_internal(&self, window: &mut Window, cx: &mut App) -> Task<()> {
4950 let Some(database_id) = self.database_id() else {
4951 return Task::ready(());
4952 };
4953
4954 fn serialize_pane_handle(
4955 pane_handle: &Entity<Pane>,
4956 window: &mut Window,
4957 cx: &mut App,
4958 ) -> SerializedPane {
4959 let (items, active, pinned_count) = {
4960 let pane = pane_handle.read(cx);
4961 let active_item_id = pane.active_item().map(|item| item.item_id());
4962 (
4963 pane.items()
4964 .filter_map(|handle| {
4965 let handle = handle.to_serializable_item_handle(cx)?;
4966
4967 Some(SerializedItem {
4968 kind: Arc::from(handle.serialized_item_kind()),
4969 item_id: handle.item_id().as_u64(),
4970 active: Some(handle.item_id()) == active_item_id,
4971 preview: pane.is_active_preview_item(handle.item_id()),
4972 })
4973 })
4974 .collect::<Vec<_>>(),
4975 pane.has_focus(window, cx),
4976 pane.pinned_count(),
4977 )
4978 };
4979
4980 SerializedPane::new(items, active, pinned_count)
4981 }
4982
4983 fn build_serialized_pane_group(
4984 pane_group: &Member,
4985 window: &mut Window,
4986 cx: &mut App,
4987 ) -> SerializedPaneGroup {
4988 match pane_group {
4989 Member::Axis(PaneAxis {
4990 axis,
4991 members,
4992 flexes,
4993 bounding_boxes: _,
4994 }) => SerializedPaneGroup::Group {
4995 axis: SerializedAxis(*axis),
4996 children: members
4997 .iter()
4998 .map(|member| build_serialized_pane_group(member, window, cx))
4999 .collect::<Vec<_>>(),
5000 flexes: Some(flexes.lock().clone()),
5001 },
5002 Member::Pane(pane_handle) => {
5003 SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, window, cx))
5004 }
5005 }
5006 }
5007
5008 fn build_serialized_docks(
5009 this: &Workspace,
5010 window: &mut Window,
5011 cx: &mut App,
5012 ) -> DockStructure {
5013 let left_dock = this.left_dock.read(cx);
5014 let left_visible = left_dock.is_open();
5015 let left_active_panel = left_dock
5016 .active_panel()
5017 .map(|panel| panel.persistent_name().to_string());
5018 let left_dock_zoom = left_dock
5019 .active_panel()
5020 .map(|panel| panel.is_zoomed(window, cx))
5021 .unwrap_or(false);
5022
5023 let right_dock = this.right_dock.read(cx);
5024 let right_visible = right_dock.is_open();
5025 let right_active_panel = right_dock
5026 .active_panel()
5027 .map(|panel| panel.persistent_name().to_string());
5028 let right_dock_zoom = right_dock
5029 .active_panel()
5030 .map(|panel| panel.is_zoomed(window, cx))
5031 .unwrap_or(false);
5032
5033 let bottom_dock = this.bottom_dock.read(cx);
5034 let bottom_visible = bottom_dock.is_open();
5035 let bottom_active_panel = bottom_dock
5036 .active_panel()
5037 .map(|panel| panel.persistent_name().to_string());
5038 let bottom_dock_zoom = bottom_dock
5039 .active_panel()
5040 .map(|panel| panel.is_zoomed(window, cx))
5041 .unwrap_or(false);
5042
5043 DockStructure {
5044 left: DockData {
5045 visible: left_visible,
5046 active_panel: left_active_panel,
5047 zoom: left_dock_zoom,
5048 },
5049 right: DockData {
5050 visible: right_visible,
5051 active_panel: right_active_panel,
5052 zoom: right_dock_zoom,
5053 },
5054 bottom: DockData {
5055 visible: bottom_visible,
5056 active_panel: bottom_active_panel,
5057 zoom: bottom_dock_zoom,
5058 },
5059 }
5060 }
5061
5062 if let Some(location) = self.serialize_workspace_location(cx) {
5063 let breakpoints = self.project.update(cx, |project, cx| {
5064 project
5065 .breakpoint_store()
5066 .read(cx)
5067 .all_source_breakpoints(cx)
5068 });
5069
5070 let center_group = build_serialized_pane_group(&self.center.root, window, cx);
5071 let docks = build_serialized_docks(self, window, cx);
5072 let window_bounds = Some(SerializedWindowBounds(window.window_bounds()));
5073 let serialized_workspace = SerializedWorkspace {
5074 id: database_id,
5075 location,
5076 center_group,
5077 window_bounds,
5078 display: Default::default(),
5079 docks,
5080 centered_layout: self.centered_layout,
5081 session_id: self.session_id.clone(),
5082 breakpoints,
5083 window_id: Some(window.window_handle().window_id().as_u64()),
5084 };
5085
5086 return window.spawn(cx, async move |_| {
5087 persistence::DB.save_workspace(serialized_workspace).await;
5088 });
5089 }
5090 Task::ready(())
5091 }
5092
5093 fn serialize_workspace_location(&self, cx: &App) -> Option<SerializedWorkspaceLocation> {
5094 if let Some(ssh_project) = &self.serialized_ssh_project {
5095 Some(SerializedWorkspaceLocation::Ssh(ssh_project.clone()))
5096 } else if let Some(local_paths) = self.local_paths(cx) {
5097 if !local_paths.is_empty() {
5098 Some(SerializedWorkspaceLocation::from_local_paths(local_paths))
5099 } else {
5100 None
5101 }
5102 } else {
5103 None
5104 }
5105 }
5106
5107 fn update_history(&self, cx: &mut App) {
5108 let Some(id) = self.database_id() else {
5109 return;
5110 };
5111 let Some(location) = self.serialize_workspace_location(cx) else {
5112 return;
5113 };
5114 if let Some(manager) = HistoryManager::global(cx) {
5115 manager.update(cx, |this, cx| {
5116 this.update_history(id, HistoryManagerEntry::new(id, &location), cx);
5117 });
5118 }
5119 }
5120
5121 async fn serialize_items(
5122 this: &WeakEntity<Self>,
5123 items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
5124 cx: &mut AsyncWindowContext,
5125 ) -> Result<()> {
5126 const CHUNK_SIZE: usize = 200;
5127
5128 let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
5129
5130 while let Some(items_received) = serializable_items.next().await {
5131 let unique_items =
5132 items_received
5133 .into_iter()
5134 .fold(HashMap::default(), |mut acc, item| {
5135 acc.entry(item.item_id()).or_insert(item);
5136 acc
5137 });
5138
5139 // We use into_iter() here so that the references to the items are moved into
5140 // the tasks and not kept alive while we're sleeping.
5141 for (_, item) in unique_items.into_iter() {
5142 if let Ok(Some(task)) = this.update_in(cx, |workspace, window, cx| {
5143 item.serialize(workspace, false, window, cx)
5144 }) {
5145 cx.background_spawn(async move { task.await.log_err() })
5146 .detach();
5147 }
5148 }
5149
5150 cx.background_executor()
5151 .timer(SERIALIZATION_THROTTLE_TIME)
5152 .await;
5153 }
5154
5155 Ok(())
5156 }
5157
5158 pub(crate) fn enqueue_item_serialization(
5159 &mut self,
5160 item: Box<dyn SerializableItemHandle>,
5161 ) -> Result<()> {
5162 self.serializable_items_tx
5163 .unbounded_send(item)
5164 .map_err(|err| anyhow!("failed to send serializable item over channel: {err}"))
5165 }
5166
5167 pub(crate) fn load_workspace(
5168 serialized_workspace: SerializedWorkspace,
5169 paths_to_open: Vec<Option<ProjectPath>>,
5170 window: &mut Window,
5171 cx: &mut Context<Workspace>,
5172 ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
5173 cx.spawn_in(window, async move |workspace, cx| {
5174 let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?;
5175
5176 let mut center_group = None;
5177 let mut center_items = None;
5178
5179 // Traverse the splits tree and add to things
5180 if let Some((group, active_pane, items)) = serialized_workspace
5181 .center_group
5182 .deserialize(&project, serialized_workspace.id, workspace.clone(), cx)
5183 .await
5184 {
5185 center_items = Some(items);
5186 center_group = Some((group, active_pane))
5187 }
5188
5189 let mut items_by_project_path = HashMap::default();
5190 let mut item_ids_by_kind = HashMap::default();
5191 let mut all_deserialized_items = Vec::default();
5192 cx.update(|_, cx| {
5193 for item in center_items.unwrap_or_default().into_iter().flatten() {
5194 if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) {
5195 item_ids_by_kind
5196 .entry(serializable_item_handle.serialized_item_kind())
5197 .or_insert(Vec::new())
5198 .push(item.item_id().as_u64() as ItemId);
5199 }
5200
5201 if let Some(project_path) = item.project_path(cx) {
5202 items_by_project_path.insert(project_path, item.clone());
5203 }
5204 all_deserialized_items.push(item);
5205 }
5206 })?;
5207
5208 let opened_items = paths_to_open
5209 .into_iter()
5210 .map(|path_to_open| {
5211 path_to_open
5212 .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
5213 })
5214 .collect::<Vec<_>>();
5215
5216 // Remove old panes from workspace panes list
5217 workspace.update_in(cx, |workspace, window, cx| {
5218 if let Some((center_group, active_pane)) = center_group {
5219 workspace.remove_panes(workspace.center.root.clone(), window, cx);
5220
5221 // Swap workspace center group
5222 workspace.center = PaneGroup::with_root(center_group);
5223 if let Some(active_pane) = active_pane {
5224 workspace.set_active_pane(&active_pane, window, cx);
5225 cx.focus_self(window);
5226 } else {
5227 workspace.set_active_pane(&workspace.center.first_pane(), window, cx);
5228 }
5229 }
5230
5231 let docks = serialized_workspace.docks;
5232
5233 for (dock, serialized_dock) in [
5234 (&mut workspace.right_dock, docks.right),
5235 (&mut workspace.left_dock, docks.left),
5236 (&mut workspace.bottom_dock, docks.bottom),
5237 ]
5238 .iter_mut()
5239 {
5240 dock.update(cx, |dock, cx| {
5241 dock.serialized_dock = Some(serialized_dock.clone());
5242 dock.restore_state(window, cx);
5243 });
5244 }
5245
5246 cx.notify();
5247 })?;
5248
5249 let _ = project
5250 .update(cx, |project, cx| {
5251 project
5252 .breakpoint_store()
5253 .update(cx, |breakpoint_store, cx| {
5254 breakpoint_store
5255 .with_serialized_breakpoints(serialized_workspace.breakpoints, cx)
5256 })
5257 })?
5258 .await;
5259
5260 // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means
5261 // after loading the items, we might have different items and in order to avoid
5262 // the database filling up, we delete items that haven't been loaded now.
5263 //
5264 // The items that have been loaded, have been saved after they've been added to the workspace.
5265 let clean_up_tasks = workspace.update_in(cx, |_, window, cx| {
5266 item_ids_by_kind
5267 .into_iter()
5268 .map(|(item_kind, loaded_items)| {
5269 SerializableItemRegistry::cleanup(
5270 item_kind,
5271 serialized_workspace.id,
5272 loaded_items,
5273 window,
5274 cx,
5275 )
5276 .log_err()
5277 })
5278 .collect::<Vec<_>>()
5279 })?;
5280
5281 futures::future::join_all(clean_up_tasks).await;
5282
5283 workspace
5284 .update_in(cx, |workspace, window, cx| {
5285 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
5286 workspace.serialize_workspace_internal(window, cx).detach();
5287
5288 // Ensure that we mark the window as edited if we did load dirty items
5289 workspace.update_window_edited(window, cx);
5290 })
5291 .ok();
5292
5293 Ok(opened_items)
5294 })
5295 }
5296
5297 fn actions(&self, div: Div, window: &mut Window, cx: &mut Context<Self>) -> Div {
5298 self.add_workspace_actions_listeners(div, window, cx)
5299 .on_action(cx.listener(Self::close_inactive_items_and_panes))
5300 .on_action(cx.listener(Self::close_all_items_and_panes))
5301 .on_action(cx.listener(Self::save_all))
5302 .on_action(cx.listener(Self::send_keystrokes))
5303 .on_action(cx.listener(Self::add_folder_to_project))
5304 .on_action(cx.listener(Self::follow_next_collaborator))
5305 .on_action(cx.listener(Self::close_window))
5306 .on_action(cx.listener(Self::activate_pane_at_index))
5307 .on_action(cx.listener(Self::move_item_to_pane_at_index))
5308 .on_action(cx.listener(Self::move_focused_panel_to_next_position))
5309 .on_action(cx.listener(|workspace, _: &Unfollow, window, cx| {
5310 let pane = workspace.active_pane().clone();
5311 workspace.unfollow_in_pane(&pane, window, cx);
5312 }))
5313 .on_action(cx.listener(|workspace, action: &Save, window, cx| {
5314 workspace
5315 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), window, cx)
5316 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
5317 }))
5318 .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, window, cx| {
5319 workspace
5320 .save_active_item(SaveIntent::SaveWithoutFormat, window, cx)
5321 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
5322 }))
5323 .on_action(cx.listener(|workspace, _: &SaveAs, window, cx| {
5324 workspace
5325 .save_active_item(SaveIntent::SaveAs, window, cx)
5326 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
5327 }))
5328 .on_action(
5329 cx.listener(|workspace, _: &ActivatePreviousPane, window, cx| {
5330 workspace.activate_previous_pane(window, cx)
5331 }),
5332 )
5333 .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
5334 workspace.activate_next_pane(window, cx)
5335 }))
5336 .on_action(
5337 cx.listener(|workspace, _: &ActivateNextWindow, _window, cx| {
5338 workspace.activate_next_window(cx)
5339 }),
5340 )
5341 .on_action(
5342 cx.listener(|workspace, _: &ActivatePreviousWindow, _window, cx| {
5343 workspace.activate_previous_window(cx)
5344 }),
5345 )
5346 .on_action(cx.listener(|workspace, _: &ActivatePaneLeft, window, cx| {
5347 workspace.activate_pane_in_direction(SplitDirection::Left, window, cx)
5348 }))
5349 .on_action(cx.listener(|workspace, _: &ActivatePaneRight, window, cx| {
5350 workspace.activate_pane_in_direction(SplitDirection::Right, window, cx)
5351 }))
5352 .on_action(cx.listener(|workspace, _: &ActivatePaneUp, window, cx| {
5353 workspace.activate_pane_in_direction(SplitDirection::Up, window, cx)
5354 }))
5355 .on_action(cx.listener(|workspace, _: &ActivatePaneDown, window, cx| {
5356 workspace.activate_pane_in_direction(SplitDirection::Down, window, cx)
5357 }))
5358 .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
5359 workspace.activate_next_pane(window, cx)
5360 }))
5361 .on_action(cx.listener(
5362 |workspace, action: &MoveItemToPaneInDirection, window, cx| {
5363 workspace.move_item_to_pane_in_direction(action, window, cx)
5364 },
5365 ))
5366 .on_action(cx.listener(|workspace, _: &SwapPaneLeft, _, cx| {
5367 workspace.swap_pane_in_direction(SplitDirection::Left, cx)
5368 }))
5369 .on_action(cx.listener(|workspace, _: &SwapPaneRight, _, cx| {
5370 workspace.swap_pane_in_direction(SplitDirection::Right, cx)
5371 }))
5372 .on_action(cx.listener(|workspace, _: &SwapPaneUp, _, cx| {
5373 workspace.swap_pane_in_direction(SplitDirection::Up, cx)
5374 }))
5375 .on_action(cx.listener(|workspace, _: &SwapPaneDown, _, cx| {
5376 workspace.swap_pane_in_direction(SplitDirection::Down, cx)
5377 }))
5378 .on_action(cx.listener(|this, _: &ToggleLeftDock, window, cx| {
5379 this.toggle_dock(DockPosition::Left, window, cx);
5380 }))
5381 .on_action(cx.listener(
5382 |workspace: &mut Workspace, _: &ToggleRightDock, window, cx| {
5383 workspace.toggle_dock(DockPosition::Right, window, cx);
5384 },
5385 ))
5386 .on_action(cx.listener(
5387 |workspace: &mut Workspace, _: &ToggleBottomDock, window, cx| {
5388 workspace.toggle_dock(DockPosition::Bottom, window, cx);
5389 },
5390 ))
5391 .on_action(cx.listener(
5392 |workspace: &mut Workspace, _: &CloseActiveDock, window, cx| {
5393 workspace.close_active_dock(window, cx);
5394 },
5395 ))
5396 .on_action(
5397 cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, window, cx| {
5398 workspace.close_all_docks(window, cx);
5399 }),
5400 )
5401 .on_action(cx.listener(
5402 |workspace: &mut Workspace, _: &ClearAllNotifications, _, cx| {
5403 workspace.clear_all_notifications(cx);
5404 },
5405 ))
5406 .on_action(cx.listener(
5407 |workspace: &mut Workspace, _: &SuppressNotification, _, cx| {
5408 if let Some((notification_id, _)) = workspace.notifications.pop() {
5409 workspace.suppress_notification(¬ification_id, cx);
5410 }
5411 },
5412 ))
5413 .on_action(cx.listener(
5414 |workspace: &mut Workspace, _: &ReopenClosedItem, window, cx| {
5415 workspace.reopen_closed_item(window, cx).detach();
5416 },
5417 ))
5418 .on_action(cx.listener(Workspace::toggle_centered_layout))
5419 .on_action(cx.listener(Workspace::cancel))
5420 }
5421
5422 #[cfg(any(test, feature = "test-support"))]
5423 pub fn test_new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
5424 use node_runtime::NodeRuntime;
5425 use session::Session;
5426
5427 let client = project.read(cx).client();
5428 let user_store = project.read(cx).user_store();
5429
5430 let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
5431 let session = cx.new(|cx| AppSession::new(Session::test(), cx));
5432 window.activate_window();
5433 let app_state = Arc::new(AppState {
5434 languages: project.read(cx).languages().clone(),
5435 workspace_store,
5436 client,
5437 user_store,
5438 fs: project.read(cx).fs().clone(),
5439 build_window_options: |_, _| Default::default(),
5440 node_runtime: NodeRuntime::unavailable(),
5441 session,
5442 });
5443 let workspace = Self::new(Default::default(), project, app_state, window, cx);
5444 workspace
5445 .active_pane
5446 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
5447 workspace
5448 }
5449
5450 pub fn register_action<A: Action>(
5451 &mut self,
5452 callback: impl Fn(&mut Self, &A, &mut Window, &mut Context<Self>) + 'static,
5453 ) -> &mut Self {
5454 let callback = Arc::new(callback);
5455
5456 self.workspace_actions.push(Box::new(move |div, _, cx| {
5457 let callback = callback.clone();
5458 div.on_action(cx.listener(move |workspace, event, window, cx| {
5459 (callback)(workspace, event, window, cx)
5460 }))
5461 }));
5462 self
5463 }
5464
5465 fn add_workspace_actions_listeners(
5466 &self,
5467 mut div: Div,
5468 window: &mut Window,
5469 cx: &mut Context<Self>,
5470 ) -> Div {
5471 for action in self.workspace_actions.iter() {
5472 div = (action)(div, window, cx)
5473 }
5474 div
5475 }
5476
5477 pub fn has_active_modal(&self, _: &mut Window, cx: &mut App) -> bool {
5478 self.modal_layer.read(cx).has_active_modal()
5479 }
5480
5481 pub fn active_modal<V: ManagedView + 'static>(&self, cx: &App) -> Option<Entity<V>> {
5482 self.modal_layer.read(cx).active_modal()
5483 }
5484
5485 pub fn toggle_modal<V: ModalView, B>(&mut self, window: &mut Window, cx: &mut App, build: B)
5486 where
5487 B: FnOnce(&mut Window, &mut Context<V>) -> V,
5488 {
5489 self.modal_layer.update(cx, |modal_layer, cx| {
5490 modal_layer.toggle_modal(window, cx, build)
5491 })
5492 }
5493
5494 pub fn toggle_status_toast<V: ToastView>(&mut self, entity: Entity<V>, cx: &mut App) {
5495 self.toast_layer
5496 .update(cx, |toast_layer, cx| toast_layer.toggle_toast(cx, entity))
5497 }
5498
5499 pub fn toggle_centered_layout(
5500 &mut self,
5501 _: &ToggleCenteredLayout,
5502 _: &mut Window,
5503 cx: &mut Context<Self>,
5504 ) {
5505 self.centered_layout = !self.centered_layout;
5506 if let Some(database_id) = self.database_id() {
5507 cx.background_spawn(DB.set_centered_layout(database_id, self.centered_layout))
5508 .detach_and_log_err(cx);
5509 }
5510 cx.notify();
5511 }
5512
5513 fn adjust_padding(padding: Option<f32>) -> f32 {
5514 padding
5515 .unwrap_or(Self::DEFAULT_PADDING)
5516 .clamp(0.0, Self::MAX_PADDING)
5517 }
5518
5519 fn render_dock(
5520 &self,
5521 position: DockPosition,
5522 dock: &Entity<Dock>,
5523 window: &mut Window,
5524 cx: &mut App,
5525 ) -> Option<Div> {
5526 if self.zoomed_position == Some(position) {
5527 return None;
5528 }
5529
5530 let leader_border = dock.read(cx).active_panel().and_then(|panel| {
5531 let pane = panel.pane(cx)?;
5532 let follower_states = &self.follower_states;
5533 leader_border_for_pane(follower_states, &pane, window, cx)
5534 });
5535
5536 Some(
5537 div()
5538 .flex()
5539 .flex_none()
5540 .overflow_hidden()
5541 .child(dock.clone())
5542 .children(leader_border),
5543 )
5544 }
5545
5546 pub fn for_window(window: &mut Window, _: &mut App) -> Option<Entity<Workspace>> {
5547 window.root().flatten()
5548 }
5549
5550 pub fn zoomed_item(&self) -> Option<&AnyWeakView> {
5551 self.zoomed.as_ref()
5552 }
5553
5554 pub fn activate_next_window(&mut self, cx: &mut Context<Self>) {
5555 let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
5556 return;
5557 };
5558 let windows = cx.windows();
5559 let Some(next_window) = windows
5560 .iter()
5561 .cycle()
5562 .skip_while(|window| window.window_id() != current_window_id)
5563 .nth(1)
5564 else {
5565 return;
5566 };
5567 next_window
5568 .update(cx, |_, window, _| window.activate_window())
5569 .ok();
5570 }
5571
5572 pub fn activate_previous_window(&mut self, cx: &mut Context<Self>) {
5573 let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
5574 return;
5575 };
5576 let windows = cx.windows();
5577 let Some(prev_window) = windows
5578 .iter()
5579 .rev()
5580 .cycle()
5581 .skip_while(|window| window.window_id() != current_window_id)
5582 .nth(1)
5583 else {
5584 return;
5585 };
5586 prev_window
5587 .update(cx, |_, window, _| window.activate_window())
5588 .ok();
5589 }
5590
5591 pub fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
5592 if cx.stop_active_drag(window) {
5593 return;
5594 } else if let Some((notification_id, _)) = self.notifications.pop() {
5595 dismiss_app_notification(¬ification_id, cx);
5596 } else {
5597 cx.emit(Event::ClearActivityIndicator);
5598 cx.propagate();
5599 }
5600 }
5601}
5602
5603fn leader_border_for_pane(
5604 follower_states: &HashMap<CollaboratorId, FollowerState>,
5605 pane: &Entity<Pane>,
5606 _: &Window,
5607 cx: &App,
5608) -> Option<Div> {
5609 let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
5610 if state.pane() == pane {
5611 Some((*leader_id, state))
5612 } else {
5613 None
5614 }
5615 })?;
5616
5617 let mut leader_color = match leader_id {
5618 CollaboratorId::PeerId(leader_peer_id) => {
5619 let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
5620 let leader = room.remote_participant_for_peer_id(leader_peer_id)?;
5621
5622 cx.theme()
5623 .players()
5624 .color_for_participant(leader.participant_index.0)
5625 .cursor
5626 }
5627 CollaboratorId::Agent => cx.theme().players().agent().cursor,
5628 };
5629 leader_color.fade_out(0.3);
5630 Some(
5631 div()
5632 .absolute()
5633 .size_full()
5634 .left_0()
5635 .top_0()
5636 .border_2()
5637 .border_color(leader_color),
5638 )
5639}
5640
5641fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
5642 ZED_WINDOW_POSITION
5643 .zip(*ZED_WINDOW_SIZE)
5644 .map(|(position, size)| Bounds {
5645 origin: position,
5646 size,
5647 })
5648}
5649
5650fn open_items(
5651 serialized_workspace: Option<SerializedWorkspace>,
5652 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
5653 window: &mut Window,
5654 cx: &mut Context<Workspace>,
5655) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> + use<> {
5656 let restored_items = serialized_workspace.map(|serialized_workspace| {
5657 Workspace::load_workspace(
5658 serialized_workspace,
5659 project_paths_to_open
5660 .iter()
5661 .map(|(_, project_path)| project_path)
5662 .cloned()
5663 .collect(),
5664 window,
5665 cx,
5666 )
5667 });
5668
5669 cx.spawn_in(window, async move |workspace, cx| {
5670 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
5671
5672 if let Some(restored_items) = restored_items {
5673 let restored_items = restored_items.await?;
5674
5675 let restored_project_paths = restored_items
5676 .iter()
5677 .filter_map(|item| {
5678 cx.update(|_, cx| item.as_ref()?.project_path(cx))
5679 .ok()
5680 .flatten()
5681 })
5682 .collect::<HashSet<_>>();
5683
5684 for restored_item in restored_items {
5685 opened_items.push(restored_item.map(Ok));
5686 }
5687
5688 project_paths_to_open
5689 .iter_mut()
5690 .for_each(|(_, project_path)| {
5691 if let Some(project_path_to_open) = project_path {
5692 if restored_project_paths.contains(project_path_to_open) {
5693 *project_path = None;
5694 }
5695 }
5696 });
5697 } else {
5698 for _ in 0..project_paths_to_open.len() {
5699 opened_items.push(None);
5700 }
5701 }
5702 assert!(opened_items.len() == project_paths_to_open.len());
5703
5704 let tasks =
5705 project_paths_to_open
5706 .into_iter()
5707 .enumerate()
5708 .map(|(ix, (abs_path, project_path))| {
5709 let workspace = workspace.clone();
5710 cx.spawn(async move |cx| {
5711 let file_project_path = project_path?;
5712 let abs_path_task = workspace.update(cx, |workspace, cx| {
5713 workspace.project().update(cx, |project, cx| {
5714 project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
5715 })
5716 });
5717
5718 // We only want to open file paths here. If one of the items
5719 // here is a directory, it was already opened further above
5720 // with a `find_or_create_worktree`.
5721 if let Ok(task) = abs_path_task {
5722 if task.await.map_or(true, |p| p.is_file()) {
5723 return Some((
5724 ix,
5725 workspace
5726 .update_in(cx, |workspace, window, cx| {
5727 workspace.open_path(
5728 file_project_path,
5729 None,
5730 true,
5731 window,
5732 cx,
5733 )
5734 })
5735 .log_err()?
5736 .await,
5737 ));
5738 }
5739 }
5740 None
5741 })
5742 });
5743
5744 let tasks = tasks.collect::<Vec<_>>();
5745
5746 let tasks = futures::future::join_all(tasks);
5747 for (ix, path_open_result) in tasks.await.into_iter().flatten() {
5748 opened_items[ix] = Some(path_open_result);
5749 }
5750
5751 Ok(opened_items)
5752 })
5753}
5754
5755enum ActivateInDirectionTarget {
5756 Pane(Entity<Pane>),
5757 Dock(Entity<Dock>),
5758}
5759
5760fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncApp) {
5761 workspace
5762 .update(cx, |workspace, _, cx| {
5763 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
5764 struct DatabaseFailedNotification;
5765
5766 workspace.show_notification(
5767 NotificationId::unique::<DatabaseFailedNotification>(),
5768 cx,
5769 |cx| {
5770 cx.new(|cx| {
5771 MessageNotification::new("Failed to load the database file.", cx)
5772 .primary_message("File an Issue")
5773 .primary_icon(IconName::Plus)
5774 .primary_on_click(|window, cx| {
5775 window.dispatch_action(Box::new(FileBugReport), cx)
5776 })
5777 })
5778 },
5779 );
5780 }
5781 })
5782 .log_err();
5783}
5784
5785impl Focusable for Workspace {
5786 fn focus_handle(&self, cx: &App) -> FocusHandle {
5787 self.active_pane.focus_handle(cx)
5788 }
5789}
5790
5791#[derive(Clone)]
5792struct DraggedDock(DockPosition);
5793
5794impl Render for DraggedDock {
5795 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
5796 gpui::Empty
5797 }
5798}
5799
5800impl Render for Workspace {
5801 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
5802 let mut context = KeyContext::new_with_defaults();
5803 context.add("Workspace");
5804 context.set("keyboard_layout", cx.keyboard_layout().name().to_string());
5805 let centered_layout = self.centered_layout
5806 && self.center.panes().len() == 1
5807 && self.active_item(cx).is_some();
5808 let render_padding = |size| {
5809 (size > 0.0).then(|| {
5810 div()
5811 .h_full()
5812 .w(relative(size))
5813 .bg(cx.theme().colors().editor_background)
5814 .border_color(cx.theme().colors().pane_group_border)
5815 })
5816 };
5817 let paddings = if centered_layout {
5818 let settings = WorkspaceSettings::get_global(cx).centered_layout;
5819 (
5820 render_padding(Self::adjust_padding(settings.left_padding)),
5821 render_padding(Self::adjust_padding(settings.right_padding)),
5822 )
5823 } else {
5824 (None, None)
5825 };
5826 let ui_font = theme::setup_ui_font(window, cx);
5827
5828 let theme = cx.theme().clone();
5829 let colors = theme.colors();
5830 let notification_entities = self
5831 .notifications
5832 .iter()
5833 .map(|(_, notification)| notification.entity_id())
5834 .collect::<Vec<_>>();
5835
5836 client_side_decorations(
5837 self.actions(div(), window, cx)
5838 .key_context(context)
5839 .relative()
5840 .size_full()
5841 .flex()
5842 .flex_col()
5843 .font(ui_font)
5844 .gap_0()
5845 .justify_start()
5846 .items_start()
5847 .text_color(colors.text)
5848 .overflow_hidden()
5849 .children(self.titlebar_item.clone())
5850 .on_modifiers_changed(move |_, _, cx| {
5851 for &id in ¬ification_entities {
5852 cx.notify(id);
5853 }
5854 })
5855 .child(
5856 div()
5857 .size_full()
5858 .relative()
5859 .flex_1()
5860 .flex()
5861 .flex_col()
5862 .child(
5863 div()
5864 .id("workspace")
5865 .bg(colors.background)
5866 .relative()
5867 .flex_1()
5868 .w_full()
5869 .flex()
5870 .flex_col()
5871 .overflow_hidden()
5872 .border_t_1()
5873 .border_b_1()
5874 .border_color(colors.border)
5875 .child({
5876 let this = cx.entity().clone();
5877 canvas(
5878 move |bounds, window, cx| {
5879 this.update(cx, |this, cx| {
5880 let bounds_changed = this.bounds != bounds;
5881 this.bounds = bounds;
5882
5883 if bounds_changed {
5884 this.left_dock.update(cx, |dock, cx| {
5885 dock.clamp_panel_size(
5886 bounds.size.width,
5887 window,
5888 cx,
5889 )
5890 });
5891
5892 this.right_dock.update(cx, |dock, cx| {
5893 dock.clamp_panel_size(
5894 bounds.size.width,
5895 window,
5896 cx,
5897 )
5898 });
5899
5900 this.bottom_dock.update(cx, |dock, cx| {
5901 dock.clamp_panel_size(
5902 bounds.size.height,
5903 window,
5904 cx,
5905 )
5906 });
5907 }
5908 })
5909 },
5910 |_, _, _, _| {},
5911 )
5912 .absolute()
5913 .size_full()
5914 })
5915 .when(self.zoomed.is_none(), |this| {
5916 this.on_drag_move(cx.listener(
5917 move |workspace,
5918 e: &DragMoveEvent<DraggedDock>,
5919 window,
5920 cx| {
5921 if workspace.previous_dock_drag_coordinates
5922 != Some(e.event.position)
5923 {
5924 workspace.previous_dock_drag_coordinates =
5925 Some(e.event.position);
5926 match e.drag(cx).0 {
5927 DockPosition::Left => {
5928 resize_left_dock(
5929 e.event.position.x
5930 - workspace.bounds.left(),
5931 workspace,
5932 window,
5933 cx,
5934 );
5935 }
5936 DockPosition::Right => {
5937 resize_right_dock(
5938 workspace.bounds.right()
5939 - e.event.position.x,
5940 workspace,
5941 window,
5942 cx,
5943 );
5944 }
5945 DockPosition::Bottom => {
5946 resize_bottom_dock(
5947 workspace.bounds.bottom()
5948 - e.event.position.y,
5949 workspace,
5950 window,
5951 cx,
5952 );
5953 }
5954 };
5955 workspace.serialize_workspace(window, cx);
5956 }
5957 },
5958 ))
5959 })
5960 .child({
5961 match self.bottom_dock_layout {
5962 BottomDockLayout::Full => div()
5963 .flex()
5964 .flex_col()
5965 .h_full()
5966 .child(
5967 div()
5968 .flex()
5969 .flex_row()
5970 .flex_1()
5971 .overflow_hidden()
5972 .children(self.render_dock(
5973 DockPosition::Left,
5974 &self.left_dock,
5975 window,
5976 cx,
5977 ))
5978 .child(
5979 div()
5980 .flex()
5981 .flex_col()
5982 .flex_1()
5983 .overflow_hidden()
5984 .child(
5985 h_flex()
5986 .flex_1()
5987 .when_some(
5988 paddings.0,
5989 |this, p| {
5990 this.child(
5991 p.border_r_1(),
5992 )
5993 },
5994 )
5995 .child(self.center.render(
5996 self.zoomed.as_ref(),
5997 &PaneRenderContext {
5998 follower_states:
5999 &self.follower_states,
6000 active_call: self.active_call(),
6001 active_pane: &self.active_pane,
6002 app_state: &self.app_state,
6003 project: &self.project,
6004 workspace: &self.weak_self,
6005 },
6006 window,
6007 cx,
6008 ))
6009 .when_some(
6010 paddings.1,
6011 |this, p| {
6012 this.child(
6013 p.border_l_1(),
6014 )
6015 },
6016 ),
6017 ),
6018 )
6019 .children(self.render_dock(
6020 DockPosition::Right,
6021 &self.right_dock,
6022 window,
6023 cx,
6024 )),
6025 )
6026 .child(div().w_full().children(self.render_dock(
6027 DockPosition::Bottom,
6028 &self.bottom_dock,
6029 window,
6030 cx
6031 ))),
6032
6033 BottomDockLayout::LeftAligned => div()
6034 .flex()
6035 .flex_row()
6036 .h_full()
6037 .child(
6038 div()
6039 .flex()
6040 .flex_col()
6041 .flex_1()
6042 .h_full()
6043 .child(
6044 div()
6045 .flex()
6046 .flex_row()
6047 .flex_1()
6048 .children(self.render_dock(DockPosition::Left, &self.left_dock, window, cx))
6049 .child(
6050 div()
6051 .flex()
6052 .flex_col()
6053 .flex_1()
6054 .overflow_hidden()
6055 .child(
6056 h_flex()
6057 .flex_1()
6058 .when_some(paddings.0, |this, p| this.child(p.border_r_1()))
6059 .child(self.center.render(
6060 self.zoomed.as_ref(),
6061 &PaneRenderContext {
6062 follower_states:
6063 &self.follower_states,
6064 active_call: self.active_call(),
6065 active_pane: &self.active_pane,
6066 app_state: &self.app_state,
6067 project: &self.project,
6068 workspace: &self.weak_self,
6069 },
6070 window,
6071 cx,
6072 ))
6073 .when_some(paddings.1, |this, p| this.child(p.border_l_1())),
6074 )
6075 )
6076 )
6077 .child(
6078 div()
6079 .w_full()
6080 .children(self.render_dock(DockPosition::Bottom, &self.bottom_dock, window, cx))
6081 ),
6082 )
6083 .children(self.render_dock(
6084 DockPosition::Right,
6085 &self.right_dock,
6086 window,
6087 cx,
6088 )),
6089
6090 BottomDockLayout::RightAligned => div()
6091 .flex()
6092 .flex_row()
6093 .h_full()
6094 .children(self.render_dock(
6095 DockPosition::Left,
6096 &self.left_dock,
6097 window,
6098 cx,
6099 ))
6100 .child(
6101 div()
6102 .flex()
6103 .flex_col()
6104 .flex_1()
6105 .h_full()
6106 .child(
6107 div()
6108 .flex()
6109 .flex_row()
6110 .flex_1()
6111 .child(
6112 div()
6113 .flex()
6114 .flex_col()
6115 .flex_1()
6116 .overflow_hidden()
6117 .child(
6118 h_flex()
6119 .flex_1()
6120 .when_some(paddings.0, |this, p| this.child(p.border_r_1()))
6121 .child(self.center.render(
6122 self.zoomed.as_ref(),
6123 &PaneRenderContext {
6124 follower_states:
6125 &self.follower_states,
6126 active_call: self.active_call(),
6127 active_pane: &self.active_pane,
6128 app_state: &self.app_state,
6129 project: &self.project,
6130 workspace: &self.weak_self,
6131 },
6132 window,
6133 cx,
6134 ))
6135 .when_some(paddings.1, |this, p| this.child(p.border_l_1())),
6136 )
6137 )
6138 .children(self.render_dock(DockPosition::Right, &self.right_dock, window, cx))
6139 )
6140 .child(
6141 div()
6142 .w_full()
6143 .children(self.render_dock(DockPosition::Bottom, &self.bottom_dock, window, cx))
6144 ),
6145 ),
6146
6147 BottomDockLayout::Contained => div()
6148 .flex()
6149 .flex_row()
6150 .h_full()
6151 .children(self.render_dock(
6152 DockPosition::Left,
6153 &self.left_dock,
6154 window,
6155 cx,
6156 ))
6157 .child(
6158 div()
6159 .flex()
6160 .flex_col()
6161 .flex_1()
6162 .overflow_hidden()
6163 .child(
6164 h_flex()
6165 .flex_1()
6166 .when_some(paddings.0, |this, p| {
6167 this.child(p.border_r_1())
6168 })
6169 .child(self.center.render(
6170 self.zoomed.as_ref(),
6171 &PaneRenderContext {
6172 follower_states:
6173 &self.follower_states,
6174 active_call: self.active_call(),
6175 active_pane: &self.active_pane,
6176 app_state: &self.app_state,
6177 project: &self.project,
6178 workspace: &self.weak_self,
6179 },
6180 window,
6181 cx,
6182 ))
6183 .when_some(paddings.1, |this, p| {
6184 this.child(p.border_l_1())
6185 }),
6186 )
6187 .children(self.render_dock(
6188 DockPosition::Bottom,
6189 &self.bottom_dock,
6190 window,
6191 cx,
6192 )),
6193 )
6194 .children(self.render_dock(
6195 DockPosition::Right,
6196 &self.right_dock,
6197 window,
6198 cx,
6199 )),
6200 }
6201 })
6202 .children(self.zoomed.as_ref().and_then(|view| {
6203 let zoomed_view = view.upgrade()?;
6204 let div = div()
6205 .occlude()
6206 .absolute()
6207 .overflow_hidden()
6208 .border_color(colors.border)
6209 .bg(colors.background)
6210 .child(zoomed_view)
6211 .inset_0()
6212 .shadow_lg();
6213
6214 Some(match self.zoomed_position {
6215 Some(DockPosition::Left) => div.right_2().border_r_1(),
6216 Some(DockPosition::Right) => div.left_2().border_l_1(),
6217 Some(DockPosition::Bottom) => div.top_2().border_t_1(),
6218 None => {
6219 div.top_2().bottom_2().left_2().right_2().border_1()
6220 }
6221 })
6222 }))
6223 .children(self.render_notifications(window, cx)),
6224 )
6225 .child(self.status_bar.clone())
6226 .child(self.modal_layer.clone())
6227 .child(self.toast_layer.clone()),
6228 ),
6229 window,
6230 cx,
6231 )
6232 }
6233}
6234
6235fn resize_bottom_dock(
6236 new_size: Pixels,
6237 workspace: &mut Workspace,
6238 window: &mut Window,
6239 cx: &mut App,
6240) {
6241 let size =
6242 new_size.min(workspace.bounds.bottom() - RESIZE_HANDLE_SIZE - workspace.bounds.top());
6243 workspace.bottom_dock.update(cx, |bottom_dock, cx| {
6244 bottom_dock.resize_active_panel(Some(size), window, cx);
6245 });
6246}
6247
6248fn resize_right_dock(
6249 new_size: Pixels,
6250 workspace: &mut Workspace,
6251 window: &mut Window,
6252 cx: &mut App,
6253) {
6254 let size = new_size.max(workspace.bounds.left() - RESIZE_HANDLE_SIZE);
6255 workspace.right_dock.update(cx, |right_dock, cx| {
6256 right_dock.resize_active_panel(Some(size), window, cx);
6257 });
6258}
6259
6260fn resize_left_dock(
6261 new_size: Pixels,
6262 workspace: &mut Workspace,
6263 window: &mut Window,
6264 cx: &mut App,
6265) {
6266 let size = new_size.min(workspace.bounds.right() - RESIZE_HANDLE_SIZE);
6267
6268 workspace.left_dock.update(cx, |left_dock, cx| {
6269 left_dock.resize_active_panel(Some(size), window, cx);
6270 });
6271}
6272
6273impl WorkspaceStore {
6274 pub fn new(client: Arc<Client>, cx: &mut Context<Self>) -> Self {
6275 Self {
6276 workspaces: Default::default(),
6277 _subscriptions: vec![
6278 client.add_request_handler(cx.weak_entity(), Self::handle_follow),
6279 client.add_message_handler(cx.weak_entity(), Self::handle_update_followers),
6280 ],
6281 client,
6282 }
6283 }
6284
6285 pub fn update_followers(
6286 &self,
6287 project_id: Option<u64>,
6288 update: proto::update_followers::Variant,
6289 cx: &App,
6290 ) -> Option<()> {
6291 let active_call = ActiveCall::try_global(cx)?;
6292 let room_id = active_call.read(cx).room()?.read(cx).id();
6293 self.client
6294 .send(proto::UpdateFollowers {
6295 room_id,
6296 project_id,
6297 variant: Some(update),
6298 })
6299 .log_err()
6300 }
6301
6302 pub async fn handle_follow(
6303 this: Entity<Self>,
6304 envelope: TypedEnvelope<proto::Follow>,
6305 mut cx: AsyncApp,
6306 ) -> Result<proto::FollowResponse> {
6307 this.update(&mut cx, |this, cx| {
6308 let follower = Follower {
6309 project_id: envelope.payload.project_id,
6310 peer_id: envelope.original_sender_id()?,
6311 };
6312
6313 let mut response = proto::FollowResponse::default();
6314 this.workspaces.retain(|workspace| {
6315 workspace
6316 .update(cx, |workspace, window, cx| {
6317 let handler_response =
6318 workspace.handle_follow(follower.project_id, window, cx);
6319 if let Some(active_view) = handler_response.active_view.clone() {
6320 if workspace.project.read(cx).remote_id() == follower.project_id {
6321 response.active_view = Some(active_view)
6322 }
6323 }
6324 })
6325 .is_ok()
6326 });
6327
6328 Ok(response)
6329 })?
6330 }
6331
6332 async fn handle_update_followers(
6333 this: Entity<Self>,
6334 envelope: TypedEnvelope<proto::UpdateFollowers>,
6335 mut cx: AsyncApp,
6336 ) -> Result<()> {
6337 let leader_id = envelope.original_sender_id()?;
6338 let update = envelope.payload;
6339
6340 this.update(&mut cx, |this, cx| {
6341 this.workspaces.retain(|workspace| {
6342 workspace
6343 .update(cx, |workspace, window, cx| {
6344 let project_id = workspace.project.read(cx).remote_id();
6345 if update.project_id != project_id && update.project_id.is_some() {
6346 return;
6347 }
6348 workspace.handle_update_followers(leader_id, update.clone(), window, cx);
6349 })
6350 .is_ok()
6351 });
6352 Ok(())
6353 })?
6354 }
6355}
6356
6357impl ViewId {
6358 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
6359 Ok(Self {
6360 creator: message
6361 .creator
6362 .map(CollaboratorId::PeerId)
6363 .context("creator is missing")?,
6364 id: message.id,
6365 })
6366 }
6367
6368 pub(crate) fn to_proto(self) -> Option<proto::ViewId> {
6369 if let CollaboratorId::PeerId(peer_id) = self.creator {
6370 Some(proto::ViewId {
6371 creator: Some(peer_id),
6372 id: self.id,
6373 })
6374 } else {
6375 None
6376 }
6377 }
6378}
6379
6380impl FollowerState {
6381 fn pane(&self) -> &Entity<Pane> {
6382 self.dock_pane.as_ref().unwrap_or(&self.center_pane)
6383 }
6384}
6385
6386pub trait WorkspaceHandle {
6387 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath>;
6388}
6389
6390impl WorkspaceHandle for Entity<Workspace> {
6391 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath> {
6392 self.read(cx)
6393 .worktrees(cx)
6394 .flat_map(|worktree| {
6395 let worktree_id = worktree.read(cx).id();
6396 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
6397 worktree_id,
6398 path: f.path.clone(),
6399 })
6400 })
6401 .collect::<Vec<_>>()
6402 }
6403}
6404
6405impl std::fmt::Debug for OpenPaths {
6406 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
6407 f.debug_struct("OpenPaths")
6408 .field("paths", &self.paths)
6409 .finish()
6410 }
6411}
6412
6413pub async fn last_opened_workspace_location() -> Option<SerializedWorkspaceLocation> {
6414 DB.last_workspace().await.log_err().flatten()
6415}
6416
6417pub fn last_session_workspace_locations(
6418 last_session_id: &str,
6419 last_session_window_stack: Option<Vec<WindowId>>,
6420) -> Option<Vec<SerializedWorkspaceLocation>> {
6421 DB.last_session_workspace_locations(last_session_id, last_session_window_stack)
6422 .log_err()
6423}
6424
6425actions!(
6426 collab,
6427 [
6428 OpenChannelNotes,
6429 Mute,
6430 Deafen,
6431 LeaveCall,
6432 ShareProject,
6433 ScreenShare
6434 ]
6435);
6436actions!(zed, [OpenLog]);
6437
6438async fn join_channel_internal(
6439 channel_id: ChannelId,
6440 app_state: &Arc<AppState>,
6441 requesting_window: Option<WindowHandle<Workspace>>,
6442 active_call: &Entity<ActiveCall>,
6443 cx: &mut AsyncApp,
6444) -> Result<bool> {
6445 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
6446 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
6447 return (false, None);
6448 };
6449
6450 let already_in_channel = room.channel_id() == Some(channel_id);
6451 let should_prompt = room.is_sharing_project()
6452 && !room.remote_participants().is_empty()
6453 && !already_in_channel;
6454 let open_room = if already_in_channel {
6455 active_call.room().cloned()
6456 } else {
6457 None
6458 };
6459 (should_prompt, open_room)
6460 })?;
6461
6462 if let Some(room) = open_room {
6463 let task = room.update(cx, |room, cx| {
6464 if let Some((project, host)) = room.most_active_project(cx) {
6465 return Some(join_in_room_project(project, host, app_state.clone(), cx));
6466 }
6467
6468 None
6469 })?;
6470 if let Some(task) = task {
6471 task.await?;
6472 }
6473 return anyhow::Ok(true);
6474 }
6475
6476 if should_prompt {
6477 if let Some(workspace) = requesting_window {
6478 let answer = workspace
6479 .update(cx, |_, window, cx| {
6480 window.prompt(
6481 PromptLevel::Warning,
6482 "Do you want to switch channels?",
6483 Some("Leaving this call will unshare your current project."),
6484 &["Yes, Join Channel", "Cancel"],
6485 cx,
6486 )
6487 })?
6488 .await;
6489
6490 if answer == Ok(1) {
6491 return Ok(false);
6492 }
6493 } else {
6494 return Ok(false); // unreachable!() hopefully
6495 }
6496 }
6497
6498 let client = cx.update(|cx| active_call.read(cx).client())?;
6499
6500 let mut client_status = client.status();
6501
6502 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
6503 'outer: loop {
6504 let Some(status) = client_status.recv().await else {
6505 anyhow::bail!("error connecting");
6506 };
6507
6508 match status {
6509 Status::Connecting
6510 | Status::Authenticating
6511 | Status::Reconnecting
6512 | Status::Reauthenticating => continue,
6513 Status::Connected { .. } => break 'outer,
6514 Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
6515 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
6516 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
6517 return Err(ErrorCode::Disconnected.into());
6518 }
6519 }
6520 }
6521
6522 let room = active_call
6523 .update(cx, |active_call, cx| {
6524 active_call.join_channel(channel_id, cx)
6525 })?
6526 .await?;
6527
6528 let Some(room) = room else {
6529 return anyhow::Ok(true);
6530 };
6531
6532 room.update(cx, |room, _| room.room_update_completed())?
6533 .await;
6534
6535 let task = room.update(cx, |room, cx| {
6536 if let Some((project, host)) = room.most_active_project(cx) {
6537 return Some(join_in_room_project(project, host, app_state.clone(), cx));
6538 }
6539
6540 // If you are the first to join a channel, see if you should share your project.
6541 if room.remote_participants().is_empty() && !room.local_participant_is_guest() {
6542 if let Some(workspace) = requesting_window {
6543 let project = workspace.update(cx, |workspace, _, cx| {
6544 let project = workspace.project.read(cx);
6545
6546 if !CallSettings::get_global(cx).share_on_join {
6547 return None;
6548 }
6549
6550 if (project.is_local() || project.is_via_ssh())
6551 && project.visible_worktrees(cx).any(|tree| {
6552 tree.read(cx)
6553 .root_entry()
6554 .map_or(false, |entry| entry.is_dir())
6555 })
6556 {
6557 Some(workspace.project.clone())
6558 } else {
6559 None
6560 }
6561 });
6562 if let Ok(Some(project)) = project {
6563 return Some(cx.spawn(async move |room, cx| {
6564 room.update(cx, |room, cx| room.share_project(project, cx))?
6565 .await?;
6566 Ok(())
6567 }));
6568 }
6569 }
6570 }
6571
6572 None
6573 })?;
6574 if let Some(task) = task {
6575 task.await?;
6576 return anyhow::Ok(true);
6577 }
6578 anyhow::Ok(false)
6579}
6580
6581pub fn join_channel(
6582 channel_id: ChannelId,
6583 app_state: Arc<AppState>,
6584 requesting_window: Option<WindowHandle<Workspace>>,
6585 cx: &mut App,
6586) -> Task<Result<()>> {
6587 let active_call = ActiveCall::global(cx);
6588 cx.spawn(async move |cx| {
6589 let result = join_channel_internal(
6590 channel_id,
6591 &app_state,
6592 requesting_window,
6593 &active_call,
6594 cx,
6595 )
6596 .await;
6597
6598 // join channel succeeded, and opened a window
6599 if matches!(result, Ok(true)) {
6600 return anyhow::Ok(());
6601 }
6602
6603 // find an existing workspace to focus and show call controls
6604 let mut active_window =
6605 requesting_window.or_else(|| activate_any_workspace_window( cx));
6606 if active_window.is_none() {
6607 // no open workspaces, make one to show the error in (blergh)
6608 let (window_handle, _) = cx
6609 .update(|cx| {
6610 Workspace::new_local(vec![], app_state.clone(), requesting_window, None, cx)
6611 })?
6612 .await?;
6613
6614 if result.is_ok() {
6615 cx.update(|cx| {
6616 cx.dispatch_action(&OpenChannelNotes);
6617 }).log_err();
6618 }
6619
6620 active_window = Some(window_handle);
6621 }
6622
6623 if let Err(err) = result {
6624 log::error!("failed to join channel: {}", err);
6625 if let Some(active_window) = active_window {
6626 active_window
6627 .update(cx, |_, window, cx| {
6628 let detail: SharedString = match err.error_code() {
6629 ErrorCode::SignedOut => {
6630 "Please sign in to continue.".into()
6631 }
6632 ErrorCode::UpgradeRequired => {
6633 "Your are running an unsupported version of Zed. Please update to continue.".into()
6634 }
6635 ErrorCode::NoSuchChannel => {
6636 "No matching channel was found. Please check the link and try again.".into()
6637 }
6638 ErrorCode::Forbidden => {
6639 "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
6640 }
6641 ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
6642 _ => format!("{}\n\nPlease try again.", err).into(),
6643 };
6644 window.prompt(
6645 PromptLevel::Critical,
6646 "Failed to join channel",
6647 Some(&detail),
6648 &["Ok"],
6649 cx)
6650 })?
6651 .await
6652 .ok();
6653 }
6654 }
6655
6656 // return ok, we showed the error to the user.
6657 anyhow::Ok(())
6658 })
6659}
6660
6661pub async fn get_any_active_workspace(
6662 app_state: Arc<AppState>,
6663 mut cx: AsyncApp,
6664) -> anyhow::Result<WindowHandle<Workspace>> {
6665 // find an existing workspace to focus and show call controls
6666 let active_window = activate_any_workspace_window(&mut cx);
6667 if active_window.is_none() {
6668 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, cx))?
6669 .await?;
6670 }
6671 activate_any_workspace_window(&mut cx).context("could not open zed")
6672}
6673
6674fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<Workspace>> {
6675 cx.update(|cx| {
6676 if let Some(workspace_window) = cx
6677 .active_window()
6678 .and_then(|window| window.downcast::<Workspace>())
6679 {
6680 return Some(workspace_window);
6681 }
6682
6683 for window in cx.windows() {
6684 if let Some(workspace_window) = window.downcast::<Workspace>() {
6685 workspace_window
6686 .update(cx, |_, window, _| window.activate_window())
6687 .ok();
6688 return Some(workspace_window);
6689 }
6690 }
6691 None
6692 })
6693 .ok()
6694 .flatten()
6695}
6696
6697pub fn local_workspace_windows(cx: &App) -> Vec<WindowHandle<Workspace>> {
6698 cx.windows()
6699 .into_iter()
6700 .filter_map(|window| window.downcast::<Workspace>())
6701 .filter(|workspace| {
6702 workspace
6703 .read(cx)
6704 .is_ok_and(|workspace| workspace.project.read(cx).is_local())
6705 })
6706 .collect()
6707}
6708
6709#[derive(Default)]
6710pub struct OpenOptions {
6711 pub visible: Option<OpenVisible>,
6712 pub focus: Option<bool>,
6713 pub open_new_workspace: Option<bool>,
6714 pub replace_window: Option<WindowHandle<Workspace>>,
6715 pub env: Option<HashMap<String, String>>,
6716}
6717
6718#[allow(clippy::type_complexity)]
6719pub fn open_paths(
6720 abs_paths: &[PathBuf],
6721 app_state: Arc<AppState>,
6722 open_options: OpenOptions,
6723 cx: &mut App,
6724) -> Task<
6725 anyhow::Result<(
6726 WindowHandle<Workspace>,
6727 Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>>,
6728 )>,
6729> {
6730 let abs_paths = abs_paths.to_vec();
6731 let mut existing = None;
6732 let mut best_match = None;
6733 let mut open_visible = OpenVisible::All;
6734
6735 cx.spawn(async move |cx| {
6736 if open_options.open_new_workspace != Some(true) {
6737 let all_paths = abs_paths.iter().map(|path| app_state.fs.metadata(path));
6738 let all_metadatas = futures::future::join_all(all_paths)
6739 .await
6740 .into_iter()
6741 .filter_map(|result| result.ok().flatten())
6742 .collect::<Vec<_>>();
6743
6744 cx.update(|cx| {
6745 for window in local_workspace_windows(&cx) {
6746 if let Ok(workspace) = window.read(&cx) {
6747 let m = workspace.project.read(&cx).visibility_for_paths(
6748 &abs_paths,
6749 &all_metadatas,
6750 open_options.open_new_workspace == None,
6751 cx,
6752 );
6753 if m > best_match {
6754 existing = Some(window);
6755 best_match = m;
6756 } else if best_match.is_none()
6757 && open_options.open_new_workspace == Some(false)
6758 {
6759 existing = Some(window)
6760 }
6761 }
6762 }
6763 })?;
6764
6765 if open_options.open_new_workspace.is_none() && existing.is_none() {
6766 if all_metadatas.iter().all(|file| !file.is_dir) {
6767 cx.update(|cx| {
6768 if let Some(window) = cx
6769 .active_window()
6770 .and_then(|window| window.downcast::<Workspace>())
6771 {
6772 if let Ok(workspace) = window.read(cx) {
6773 let project = workspace.project().read(cx);
6774 if project.is_local() && !project.is_via_collab() {
6775 existing = Some(window);
6776 open_visible = OpenVisible::None;
6777 return;
6778 }
6779 }
6780 }
6781 for window in local_workspace_windows(cx) {
6782 if let Ok(workspace) = window.read(cx) {
6783 let project = workspace.project().read(cx);
6784 if project.is_via_collab() {
6785 continue;
6786 }
6787 existing = Some(window);
6788 open_visible = OpenVisible::None;
6789 break;
6790 }
6791 }
6792 })?;
6793 }
6794 }
6795 }
6796
6797 if let Some(existing) = existing {
6798 let open_task = existing
6799 .update(cx, |workspace, window, cx| {
6800 window.activate_window();
6801 workspace.open_paths(
6802 abs_paths,
6803 OpenOptions {
6804 visible: Some(open_visible),
6805 ..Default::default()
6806 },
6807 None,
6808 window,
6809 cx,
6810 )
6811 })?
6812 .await;
6813
6814 _ = existing.update(cx, |workspace, _, cx| {
6815 for item in open_task.iter().flatten() {
6816 if let Err(e) = item {
6817 workspace.show_error(&e, cx);
6818 }
6819 }
6820 });
6821
6822 Ok((existing, open_task))
6823 } else {
6824 cx.update(move |cx| {
6825 Workspace::new_local(
6826 abs_paths,
6827 app_state.clone(),
6828 open_options.replace_window,
6829 open_options.env,
6830 cx,
6831 )
6832 })?
6833 .await
6834 }
6835 })
6836}
6837
6838pub fn open_new(
6839 open_options: OpenOptions,
6840 app_state: Arc<AppState>,
6841 cx: &mut App,
6842 init: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + 'static + Send,
6843) -> Task<anyhow::Result<()>> {
6844 let task = Workspace::new_local(Vec::new(), app_state, None, open_options.env, cx);
6845 cx.spawn(async move |cx| {
6846 let (workspace, opened_paths) = task.await?;
6847 workspace.update(cx, |workspace, window, cx| {
6848 if opened_paths.is_empty() {
6849 init(workspace, window, cx)
6850 }
6851 })?;
6852 Ok(())
6853 })
6854}
6855
6856pub fn create_and_open_local_file(
6857 path: &'static Path,
6858 window: &mut Window,
6859 cx: &mut Context<Workspace>,
6860 default_content: impl 'static + Send + FnOnce() -> Rope,
6861) -> Task<Result<Box<dyn ItemHandle>>> {
6862 cx.spawn_in(window, async move |workspace, cx| {
6863 let fs = workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?;
6864 if !fs.is_file(path).await {
6865 fs.create_file(path, Default::default()).await?;
6866 fs.save(path, &default_content(), Default::default())
6867 .await?;
6868 }
6869
6870 let mut items = workspace
6871 .update_in(cx, |workspace, window, cx| {
6872 workspace.with_local_workspace(window, cx, |workspace, window, cx| {
6873 workspace.open_paths(
6874 vec![path.to_path_buf()],
6875 OpenOptions {
6876 visible: Some(OpenVisible::None),
6877 ..Default::default()
6878 },
6879 None,
6880 window,
6881 cx,
6882 )
6883 })
6884 })?
6885 .await?
6886 .await;
6887
6888 let item = items.pop().flatten();
6889 item.with_context(|| format!("path {path:?} is not a file"))?
6890 })
6891}
6892
6893pub fn open_ssh_project_with_new_connection(
6894 window: WindowHandle<Workspace>,
6895 connection_options: SshConnectionOptions,
6896 cancel_rx: oneshot::Receiver<()>,
6897 delegate: Arc<dyn SshClientDelegate>,
6898 app_state: Arc<AppState>,
6899 paths: Vec<PathBuf>,
6900 cx: &mut App,
6901) -> Task<Result<()>> {
6902 cx.spawn(async move |cx| {
6903 let (serialized_ssh_project, workspace_id, serialized_workspace) =
6904 serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
6905
6906 let session = match cx
6907 .update(|cx| {
6908 remote::SshRemoteClient::new(
6909 ConnectionIdentifier::Workspace(workspace_id.0),
6910 connection_options,
6911 cancel_rx,
6912 delegate,
6913 cx,
6914 )
6915 })?
6916 .await?
6917 {
6918 Some(result) => result,
6919 None => return Ok(()),
6920 };
6921
6922 let project = cx.update(|cx| {
6923 project::Project::ssh(
6924 session,
6925 app_state.client.clone(),
6926 app_state.node_runtime.clone(),
6927 app_state.user_store.clone(),
6928 app_state.languages.clone(),
6929 app_state.fs.clone(),
6930 cx,
6931 )
6932 })?;
6933
6934 open_ssh_project_inner(
6935 project,
6936 paths,
6937 serialized_ssh_project,
6938 workspace_id,
6939 serialized_workspace,
6940 app_state,
6941 window,
6942 cx,
6943 )
6944 .await
6945 })
6946}
6947
6948pub fn open_ssh_project_with_existing_connection(
6949 connection_options: SshConnectionOptions,
6950 project: Entity<Project>,
6951 paths: Vec<PathBuf>,
6952 app_state: Arc<AppState>,
6953 window: WindowHandle<Workspace>,
6954 cx: &mut AsyncApp,
6955) -> Task<Result<()>> {
6956 cx.spawn(async move |cx| {
6957 let (serialized_ssh_project, workspace_id, serialized_workspace) =
6958 serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
6959
6960 open_ssh_project_inner(
6961 project,
6962 paths,
6963 serialized_ssh_project,
6964 workspace_id,
6965 serialized_workspace,
6966 app_state,
6967 window,
6968 cx,
6969 )
6970 .await
6971 })
6972}
6973
6974async fn open_ssh_project_inner(
6975 project: Entity<Project>,
6976 paths: Vec<PathBuf>,
6977 serialized_ssh_project: SerializedSshProject,
6978 workspace_id: WorkspaceId,
6979 serialized_workspace: Option<SerializedWorkspace>,
6980 app_state: Arc<AppState>,
6981 window: WindowHandle<Workspace>,
6982 cx: &mut AsyncApp,
6983) -> Result<()> {
6984 let toolchains = DB.toolchains(workspace_id).await?;
6985 for (toolchain, worktree_id, path) in toolchains {
6986 project
6987 .update(cx, |this, cx| {
6988 this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx)
6989 })?
6990 .await;
6991 }
6992 let mut project_paths_to_open = vec![];
6993 let mut project_path_errors = vec![];
6994
6995 for path in paths {
6996 let result = cx
6997 .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))?
6998 .await;
6999 match result {
7000 Ok((_, project_path)) => {
7001 project_paths_to_open.push((path.clone(), Some(project_path)));
7002 }
7003 Err(error) => {
7004 project_path_errors.push(error);
7005 }
7006 };
7007 }
7008
7009 if project_paths_to_open.is_empty() {
7010 return Err(project_path_errors.pop().context("no paths given")?);
7011 }
7012
7013 cx.update_window(window.into(), |_, window, cx| {
7014 window.replace_root(cx, |window, cx| {
7015 telemetry::event!("SSH Project Opened");
7016
7017 let mut workspace =
7018 Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
7019 workspace.set_serialized_ssh_project(serialized_ssh_project);
7020 workspace.update_history(cx);
7021 workspace
7022 });
7023 })?;
7024
7025 window
7026 .update(cx, |_, window, cx| {
7027 window.activate_window();
7028 open_items(serialized_workspace, project_paths_to_open, window, cx)
7029 })?
7030 .await?;
7031
7032 window.update(cx, |workspace, _, cx| {
7033 for error in project_path_errors {
7034 if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
7035 if let Some(path) = error.error_tag("path") {
7036 workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
7037 }
7038 } else {
7039 workspace.show_error(&error, cx)
7040 }
7041 }
7042 })?;
7043
7044 Ok(())
7045}
7046
7047fn serialize_ssh_project(
7048 connection_options: SshConnectionOptions,
7049 paths: Vec<PathBuf>,
7050 cx: &AsyncApp,
7051) -> Task<
7052 Result<(
7053 SerializedSshProject,
7054 WorkspaceId,
7055 Option<SerializedWorkspace>,
7056 )>,
7057> {
7058 cx.background_spawn(async move {
7059 let serialized_ssh_project = persistence::DB
7060 .get_or_create_ssh_project(
7061 connection_options.host.clone(),
7062 connection_options.port,
7063 paths
7064 .iter()
7065 .map(|path| path.to_string_lossy().to_string())
7066 .collect::<Vec<_>>(),
7067 connection_options.username.clone(),
7068 )
7069 .await?;
7070
7071 let serialized_workspace =
7072 persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
7073
7074 let workspace_id = if let Some(workspace_id) =
7075 serialized_workspace.as_ref().map(|workspace| workspace.id)
7076 {
7077 workspace_id
7078 } else {
7079 persistence::DB.next_id().await?
7080 };
7081
7082 Ok((serialized_ssh_project, workspace_id, serialized_workspace))
7083 })
7084}
7085
7086pub fn join_in_room_project(
7087 project_id: u64,
7088 follow_user_id: u64,
7089 app_state: Arc<AppState>,
7090 cx: &mut App,
7091) -> Task<Result<()>> {
7092 let windows = cx.windows();
7093 cx.spawn(async move |cx| {
7094 let existing_workspace = windows.into_iter().find_map(|window_handle| {
7095 window_handle
7096 .downcast::<Workspace>()
7097 .and_then(|window_handle| {
7098 window_handle
7099 .update(cx, |workspace, _window, cx| {
7100 if workspace.project().read(cx).remote_id() == Some(project_id) {
7101 Some(window_handle)
7102 } else {
7103 None
7104 }
7105 })
7106 .unwrap_or(None)
7107 })
7108 });
7109
7110 let workspace = if let Some(existing_workspace) = existing_workspace {
7111 existing_workspace
7112 } else {
7113 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
7114 let room = active_call
7115 .read_with(cx, |call, _| call.room().cloned())?
7116 .context("not in a call")?;
7117 let project = room
7118 .update(cx, |room, cx| {
7119 room.join_project(
7120 project_id,
7121 app_state.languages.clone(),
7122 app_state.fs.clone(),
7123 cx,
7124 )
7125 })?
7126 .await?;
7127
7128 let window_bounds_override = window_bounds_env_override();
7129 cx.update(|cx| {
7130 let mut options = (app_state.build_window_options)(None, cx);
7131 options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
7132 cx.open_window(options, |window, cx| {
7133 cx.new(|cx| {
7134 Workspace::new(Default::default(), project, app_state.clone(), window, cx)
7135 })
7136 })
7137 })??
7138 };
7139
7140 workspace.update(cx, |workspace, window, cx| {
7141 cx.activate(true);
7142 window.activate_window();
7143
7144 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
7145 let follow_peer_id = room
7146 .read(cx)
7147 .remote_participants()
7148 .iter()
7149 .find(|(_, participant)| participant.user.id == follow_user_id)
7150 .map(|(_, p)| p.peer_id)
7151 .or_else(|| {
7152 // If we couldn't follow the given user, follow the host instead.
7153 let collaborator = workspace
7154 .project()
7155 .read(cx)
7156 .collaborators()
7157 .values()
7158 .find(|collaborator| collaborator.is_host)?;
7159 Some(collaborator.peer_id)
7160 });
7161
7162 if let Some(follow_peer_id) = follow_peer_id {
7163 workspace.follow(follow_peer_id, window, cx);
7164 }
7165 }
7166 })?;
7167
7168 anyhow::Ok(())
7169 })
7170}
7171
7172pub fn reload(reload: &Reload, cx: &mut App) {
7173 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
7174 let mut workspace_windows = cx
7175 .windows()
7176 .into_iter()
7177 .filter_map(|window| window.downcast::<Workspace>())
7178 .collect::<Vec<_>>();
7179
7180 // If multiple windows have unsaved changes, and need a save prompt,
7181 // prompt in the active window before switching to a different window.
7182 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
7183
7184 let mut prompt = None;
7185 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
7186 prompt = window
7187 .update(cx, |_, window, cx| {
7188 window.prompt(
7189 PromptLevel::Info,
7190 "Are you sure you want to restart?",
7191 None,
7192 &["Restart", "Cancel"],
7193 cx,
7194 )
7195 })
7196 .ok();
7197 }
7198
7199 let binary_path = reload.binary_path.clone();
7200 cx.spawn(async move |cx| {
7201 if let Some(prompt) = prompt {
7202 let answer = prompt.await?;
7203 if answer != 0 {
7204 return Ok(());
7205 }
7206 }
7207
7208 // If the user cancels any save prompt, then keep the app open.
7209 for window in workspace_windows {
7210 if let Ok(should_close) = window.update(cx, |workspace, window, cx| {
7211 workspace.prepare_to_close(CloseIntent::Quit, window, cx)
7212 }) {
7213 if !should_close.await? {
7214 return Ok(());
7215 }
7216 }
7217 }
7218
7219 cx.update(|cx| cx.restart(binary_path))
7220 })
7221 .detach_and_log_err(cx);
7222}
7223
7224fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
7225 let mut parts = value.split(',');
7226 let x: usize = parts.next()?.parse().ok()?;
7227 let y: usize = parts.next()?.parse().ok()?;
7228 Some(point(px(x as f32), px(y as f32)))
7229}
7230
7231fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
7232 let mut parts = value.split(',');
7233 let width: usize = parts.next()?.parse().ok()?;
7234 let height: usize = parts.next()?.parse().ok()?;
7235 Some(size(px(width as f32), px(height as f32)))
7236}
7237
7238pub fn client_side_decorations(
7239 element: impl IntoElement,
7240 window: &mut Window,
7241 cx: &mut App,
7242) -> Stateful<Div> {
7243 const BORDER_SIZE: Pixels = px(1.0);
7244 let decorations = window.window_decorations();
7245
7246 if matches!(decorations, Decorations::Client { .. }) {
7247 window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
7248 }
7249
7250 struct GlobalResizeEdge(ResizeEdge);
7251 impl Global for GlobalResizeEdge {}
7252
7253 div()
7254 .id("window-backdrop")
7255 .bg(transparent_black())
7256 .map(|div| match decorations {
7257 Decorations::Server => div,
7258 Decorations::Client { tiling, .. } => div
7259 .when(!(tiling.top || tiling.right), |div| {
7260 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7261 })
7262 .when(!(tiling.top || tiling.left), |div| {
7263 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7264 })
7265 .when(!(tiling.bottom || tiling.right), |div| {
7266 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7267 })
7268 .when(!(tiling.bottom || tiling.left), |div| {
7269 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7270 })
7271 .when(!tiling.top, |div| {
7272 div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
7273 })
7274 .when(!tiling.bottom, |div| {
7275 div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
7276 })
7277 .when(!tiling.left, |div| {
7278 div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
7279 })
7280 .when(!tiling.right, |div| {
7281 div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
7282 })
7283 .on_mouse_move(move |e, window, cx| {
7284 let size = window.window_bounds().get_bounds().size;
7285 let pos = e.position;
7286
7287 let new_edge =
7288 resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
7289
7290 let edge = cx.try_global::<GlobalResizeEdge>();
7291 if new_edge != edge.map(|edge| edge.0) {
7292 window
7293 .window_handle()
7294 .update(cx, |workspace, _, cx| {
7295 cx.notify(workspace.entity_id());
7296 })
7297 .ok();
7298 }
7299 })
7300 .on_mouse_down(MouseButton::Left, move |e, window, _| {
7301 let size = window.window_bounds().get_bounds().size;
7302 let pos = e.position;
7303
7304 let edge = match resize_edge(
7305 pos,
7306 theme::CLIENT_SIDE_DECORATION_SHADOW,
7307 size,
7308 tiling,
7309 ) {
7310 Some(value) => value,
7311 None => return,
7312 };
7313
7314 window.start_window_resize(edge);
7315 }),
7316 })
7317 .size_full()
7318 .child(
7319 div()
7320 .cursor(CursorStyle::Arrow)
7321 .map(|div| match decorations {
7322 Decorations::Server => div,
7323 Decorations::Client { tiling } => div
7324 .border_color(cx.theme().colors().border)
7325 .when(!(tiling.top || tiling.right), |div| {
7326 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7327 })
7328 .when(!(tiling.top || tiling.left), |div| {
7329 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7330 })
7331 .when(!(tiling.bottom || tiling.right), |div| {
7332 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7333 })
7334 .when(!(tiling.bottom || tiling.left), |div| {
7335 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7336 })
7337 .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
7338 .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
7339 .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
7340 .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
7341 .when(!tiling.is_tiled(), |div| {
7342 div.shadow(vec![gpui::BoxShadow {
7343 color: Hsla {
7344 h: 0.,
7345 s: 0.,
7346 l: 0.,
7347 a: 0.4,
7348 },
7349 blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
7350 spread_radius: px(0.),
7351 offset: point(px(0.0), px(0.0)),
7352 }])
7353 }),
7354 })
7355 .on_mouse_move(|_e, _, cx| {
7356 cx.stop_propagation();
7357 })
7358 .size_full()
7359 .child(element),
7360 )
7361 .map(|div| match decorations {
7362 Decorations::Server => div,
7363 Decorations::Client { tiling, .. } => div.child(
7364 canvas(
7365 |_bounds, window, _| {
7366 window.insert_hitbox(
7367 Bounds::new(
7368 point(px(0.0), px(0.0)),
7369 window.window_bounds().get_bounds().size,
7370 ),
7371 HitboxBehavior::Normal,
7372 )
7373 },
7374 move |_bounds, hitbox, window, cx| {
7375 let mouse = window.mouse_position();
7376 let size = window.window_bounds().get_bounds().size;
7377 let Some(edge) =
7378 resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
7379 else {
7380 return;
7381 };
7382 cx.set_global(GlobalResizeEdge(edge));
7383 window.set_cursor_style(
7384 match edge {
7385 ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
7386 ResizeEdge::Left | ResizeEdge::Right => {
7387 CursorStyle::ResizeLeftRight
7388 }
7389 ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
7390 CursorStyle::ResizeUpLeftDownRight
7391 }
7392 ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
7393 CursorStyle::ResizeUpRightDownLeft
7394 }
7395 },
7396 Some(&hitbox),
7397 );
7398 },
7399 )
7400 .size_full()
7401 .absolute(),
7402 ),
7403 })
7404}
7405
7406fn resize_edge(
7407 pos: Point<Pixels>,
7408 shadow_size: Pixels,
7409 window_size: Size<Pixels>,
7410 tiling: Tiling,
7411) -> Option<ResizeEdge> {
7412 let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
7413 if bounds.contains(&pos) {
7414 return None;
7415 }
7416
7417 let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
7418 let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
7419 if !tiling.top && top_left_bounds.contains(&pos) {
7420 return Some(ResizeEdge::TopLeft);
7421 }
7422
7423 let top_right_bounds = Bounds::new(
7424 Point::new(window_size.width - corner_size.width, px(0.)),
7425 corner_size,
7426 );
7427 if !tiling.top && top_right_bounds.contains(&pos) {
7428 return Some(ResizeEdge::TopRight);
7429 }
7430
7431 let bottom_left_bounds = Bounds::new(
7432 Point::new(px(0.), window_size.height - corner_size.height),
7433 corner_size,
7434 );
7435 if !tiling.bottom && bottom_left_bounds.contains(&pos) {
7436 return Some(ResizeEdge::BottomLeft);
7437 }
7438
7439 let bottom_right_bounds = Bounds::new(
7440 Point::new(
7441 window_size.width - corner_size.width,
7442 window_size.height - corner_size.height,
7443 ),
7444 corner_size,
7445 );
7446 if !tiling.bottom && bottom_right_bounds.contains(&pos) {
7447 return Some(ResizeEdge::BottomRight);
7448 }
7449
7450 if !tiling.top && pos.y < shadow_size {
7451 Some(ResizeEdge::Top)
7452 } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
7453 Some(ResizeEdge::Bottom)
7454 } else if !tiling.left && pos.x < shadow_size {
7455 Some(ResizeEdge::Left)
7456 } else if !tiling.right && pos.x > window_size.width - shadow_size {
7457 Some(ResizeEdge::Right)
7458 } else {
7459 None
7460 }
7461}
7462
7463fn join_pane_into_active(
7464 active_pane: &Entity<Pane>,
7465 pane: &Entity<Pane>,
7466 window: &mut Window,
7467 cx: &mut App,
7468) {
7469 if pane == active_pane {
7470 return;
7471 } else if pane.read(cx).items_len() == 0 {
7472 pane.update(cx, |_, cx| {
7473 cx.emit(pane::Event::Remove {
7474 focus_on_pane: None,
7475 });
7476 })
7477 } else {
7478 move_all_items(pane, active_pane, window, cx);
7479 }
7480}
7481
7482fn move_all_items(
7483 from_pane: &Entity<Pane>,
7484 to_pane: &Entity<Pane>,
7485 window: &mut Window,
7486 cx: &mut App,
7487) {
7488 let destination_is_different = from_pane != to_pane;
7489 let mut moved_items = 0;
7490 for (item_ix, item_handle) in from_pane
7491 .read(cx)
7492 .items()
7493 .enumerate()
7494 .map(|(ix, item)| (ix, item.clone()))
7495 .collect::<Vec<_>>()
7496 {
7497 let ix = item_ix - moved_items;
7498 if destination_is_different {
7499 // Close item from previous pane
7500 from_pane.update(cx, |source, cx| {
7501 source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), window, cx);
7502 });
7503 moved_items += 1;
7504 }
7505
7506 // This automatically removes duplicate items in the pane
7507 to_pane.update(cx, |destination, cx| {
7508 destination.add_item(item_handle, true, true, None, window, cx);
7509 window.focus(&destination.focus_handle(cx))
7510 });
7511 }
7512}
7513
7514pub fn move_item(
7515 source: &Entity<Pane>,
7516 destination: &Entity<Pane>,
7517 item_id_to_move: EntityId,
7518 destination_index: usize,
7519 window: &mut Window,
7520 cx: &mut App,
7521) {
7522 let Some((item_ix, item_handle)) = source
7523 .read(cx)
7524 .items()
7525 .enumerate()
7526 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
7527 .map(|(ix, item)| (ix, item.clone()))
7528 else {
7529 // Tab was closed during drag
7530 return;
7531 };
7532
7533 if source != destination {
7534 // Close item from previous pane
7535 source.update(cx, |source, cx| {
7536 source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), window, cx);
7537 });
7538 }
7539
7540 // This automatically removes duplicate items in the pane
7541 destination.update(cx, |destination, cx| {
7542 destination.add_item(item_handle, true, true, Some(destination_index), window, cx);
7543 window.focus(&destination.focus_handle(cx))
7544 });
7545}
7546
7547pub fn move_active_item(
7548 source: &Entity<Pane>,
7549 destination: &Entity<Pane>,
7550 focus_destination: bool,
7551 close_if_empty: bool,
7552 window: &mut Window,
7553 cx: &mut App,
7554) {
7555 if source == destination {
7556 return;
7557 }
7558 let Some(active_item) = source.read(cx).active_item() else {
7559 return;
7560 };
7561 source.update(cx, |source_pane, cx| {
7562 let item_id = active_item.item_id();
7563 source_pane.remove_item(item_id, false, close_if_empty, window, cx);
7564 destination.update(cx, |target_pane, cx| {
7565 target_pane.add_item(
7566 active_item,
7567 focus_destination,
7568 focus_destination,
7569 Some(target_pane.items_len()),
7570 window,
7571 cx,
7572 );
7573 });
7574 });
7575}
7576
7577#[derive(Debug)]
7578pub struct WorkspacePosition {
7579 pub window_bounds: Option<WindowBounds>,
7580 pub display: Option<Uuid>,
7581 pub centered_layout: bool,
7582}
7583
7584pub fn ssh_workspace_position_from_db(
7585 host: String,
7586 port: Option<u16>,
7587 user: Option<String>,
7588 paths_to_open: &[PathBuf],
7589 cx: &App,
7590) -> Task<Result<WorkspacePosition>> {
7591 let paths = paths_to_open
7592 .iter()
7593 .map(|path| path.to_string_lossy().to_string())
7594 .collect::<Vec<_>>();
7595
7596 cx.background_spawn(async move {
7597 let serialized_ssh_project = persistence::DB
7598 .get_or_create_ssh_project(host, port, paths, user)
7599 .await
7600 .context("fetching serialized ssh project")?;
7601 let serialized_workspace =
7602 persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
7603
7604 let (window_bounds, display) = if let Some(bounds) = window_bounds_env_override() {
7605 (Some(WindowBounds::Windowed(bounds)), None)
7606 } else {
7607 let restorable_bounds = serialized_workspace
7608 .as_ref()
7609 .and_then(|workspace| Some((workspace.display?, workspace.window_bounds?)))
7610 .or_else(|| {
7611 let (display, window_bounds) = DB.last_window().log_err()?;
7612 Some((display?, window_bounds?))
7613 });
7614
7615 if let Some((serialized_display, serialized_status)) = restorable_bounds {
7616 (Some(serialized_status.0), Some(serialized_display))
7617 } else {
7618 (None, None)
7619 }
7620 };
7621
7622 let centered_layout = serialized_workspace
7623 .as_ref()
7624 .map(|w| w.centered_layout)
7625 .unwrap_or(false);
7626
7627 Ok(WorkspacePosition {
7628 window_bounds,
7629 display,
7630 centered_layout,
7631 })
7632 })
7633}
7634
7635#[cfg(test)]
7636mod tests {
7637 use std::{cell::RefCell, rc::Rc};
7638
7639 use super::*;
7640 use crate::{
7641 dock::{PanelEvent, test::TestPanel},
7642 item::{
7643 ItemEvent,
7644 test::{TestItem, TestProjectItem},
7645 },
7646 };
7647 use fs::FakeFs;
7648 use gpui::{
7649 DismissEvent, Empty, EventEmitter, FocusHandle, Focusable, Render, TestAppContext,
7650 UpdateGlobal, VisualTestContext, px,
7651 };
7652 use project::{Project, ProjectEntryId};
7653 use serde_json::json;
7654 use settings::SettingsStore;
7655
7656 #[gpui::test]
7657 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
7658 init_test(cx);
7659
7660 let fs = FakeFs::new(cx.executor());
7661 let project = Project::test(fs, [], cx).await;
7662 let (workspace, cx) =
7663 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7664
7665 // Adding an item with no ambiguity renders the tab without detail.
7666 let item1 = cx.new(|cx| {
7667 let mut item = TestItem::new(cx);
7668 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
7669 item
7670 });
7671 workspace.update_in(cx, |workspace, window, cx| {
7672 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7673 });
7674 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
7675
7676 // Adding an item that creates ambiguity increases the level of detail on
7677 // both tabs.
7678 let item2 = cx.new_window_entity(|_window, cx| {
7679 let mut item = TestItem::new(cx);
7680 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
7681 item
7682 });
7683 workspace.update_in(cx, |workspace, window, cx| {
7684 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7685 });
7686 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
7687 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
7688
7689 // Adding an item that creates ambiguity increases the level of detail only
7690 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
7691 // we stop at the highest detail available.
7692 let item3 = cx.new(|cx| {
7693 let mut item = TestItem::new(cx);
7694 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
7695 item
7696 });
7697 workspace.update_in(cx, |workspace, window, cx| {
7698 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7699 });
7700 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
7701 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
7702 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
7703 }
7704
7705 #[gpui::test]
7706 async fn test_tracking_active_path(cx: &mut TestAppContext) {
7707 init_test(cx);
7708
7709 let fs = FakeFs::new(cx.executor());
7710 fs.insert_tree(
7711 "/root1",
7712 json!({
7713 "one.txt": "",
7714 "two.txt": "",
7715 }),
7716 )
7717 .await;
7718 fs.insert_tree(
7719 "/root2",
7720 json!({
7721 "three.txt": "",
7722 }),
7723 )
7724 .await;
7725
7726 let project = Project::test(fs, ["root1".as_ref()], cx).await;
7727 let (workspace, cx) =
7728 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7729 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
7730 let worktree_id = project.update(cx, |project, cx| {
7731 project.worktrees(cx).next().unwrap().read(cx).id()
7732 });
7733
7734 let item1 = cx.new(|cx| {
7735 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
7736 });
7737 let item2 = cx.new(|cx| {
7738 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
7739 });
7740
7741 // Add an item to an empty pane
7742 workspace.update_in(cx, |workspace, window, cx| {
7743 workspace.add_item_to_active_pane(Box::new(item1), None, true, window, cx)
7744 });
7745 project.update(cx, |project, cx| {
7746 assert_eq!(
7747 project.active_entry(),
7748 project
7749 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
7750 .map(|e| e.id)
7751 );
7752 });
7753 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
7754
7755 // Add a second item to a non-empty pane
7756 workspace.update_in(cx, |workspace, window, cx| {
7757 workspace.add_item_to_active_pane(Box::new(item2), None, true, window, cx)
7758 });
7759 assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
7760 project.update(cx, |project, cx| {
7761 assert_eq!(
7762 project.active_entry(),
7763 project
7764 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
7765 .map(|e| e.id)
7766 );
7767 });
7768
7769 // Close the active item
7770 pane.update_in(cx, |pane, window, cx| {
7771 pane.close_active_item(&Default::default(), window, cx)
7772 })
7773 .await
7774 .unwrap();
7775 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
7776 project.update(cx, |project, cx| {
7777 assert_eq!(
7778 project.active_entry(),
7779 project
7780 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
7781 .map(|e| e.id)
7782 );
7783 });
7784
7785 // Add a project folder
7786 project
7787 .update(cx, |project, cx| {
7788 project.find_or_create_worktree("root2", true, cx)
7789 })
7790 .await
7791 .unwrap();
7792 assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
7793
7794 // Remove a project folder
7795 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
7796 assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
7797 }
7798
7799 #[gpui::test]
7800 async fn test_close_window(cx: &mut TestAppContext) {
7801 init_test(cx);
7802
7803 let fs = FakeFs::new(cx.executor());
7804 fs.insert_tree("/root", json!({ "one": "" })).await;
7805
7806 let project = Project::test(fs, ["root".as_ref()], cx).await;
7807 let (workspace, cx) =
7808 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7809
7810 // When there are no dirty items, there's nothing to do.
7811 let item1 = cx.new(TestItem::new);
7812 workspace.update_in(cx, |w, window, cx| {
7813 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx)
7814 });
7815 let task = workspace.update_in(cx, |w, window, cx| {
7816 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
7817 });
7818 assert!(task.await.unwrap());
7819
7820 // When there are dirty untitled items, prompt to save each one. If the user
7821 // cancels any prompt, then abort.
7822 let item2 = cx.new(|cx| TestItem::new(cx).with_dirty(true));
7823 let item3 = cx.new(|cx| {
7824 TestItem::new(cx)
7825 .with_dirty(true)
7826 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7827 });
7828 workspace.update_in(cx, |w, window, cx| {
7829 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7830 w.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7831 });
7832 let task = workspace.update_in(cx, |w, window, cx| {
7833 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
7834 });
7835 cx.executor().run_until_parked();
7836 cx.simulate_prompt_answer("Cancel"); // cancel save all
7837 cx.executor().run_until_parked();
7838 assert!(!cx.has_pending_prompt());
7839 assert!(!task.await.unwrap());
7840 }
7841
7842 #[gpui::test]
7843 async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
7844 init_test(cx);
7845
7846 // Register TestItem as a serializable item
7847 cx.update(|cx| {
7848 register_serializable_item::<TestItem>(cx);
7849 });
7850
7851 let fs = FakeFs::new(cx.executor());
7852 fs.insert_tree("/root", json!({ "one": "" })).await;
7853
7854 let project = Project::test(fs, ["root".as_ref()], cx).await;
7855 let (workspace, cx) =
7856 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7857
7858 // When there are dirty untitled items, but they can serialize, then there is no prompt.
7859 let item1 = cx.new(|cx| {
7860 TestItem::new(cx)
7861 .with_dirty(true)
7862 .with_serialize(|| Some(Task::ready(Ok(()))))
7863 });
7864 let item2 = cx.new(|cx| {
7865 TestItem::new(cx)
7866 .with_dirty(true)
7867 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7868 .with_serialize(|| Some(Task::ready(Ok(()))))
7869 });
7870 workspace.update_in(cx, |w, window, cx| {
7871 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7872 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7873 });
7874 let task = workspace.update_in(cx, |w, window, cx| {
7875 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
7876 });
7877 assert!(task.await.unwrap());
7878 }
7879
7880 #[gpui::test]
7881 async fn test_close_pane_items(cx: &mut TestAppContext) {
7882 init_test(cx);
7883
7884 let fs = FakeFs::new(cx.executor());
7885
7886 let project = Project::test(fs, None, cx).await;
7887 let (workspace, cx) =
7888 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7889
7890 let item1 = cx.new(|cx| {
7891 TestItem::new(cx)
7892 .with_dirty(true)
7893 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7894 });
7895 let item2 = cx.new(|cx| {
7896 TestItem::new(cx)
7897 .with_dirty(true)
7898 .with_conflict(true)
7899 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7900 });
7901 let item3 = cx.new(|cx| {
7902 TestItem::new(cx)
7903 .with_dirty(true)
7904 .with_conflict(true)
7905 .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
7906 });
7907 let item4 = cx.new(|cx| {
7908 TestItem::new(cx).with_dirty(true).with_project_items(&[{
7909 let project_item = TestProjectItem::new_untitled(cx);
7910 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
7911 project_item
7912 }])
7913 });
7914 let pane = workspace.update_in(cx, |workspace, window, cx| {
7915 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7916 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7917 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7918 workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, window, cx);
7919 workspace.active_pane().clone()
7920 });
7921
7922 let close_items = pane.update_in(cx, |pane, window, cx| {
7923 pane.activate_item(1, true, true, window, cx);
7924 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7925 let item1_id = item1.item_id();
7926 let item3_id = item3.item_id();
7927 let item4_id = item4.item_id();
7928 pane.close_items(window, cx, SaveIntent::Close, move |id| {
7929 [item1_id, item3_id, item4_id].contains(&id)
7930 })
7931 });
7932 cx.executor().run_until_parked();
7933
7934 assert!(cx.has_pending_prompt());
7935 cx.simulate_prompt_answer("Save all");
7936
7937 cx.executor().run_until_parked();
7938
7939 // Item 1 is saved. There's a prompt to save item 3.
7940 pane.update(cx, |pane, cx| {
7941 assert_eq!(item1.read(cx).save_count, 1);
7942 assert_eq!(item1.read(cx).save_as_count, 0);
7943 assert_eq!(item1.read(cx).reload_count, 0);
7944 assert_eq!(pane.items_len(), 3);
7945 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
7946 });
7947 assert!(cx.has_pending_prompt());
7948
7949 // Cancel saving item 3.
7950 cx.simulate_prompt_answer("Discard");
7951 cx.executor().run_until_parked();
7952
7953 // Item 3 is reloaded. There's a prompt to save item 4.
7954 pane.update(cx, |pane, cx| {
7955 assert_eq!(item3.read(cx).save_count, 0);
7956 assert_eq!(item3.read(cx).save_as_count, 0);
7957 assert_eq!(item3.read(cx).reload_count, 1);
7958 assert_eq!(pane.items_len(), 2);
7959 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
7960 });
7961
7962 // There's a prompt for a path for item 4.
7963 cx.simulate_new_path_selection(|_| Some(Default::default()));
7964 close_items.await.unwrap();
7965
7966 // The requested items are closed.
7967 pane.update(cx, |pane, cx| {
7968 assert_eq!(item4.read(cx).save_count, 0);
7969 assert_eq!(item4.read(cx).save_as_count, 1);
7970 assert_eq!(item4.read(cx).reload_count, 0);
7971 assert_eq!(pane.items_len(), 1);
7972 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7973 });
7974 }
7975
7976 #[gpui::test]
7977 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
7978 init_test(cx);
7979
7980 let fs = FakeFs::new(cx.executor());
7981 let project = Project::test(fs, [], cx).await;
7982 let (workspace, cx) =
7983 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7984
7985 // Create several workspace items with single project entries, and two
7986 // workspace items with multiple project entries.
7987 let single_entry_items = (0..=4)
7988 .map(|project_entry_id| {
7989 cx.new(|cx| {
7990 TestItem::new(cx)
7991 .with_dirty(true)
7992 .with_project_items(&[dirty_project_item(
7993 project_entry_id,
7994 &format!("{project_entry_id}.txt"),
7995 cx,
7996 )])
7997 })
7998 })
7999 .collect::<Vec<_>>();
8000 let item_2_3 = cx.new(|cx| {
8001 TestItem::new(cx)
8002 .with_dirty(true)
8003 .with_singleton(false)
8004 .with_project_items(&[
8005 single_entry_items[2].read(cx).project_items[0].clone(),
8006 single_entry_items[3].read(cx).project_items[0].clone(),
8007 ])
8008 });
8009 let item_3_4 = cx.new(|cx| {
8010 TestItem::new(cx)
8011 .with_dirty(true)
8012 .with_singleton(false)
8013 .with_project_items(&[
8014 single_entry_items[3].read(cx).project_items[0].clone(),
8015 single_entry_items[4].read(cx).project_items[0].clone(),
8016 ])
8017 });
8018
8019 // Create two panes that contain the following project entries:
8020 // left pane:
8021 // multi-entry items: (2, 3)
8022 // single-entry items: 0, 2, 3, 4
8023 // right pane:
8024 // single-entry items: 4, 1
8025 // multi-entry items: (3, 4)
8026 let (left_pane, right_pane) = workspace.update_in(cx, |workspace, window, cx| {
8027 let left_pane = workspace.active_pane().clone();
8028 workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, window, cx);
8029 workspace.add_item_to_active_pane(
8030 single_entry_items[0].boxed_clone(),
8031 None,
8032 true,
8033 window,
8034 cx,
8035 );
8036 workspace.add_item_to_active_pane(
8037 single_entry_items[2].boxed_clone(),
8038 None,
8039 true,
8040 window,
8041 cx,
8042 );
8043 workspace.add_item_to_active_pane(
8044 single_entry_items[3].boxed_clone(),
8045 None,
8046 true,
8047 window,
8048 cx,
8049 );
8050 workspace.add_item_to_active_pane(
8051 single_entry_items[4].boxed_clone(),
8052 None,
8053 true,
8054 window,
8055 cx,
8056 );
8057
8058 let right_pane = workspace
8059 .split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx)
8060 .unwrap();
8061
8062 right_pane.update(cx, |pane, cx| {
8063 pane.add_item(
8064 single_entry_items[1].boxed_clone(),
8065 true,
8066 true,
8067 None,
8068 window,
8069 cx,
8070 );
8071 pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx);
8072 });
8073
8074 (left_pane, right_pane)
8075 });
8076
8077 cx.focus(&right_pane);
8078
8079 let mut close = right_pane.update_in(cx, |pane, window, cx| {
8080 pane.close_all_items(&CloseAllItems::default(), window, cx)
8081 .unwrap()
8082 });
8083 cx.executor().run_until_parked();
8084
8085 let msg = cx.pending_prompt().unwrap().0;
8086 assert!(msg.contains("1.txt"));
8087 assert!(!msg.contains("2.txt"));
8088 assert!(!msg.contains("3.txt"));
8089 assert!(!msg.contains("4.txt"));
8090
8091 cx.simulate_prompt_answer("Cancel");
8092 close.await;
8093
8094 left_pane
8095 .update_in(cx, |left_pane, window, cx| {
8096 left_pane.close_item_by_id(
8097 single_entry_items[3].entity_id(),
8098 SaveIntent::Skip,
8099 window,
8100 cx,
8101 )
8102 })
8103 .await
8104 .unwrap();
8105
8106 close = right_pane.update_in(cx, |pane, window, cx| {
8107 pane.close_all_items(&CloseAllItems::default(), window, cx)
8108 .unwrap()
8109 });
8110 cx.executor().run_until_parked();
8111
8112 let details = cx.pending_prompt().unwrap().1;
8113 assert!(details.contains("1.txt"));
8114 assert!(!details.contains("2.txt"));
8115 assert!(details.contains("3.txt"));
8116 // ideally this assertion could be made, but today we can only
8117 // save whole items not project items, so the orphaned item 3 causes
8118 // 4 to be saved too.
8119 // assert!(!details.contains("4.txt"));
8120
8121 cx.simulate_prompt_answer("Save all");
8122
8123 cx.executor().run_until_parked();
8124 close.await;
8125 right_pane.read_with(cx, |pane, _| {
8126 assert_eq!(pane.items_len(), 0);
8127 });
8128 }
8129
8130 #[gpui::test]
8131 async fn test_autosave(cx: &mut gpui::TestAppContext) {
8132 init_test(cx);
8133
8134 let fs = FakeFs::new(cx.executor());
8135 let project = Project::test(fs, [], cx).await;
8136 let (workspace, cx) =
8137 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8138 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
8139
8140 let item = cx.new(|cx| {
8141 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
8142 });
8143 let item_id = item.entity_id();
8144 workspace.update_in(cx, |workspace, window, cx| {
8145 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
8146 });
8147
8148 // Autosave on window change.
8149 item.update(cx, |item, cx| {
8150 SettingsStore::update_global(cx, |settings, cx| {
8151 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
8152 settings.autosave = Some(AutosaveSetting::OnWindowChange);
8153 })
8154 });
8155 item.is_dirty = true;
8156 });
8157
8158 // Deactivating the window saves the file.
8159 cx.deactivate_window();
8160 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
8161
8162 // Re-activating the window doesn't save the file.
8163 cx.update(|window, _| window.activate_window());
8164 cx.executor().run_until_parked();
8165 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
8166
8167 // Autosave on focus change.
8168 item.update_in(cx, |item, window, cx| {
8169 cx.focus_self(window);
8170 SettingsStore::update_global(cx, |settings, cx| {
8171 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
8172 settings.autosave = Some(AutosaveSetting::OnFocusChange);
8173 })
8174 });
8175 item.is_dirty = true;
8176 });
8177
8178 // Blurring the item saves the file.
8179 item.update_in(cx, |_, window, _| window.blur());
8180 cx.executor().run_until_parked();
8181 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
8182
8183 // Deactivating the window still saves the file.
8184 item.update_in(cx, |item, window, cx| {
8185 cx.focus_self(window);
8186 item.is_dirty = true;
8187 });
8188 cx.deactivate_window();
8189 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
8190
8191 // Autosave after delay.
8192 item.update(cx, |item, cx| {
8193 SettingsStore::update_global(cx, |settings, cx| {
8194 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
8195 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
8196 })
8197 });
8198 item.is_dirty = true;
8199 cx.emit(ItemEvent::Edit);
8200 });
8201
8202 // Delay hasn't fully expired, so the file is still dirty and unsaved.
8203 cx.executor().advance_clock(Duration::from_millis(250));
8204 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
8205
8206 // After delay expires, the file is saved.
8207 cx.executor().advance_clock(Duration::from_millis(250));
8208 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
8209
8210 // Autosave on focus change, ensuring closing the tab counts as such.
8211 item.update(cx, |item, cx| {
8212 SettingsStore::update_global(cx, |settings, cx| {
8213 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
8214 settings.autosave = Some(AutosaveSetting::OnFocusChange);
8215 })
8216 });
8217 item.is_dirty = true;
8218 for project_item in &mut item.project_items {
8219 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
8220 }
8221 });
8222
8223 pane.update_in(cx, |pane, window, cx| {
8224 pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
8225 })
8226 .await
8227 .unwrap();
8228 assert!(!cx.has_pending_prompt());
8229 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
8230
8231 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
8232 workspace.update_in(cx, |workspace, window, cx| {
8233 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
8234 });
8235 item.update_in(cx, |item, window, cx| {
8236 item.project_items[0].update(cx, |item, _| {
8237 item.entry_id = None;
8238 });
8239 item.is_dirty = true;
8240 window.blur();
8241 });
8242 cx.run_until_parked();
8243 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
8244
8245 // Ensure autosave is prevented for deleted files also when closing the buffer.
8246 let _close_items = pane.update_in(cx, |pane, window, cx| {
8247 pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
8248 });
8249 cx.run_until_parked();
8250 assert!(cx.has_pending_prompt());
8251 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
8252 }
8253
8254 #[gpui::test]
8255 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
8256 init_test(cx);
8257
8258 let fs = FakeFs::new(cx.executor());
8259
8260 let project = Project::test(fs, [], cx).await;
8261 let (workspace, cx) =
8262 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8263
8264 let item = cx.new(|cx| {
8265 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
8266 });
8267 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
8268 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
8269 let toolbar_notify_count = Rc::new(RefCell::new(0));
8270
8271 workspace.update_in(cx, |workspace, window, cx| {
8272 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
8273 let toolbar_notification_count = toolbar_notify_count.clone();
8274 cx.observe_in(&toolbar, window, move |_, _, _, _| {
8275 *toolbar_notification_count.borrow_mut() += 1
8276 })
8277 .detach();
8278 });
8279
8280 pane.read_with(cx, |pane, _| {
8281 assert!(!pane.can_navigate_backward());
8282 assert!(!pane.can_navigate_forward());
8283 });
8284
8285 item.update_in(cx, |item, _, cx| {
8286 item.set_state("one".to_string(), cx);
8287 });
8288
8289 // Toolbar must be notified to re-render the navigation buttons
8290 assert_eq!(*toolbar_notify_count.borrow(), 1);
8291
8292 pane.read_with(cx, |pane, _| {
8293 assert!(pane.can_navigate_backward());
8294 assert!(!pane.can_navigate_forward());
8295 });
8296
8297 workspace
8298 .update_in(cx, |workspace, window, cx| {
8299 workspace.go_back(pane.downgrade(), window, cx)
8300 })
8301 .await
8302 .unwrap();
8303
8304 assert_eq!(*toolbar_notify_count.borrow(), 2);
8305 pane.read_with(cx, |pane, _| {
8306 assert!(!pane.can_navigate_backward());
8307 assert!(pane.can_navigate_forward());
8308 });
8309 }
8310
8311 #[gpui::test]
8312 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
8313 init_test(cx);
8314 let fs = FakeFs::new(cx.executor());
8315
8316 let project = Project::test(fs, [], cx).await;
8317 let (workspace, cx) =
8318 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8319
8320 let panel = workspace.update_in(cx, |workspace, window, cx| {
8321 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
8322 workspace.add_panel(panel.clone(), window, cx);
8323
8324 workspace
8325 .right_dock()
8326 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
8327
8328 panel
8329 });
8330
8331 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
8332 pane.update_in(cx, |pane, window, cx| {
8333 let item = cx.new(TestItem::new);
8334 pane.add_item(Box::new(item), true, true, None, window, cx);
8335 });
8336
8337 // Transfer focus from center to panel
8338 workspace.update_in(cx, |workspace, window, cx| {
8339 workspace.toggle_panel_focus::<TestPanel>(window, cx);
8340 });
8341
8342 workspace.update_in(cx, |workspace, window, cx| {
8343 assert!(workspace.right_dock().read(cx).is_open());
8344 assert!(!panel.is_zoomed(window, cx));
8345 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8346 });
8347
8348 // Transfer focus from panel to center
8349 workspace.update_in(cx, |workspace, window, cx| {
8350 workspace.toggle_panel_focus::<TestPanel>(window, cx);
8351 });
8352
8353 workspace.update_in(cx, |workspace, window, cx| {
8354 assert!(workspace.right_dock().read(cx).is_open());
8355 assert!(!panel.is_zoomed(window, cx));
8356 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8357 });
8358
8359 // Close the dock
8360 workspace.update_in(cx, |workspace, window, cx| {
8361 workspace.toggle_dock(DockPosition::Right, window, cx);
8362 });
8363
8364 workspace.update_in(cx, |workspace, window, cx| {
8365 assert!(!workspace.right_dock().read(cx).is_open());
8366 assert!(!panel.is_zoomed(window, cx));
8367 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8368 });
8369
8370 // Open the dock
8371 workspace.update_in(cx, |workspace, window, cx| {
8372 workspace.toggle_dock(DockPosition::Right, window, cx);
8373 });
8374
8375 workspace.update_in(cx, |workspace, window, cx| {
8376 assert!(workspace.right_dock().read(cx).is_open());
8377 assert!(!panel.is_zoomed(window, cx));
8378 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8379 });
8380
8381 // Focus and zoom panel
8382 panel.update_in(cx, |panel, window, cx| {
8383 cx.focus_self(window);
8384 panel.set_zoomed(true, window, cx)
8385 });
8386
8387 workspace.update_in(cx, |workspace, window, cx| {
8388 assert!(workspace.right_dock().read(cx).is_open());
8389 assert!(panel.is_zoomed(window, cx));
8390 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8391 });
8392
8393 // Transfer focus to the center closes the dock
8394 workspace.update_in(cx, |workspace, window, cx| {
8395 workspace.toggle_panel_focus::<TestPanel>(window, cx);
8396 });
8397
8398 workspace.update_in(cx, |workspace, window, cx| {
8399 assert!(!workspace.right_dock().read(cx).is_open());
8400 assert!(panel.is_zoomed(window, cx));
8401 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8402 });
8403
8404 // Transferring focus back to the panel keeps it zoomed
8405 workspace.update_in(cx, |workspace, window, cx| {
8406 workspace.toggle_panel_focus::<TestPanel>(window, cx);
8407 });
8408
8409 workspace.update_in(cx, |workspace, window, cx| {
8410 assert!(workspace.right_dock().read(cx).is_open());
8411 assert!(panel.is_zoomed(window, cx));
8412 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8413 });
8414
8415 // Close the dock while it is zoomed
8416 workspace.update_in(cx, |workspace, window, cx| {
8417 workspace.toggle_dock(DockPosition::Right, window, cx)
8418 });
8419
8420 workspace.update_in(cx, |workspace, window, cx| {
8421 assert!(!workspace.right_dock().read(cx).is_open());
8422 assert!(panel.is_zoomed(window, cx));
8423 assert!(workspace.zoomed.is_none());
8424 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8425 });
8426
8427 // Opening the dock, when it's zoomed, retains focus
8428 workspace.update_in(cx, |workspace, window, cx| {
8429 workspace.toggle_dock(DockPosition::Right, window, cx)
8430 });
8431
8432 workspace.update_in(cx, |workspace, window, cx| {
8433 assert!(workspace.right_dock().read(cx).is_open());
8434 assert!(panel.is_zoomed(window, cx));
8435 assert!(workspace.zoomed.is_some());
8436 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8437 });
8438
8439 // Unzoom and close the panel, zoom the active pane.
8440 panel.update_in(cx, |panel, window, cx| panel.set_zoomed(false, window, cx));
8441 workspace.update_in(cx, |workspace, window, cx| {
8442 workspace.toggle_dock(DockPosition::Right, window, cx)
8443 });
8444 pane.update_in(cx, |pane, window, cx| {
8445 pane.toggle_zoom(&Default::default(), window, cx)
8446 });
8447
8448 // Opening a dock unzooms the pane.
8449 workspace.update_in(cx, |workspace, window, cx| {
8450 workspace.toggle_dock(DockPosition::Right, window, cx)
8451 });
8452 workspace.update_in(cx, |workspace, window, cx| {
8453 let pane = pane.read(cx);
8454 assert!(!pane.is_zoomed());
8455 assert!(!pane.focus_handle(cx).is_focused(window));
8456 assert!(workspace.right_dock().read(cx).is_open());
8457 assert!(workspace.zoomed.is_none());
8458 });
8459 }
8460
8461 #[gpui::test]
8462 async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
8463 init_test(cx);
8464
8465 let fs = FakeFs::new(cx.executor());
8466
8467 let project = Project::test(fs, None, cx).await;
8468 let (workspace, cx) =
8469 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8470
8471 // Let's arrange the panes like this:
8472 //
8473 // +-----------------------+
8474 // | top |
8475 // +------+--------+-------+
8476 // | left | center | right |
8477 // +------+--------+-------+
8478 // | bottom |
8479 // +-----------------------+
8480
8481 let top_item = cx.new(|cx| {
8482 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
8483 });
8484 let bottom_item = cx.new(|cx| {
8485 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
8486 });
8487 let left_item = cx.new(|cx| {
8488 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
8489 });
8490 let right_item = cx.new(|cx| {
8491 TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
8492 });
8493 let center_item = cx.new(|cx| {
8494 TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
8495 });
8496
8497 let top_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8498 let top_pane_id = workspace.active_pane().entity_id();
8499 workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, window, cx);
8500 workspace.split_pane(
8501 workspace.active_pane().clone(),
8502 SplitDirection::Down,
8503 window,
8504 cx,
8505 );
8506 top_pane_id
8507 });
8508 let bottom_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8509 let bottom_pane_id = workspace.active_pane().entity_id();
8510 workspace.add_item_to_active_pane(
8511 Box::new(bottom_item.clone()),
8512 None,
8513 false,
8514 window,
8515 cx,
8516 );
8517 workspace.split_pane(
8518 workspace.active_pane().clone(),
8519 SplitDirection::Up,
8520 window,
8521 cx,
8522 );
8523 bottom_pane_id
8524 });
8525 let left_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8526 let left_pane_id = workspace.active_pane().entity_id();
8527 workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, window, cx);
8528 workspace.split_pane(
8529 workspace.active_pane().clone(),
8530 SplitDirection::Right,
8531 window,
8532 cx,
8533 );
8534 left_pane_id
8535 });
8536 let right_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8537 let right_pane_id = workspace.active_pane().entity_id();
8538 workspace.add_item_to_active_pane(
8539 Box::new(right_item.clone()),
8540 None,
8541 false,
8542 window,
8543 cx,
8544 );
8545 workspace.split_pane(
8546 workspace.active_pane().clone(),
8547 SplitDirection::Left,
8548 window,
8549 cx,
8550 );
8551 right_pane_id
8552 });
8553 let center_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8554 let center_pane_id = workspace.active_pane().entity_id();
8555 workspace.add_item_to_active_pane(
8556 Box::new(center_item.clone()),
8557 None,
8558 false,
8559 window,
8560 cx,
8561 );
8562 center_pane_id
8563 });
8564 cx.executor().run_until_parked();
8565
8566 workspace.update_in(cx, |workspace, window, cx| {
8567 assert_eq!(center_pane_id, workspace.active_pane().entity_id());
8568
8569 // Join into next from center pane into right
8570 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
8571 });
8572
8573 workspace.update_in(cx, |workspace, window, cx| {
8574 let active_pane = workspace.active_pane();
8575 assert_eq!(right_pane_id, active_pane.entity_id());
8576 assert_eq!(2, active_pane.read(cx).items_len());
8577 let item_ids_in_pane =
8578 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
8579 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
8580 assert!(item_ids_in_pane.contains(&right_item.item_id()));
8581
8582 // Join into next from right pane into bottom
8583 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
8584 });
8585
8586 workspace.update_in(cx, |workspace, window, cx| {
8587 let active_pane = workspace.active_pane();
8588 assert_eq!(bottom_pane_id, active_pane.entity_id());
8589 assert_eq!(3, active_pane.read(cx).items_len());
8590 let item_ids_in_pane =
8591 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
8592 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
8593 assert!(item_ids_in_pane.contains(&right_item.item_id()));
8594 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
8595
8596 // Join into next from bottom pane into left
8597 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
8598 });
8599
8600 workspace.update_in(cx, |workspace, window, cx| {
8601 let active_pane = workspace.active_pane();
8602 assert_eq!(left_pane_id, active_pane.entity_id());
8603 assert_eq!(4, active_pane.read(cx).items_len());
8604 let item_ids_in_pane =
8605 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
8606 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
8607 assert!(item_ids_in_pane.contains(&right_item.item_id()));
8608 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
8609 assert!(item_ids_in_pane.contains(&left_item.item_id()));
8610
8611 // Join into next from left pane into top
8612 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
8613 });
8614
8615 workspace.update_in(cx, |workspace, window, cx| {
8616 let active_pane = workspace.active_pane();
8617 assert_eq!(top_pane_id, active_pane.entity_id());
8618 assert_eq!(5, active_pane.read(cx).items_len());
8619 let item_ids_in_pane =
8620 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
8621 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
8622 assert!(item_ids_in_pane.contains(&right_item.item_id()));
8623 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
8624 assert!(item_ids_in_pane.contains(&left_item.item_id()));
8625 assert!(item_ids_in_pane.contains(&top_item.item_id()));
8626
8627 // Single pane left: no-op
8628 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx)
8629 });
8630
8631 workspace.update(cx, |workspace, _cx| {
8632 let active_pane = workspace.active_pane();
8633 assert_eq!(top_pane_id, active_pane.entity_id());
8634 });
8635 }
8636
8637 fn add_an_item_to_active_pane(
8638 cx: &mut VisualTestContext,
8639 workspace: &Entity<Workspace>,
8640 item_id: u64,
8641 ) -> Entity<TestItem> {
8642 let item = cx.new(|cx| {
8643 TestItem::new(cx).with_project_items(&[TestProjectItem::new(
8644 item_id,
8645 "item{item_id}.txt",
8646 cx,
8647 )])
8648 });
8649 workspace.update_in(cx, |workspace, window, cx| {
8650 workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx);
8651 });
8652 return item;
8653 }
8654
8655 fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> {
8656 return workspace.update_in(cx, |workspace, window, cx| {
8657 let new_pane = workspace.split_pane(
8658 workspace.active_pane().clone(),
8659 SplitDirection::Right,
8660 window,
8661 cx,
8662 );
8663 new_pane
8664 });
8665 }
8666
8667 #[gpui::test]
8668 async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
8669 init_test(cx);
8670 let fs = FakeFs::new(cx.executor());
8671 let project = Project::test(fs, None, cx).await;
8672 let (workspace, cx) =
8673 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8674
8675 add_an_item_to_active_pane(cx, &workspace, 1);
8676 split_pane(cx, &workspace);
8677 add_an_item_to_active_pane(cx, &workspace, 2);
8678 split_pane(cx, &workspace); // empty pane
8679 split_pane(cx, &workspace);
8680 let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
8681
8682 cx.executor().run_until_parked();
8683
8684 workspace.update(cx, |workspace, cx| {
8685 let num_panes = workspace.panes().len();
8686 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
8687 let active_item = workspace
8688 .active_pane()
8689 .read(cx)
8690 .active_item()
8691 .expect("item is in focus");
8692
8693 assert_eq!(num_panes, 4);
8694 assert_eq!(num_items_in_current_pane, 1);
8695 assert_eq!(active_item.item_id(), last_item.item_id());
8696 });
8697
8698 workspace.update_in(cx, |workspace, window, cx| {
8699 workspace.join_all_panes(window, cx);
8700 });
8701
8702 workspace.update(cx, |workspace, cx| {
8703 let num_panes = workspace.panes().len();
8704 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
8705 let active_item = workspace
8706 .active_pane()
8707 .read(cx)
8708 .active_item()
8709 .expect("item is in focus");
8710
8711 assert_eq!(num_panes, 1);
8712 assert_eq!(num_items_in_current_pane, 3);
8713 assert_eq!(active_item.item_id(), last_item.item_id());
8714 });
8715 }
8716 struct TestModal(FocusHandle);
8717
8718 impl TestModal {
8719 fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
8720 Self(cx.focus_handle())
8721 }
8722 }
8723
8724 impl EventEmitter<DismissEvent> for TestModal {}
8725
8726 impl Focusable for TestModal {
8727 fn focus_handle(&self, _cx: &App) -> FocusHandle {
8728 self.0.clone()
8729 }
8730 }
8731
8732 impl ModalView for TestModal {}
8733
8734 impl Render for TestModal {
8735 fn render(
8736 &mut self,
8737 _window: &mut Window,
8738 _cx: &mut Context<TestModal>,
8739 ) -> impl IntoElement {
8740 div().track_focus(&self.0)
8741 }
8742 }
8743
8744 #[gpui::test]
8745 async fn test_panels(cx: &mut gpui::TestAppContext) {
8746 init_test(cx);
8747 let fs = FakeFs::new(cx.executor());
8748
8749 let project = Project::test(fs, [], cx).await;
8750 let (workspace, cx) =
8751 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8752
8753 let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| {
8754 let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, cx));
8755 workspace.add_panel(panel_1.clone(), window, cx);
8756 workspace.toggle_dock(DockPosition::Left, window, cx);
8757 let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
8758 workspace.add_panel(panel_2.clone(), window, cx);
8759 workspace.toggle_dock(DockPosition::Right, window, cx);
8760
8761 let left_dock = workspace.left_dock();
8762 assert_eq!(
8763 left_dock.read(cx).visible_panel().unwrap().panel_id(),
8764 panel_1.panel_id()
8765 );
8766 assert_eq!(
8767 left_dock.read(cx).active_panel_size(window, cx).unwrap(),
8768 panel_1.size(window, cx)
8769 );
8770
8771 left_dock.update(cx, |left_dock, cx| {
8772 left_dock.resize_active_panel(Some(px(1337.)), window, cx)
8773 });
8774 assert_eq!(
8775 workspace
8776 .right_dock()
8777 .read(cx)
8778 .visible_panel()
8779 .unwrap()
8780 .panel_id(),
8781 panel_2.panel_id(),
8782 );
8783
8784 (panel_1, panel_2)
8785 });
8786
8787 // Move panel_1 to the right
8788 panel_1.update_in(cx, |panel_1, window, cx| {
8789 panel_1.set_position(DockPosition::Right, window, cx)
8790 });
8791
8792 workspace.update_in(cx, |workspace, window, cx| {
8793 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
8794 // Since it was the only panel on the left, the left dock should now be closed.
8795 assert!(!workspace.left_dock().read(cx).is_open());
8796 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
8797 let right_dock = workspace.right_dock();
8798 assert_eq!(
8799 right_dock.read(cx).visible_panel().unwrap().panel_id(),
8800 panel_1.panel_id()
8801 );
8802 assert_eq!(
8803 right_dock.read(cx).active_panel_size(window, cx).unwrap(),
8804 px(1337.)
8805 );
8806
8807 // Now we move panel_2 to the left
8808 panel_2.set_position(DockPosition::Left, window, cx);
8809 });
8810
8811 workspace.update(cx, |workspace, cx| {
8812 // Since panel_2 was not visible on the right, we don't open the left dock.
8813 assert!(!workspace.left_dock().read(cx).is_open());
8814 // And the right dock is unaffected in its displaying of panel_1
8815 assert!(workspace.right_dock().read(cx).is_open());
8816 assert_eq!(
8817 workspace
8818 .right_dock()
8819 .read(cx)
8820 .visible_panel()
8821 .unwrap()
8822 .panel_id(),
8823 panel_1.panel_id(),
8824 );
8825 });
8826
8827 // Move panel_1 back to the left
8828 panel_1.update_in(cx, |panel_1, window, cx| {
8829 panel_1.set_position(DockPosition::Left, window, cx)
8830 });
8831
8832 workspace.update_in(cx, |workspace, window, cx| {
8833 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
8834 let left_dock = workspace.left_dock();
8835 assert!(left_dock.read(cx).is_open());
8836 assert_eq!(
8837 left_dock.read(cx).visible_panel().unwrap().panel_id(),
8838 panel_1.panel_id()
8839 );
8840 assert_eq!(
8841 left_dock.read(cx).active_panel_size(window, cx).unwrap(),
8842 px(1337.)
8843 );
8844 // And the right dock should be closed as it no longer has any panels.
8845 assert!(!workspace.right_dock().read(cx).is_open());
8846
8847 // Now we move panel_1 to the bottom
8848 panel_1.set_position(DockPosition::Bottom, window, cx);
8849 });
8850
8851 workspace.update_in(cx, |workspace, window, cx| {
8852 // Since panel_1 was visible on the left, we close the left dock.
8853 assert!(!workspace.left_dock().read(cx).is_open());
8854 // The bottom dock is sized based on the panel's default size,
8855 // since the panel orientation changed from vertical to horizontal.
8856 let bottom_dock = workspace.bottom_dock();
8857 assert_eq!(
8858 bottom_dock.read(cx).active_panel_size(window, cx).unwrap(),
8859 panel_1.size(window, cx),
8860 );
8861 // Close bottom dock and move panel_1 back to the left.
8862 bottom_dock.update(cx, |bottom_dock, cx| {
8863 bottom_dock.set_open(false, window, cx)
8864 });
8865 panel_1.set_position(DockPosition::Left, window, cx);
8866 });
8867
8868 // Emit activated event on panel 1
8869 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
8870
8871 // Now the left dock is open and panel_1 is active and focused.
8872 workspace.update_in(cx, |workspace, window, cx| {
8873 let left_dock = workspace.left_dock();
8874 assert!(left_dock.read(cx).is_open());
8875 assert_eq!(
8876 left_dock.read(cx).visible_panel().unwrap().panel_id(),
8877 panel_1.panel_id(),
8878 );
8879 assert!(panel_1.focus_handle(cx).is_focused(window));
8880 });
8881
8882 // Emit closed event on panel 2, which is not active
8883 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
8884
8885 // Wo don't close the left dock, because panel_2 wasn't the active panel
8886 workspace.update(cx, |workspace, cx| {
8887 let left_dock = workspace.left_dock();
8888 assert!(left_dock.read(cx).is_open());
8889 assert_eq!(
8890 left_dock.read(cx).visible_panel().unwrap().panel_id(),
8891 panel_1.panel_id(),
8892 );
8893 });
8894
8895 // Emitting a ZoomIn event shows the panel as zoomed.
8896 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
8897 workspace.read_with(cx, |workspace, _| {
8898 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8899 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
8900 });
8901
8902 // Move panel to another dock while it is zoomed
8903 panel_1.update_in(cx, |panel, window, cx| {
8904 panel.set_position(DockPosition::Right, window, cx)
8905 });
8906 workspace.read_with(cx, |workspace, _| {
8907 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8908
8909 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8910 });
8911
8912 // This is a helper for getting a:
8913 // - valid focus on an element,
8914 // - that isn't a part of the panes and panels system of the Workspace,
8915 // - and doesn't trigger the 'on_focus_lost' API.
8916 let focus_other_view = {
8917 let workspace = workspace.clone();
8918 move |cx: &mut VisualTestContext| {
8919 workspace.update_in(cx, |workspace, window, cx| {
8920 if let Some(_) = workspace.active_modal::<TestModal>(cx) {
8921 workspace.toggle_modal(window, cx, TestModal::new);
8922 workspace.toggle_modal(window, cx, TestModal::new);
8923 } else {
8924 workspace.toggle_modal(window, cx, TestModal::new);
8925 }
8926 })
8927 }
8928 };
8929
8930 // If focus is transferred to another view that's not a panel or another pane, we still show
8931 // the panel as zoomed.
8932 focus_other_view(cx);
8933 workspace.read_with(cx, |workspace, _| {
8934 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8935 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8936 });
8937
8938 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
8939 workspace.update_in(cx, |_workspace, window, cx| {
8940 cx.focus_self(window);
8941 });
8942 workspace.read_with(cx, |workspace, _| {
8943 assert_eq!(workspace.zoomed, None);
8944 assert_eq!(workspace.zoomed_position, None);
8945 });
8946
8947 // If focus is transferred again to another view that's not a panel or a pane, we won't
8948 // show the panel as zoomed because it wasn't zoomed before.
8949 focus_other_view(cx);
8950 workspace.read_with(cx, |workspace, _| {
8951 assert_eq!(workspace.zoomed, None);
8952 assert_eq!(workspace.zoomed_position, None);
8953 });
8954
8955 // When the panel is activated, it is zoomed again.
8956 cx.dispatch_action(ToggleRightDock);
8957 workspace.read_with(cx, |workspace, _| {
8958 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8959 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8960 });
8961
8962 // Emitting a ZoomOut event unzooms the panel.
8963 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
8964 workspace.read_with(cx, |workspace, _| {
8965 assert_eq!(workspace.zoomed, None);
8966 assert_eq!(workspace.zoomed_position, None);
8967 });
8968
8969 // Emit closed event on panel 1, which is active
8970 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
8971
8972 // Now the left dock is closed, because panel_1 was the active panel
8973 workspace.update(cx, |workspace, cx| {
8974 let right_dock = workspace.right_dock();
8975 assert!(!right_dock.read(cx).is_open());
8976 });
8977 }
8978
8979 #[gpui::test]
8980 async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
8981 init_test(cx);
8982
8983 let fs = FakeFs::new(cx.background_executor.clone());
8984 let project = Project::test(fs, [], cx).await;
8985 let (workspace, cx) =
8986 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8987 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
8988
8989 let dirty_regular_buffer = cx.new(|cx| {
8990 TestItem::new(cx)
8991 .with_dirty(true)
8992 .with_label("1.txt")
8993 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8994 });
8995 let dirty_regular_buffer_2 = cx.new(|cx| {
8996 TestItem::new(cx)
8997 .with_dirty(true)
8998 .with_label("2.txt")
8999 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
9000 });
9001 let dirty_multi_buffer_with_both = cx.new(|cx| {
9002 TestItem::new(cx)
9003 .with_dirty(true)
9004 .with_singleton(false)
9005 .with_label("Fake Project Search")
9006 .with_project_items(&[
9007 dirty_regular_buffer.read(cx).project_items[0].clone(),
9008 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
9009 ])
9010 });
9011 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
9012 workspace.update_in(cx, |workspace, window, cx| {
9013 workspace.add_item(
9014 pane.clone(),
9015 Box::new(dirty_regular_buffer.clone()),
9016 None,
9017 false,
9018 false,
9019 window,
9020 cx,
9021 );
9022 workspace.add_item(
9023 pane.clone(),
9024 Box::new(dirty_regular_buffer_2.clone()),
9025 None,
9026 false,
9027 false,
9028 window,
9029 cx,
9030 );
9031 workspace.add_item(
9032 pane.clone(),
9033 Box::new(dirty_multi_buffer_with_both.clone()),
9034 None,
9035 false,
9036 false,
9037 window,
9038 cx,
9039 );
9040 });
9041
9042 pane.update_in(cx, |pane, window, cx| {
9043 pane.activate_item(2, true, true, window, cx);
9044 assert_eq!(
9045 pane.active_item().unwrap().item_id(),
9046 multi_buffer_with_both_files_id,
9047 "Should select the multi buffer in the pane"
9048 );
9049 });
9050 let close_all_but_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
9051 pane.close_inactive_items(
9052 &CloseInactiveItems {
9053 save_intent: Some(SaveIntent::Save),
9054 close_pinned: true,
9055 },
9056 window,
9057 cx,
9058 )
9059 });
9060 cx.background_executor.run_until_parked();
9061 assert!(!cx.has_pending_prompt());
9062 close_all_but_multi_buffer_task
9063 .await
9064 .expect("Closing all buffers but the multi buffer failed");
9065 pane.update(cx, |pane, cx| {
9066 assert_eq!(dirty_regular_buffer.read(cx).save_count, 1);
9067 assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
9068 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 1);
9069 assert_eq!(pane.items_len(), 1);
9070 assert_eq!(
9071 pane.active_item().unwrap().item_id(),
9072 multi_buffer_with_both_files_id,
9073 "Should have only the multi buffer left in the pane"
9074 );
9075 assert!(
9076 dirty_multi_buffer_with_both.read(cx).is_dirty,
9077 "The multi buffer containing the unsaved buffer should still be dirty"
9078 );
9079 });
9080
9081 dirty_regular_buffer.update(cx, |buffer, cx| {
9082 buffer.project_items[0].update(cx, |pi, _| pi.is_dirty = true)
9083 });
9084
9085 let close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
9086 pane.close_active_item(
9087 &CloseActiveItem {
9088 save_intent: Some(SaveIntent::Close),
9089 close_pinned: false,
9090 },
9091 window,
9092 cx,
9093 )
9094 });
9095 cx.background_executor.run_until_parked();
9096 assert!(
9097 cx.has_pending_prompt(),
9098 "Dirty multi buffer should prompt a save dialog"
9099 );
9100 cx.simulate_prompt_answer("Save");
9101 cx.background_executor.run_until_parked();
9102 close_multi_buffer_task
9103 .await
9104 .expect("Closing the multi buffer failed");
9105 pane.update(cx, |pane, cx| {
9106 assert_eq!(
9107 dirty_multi_buffer_with_both.read(cx).save_count,
9108 1,
9109 "Multi buffer item should get be saved"
9110 );
9111 // Test impl does not save inner items, so we do not assert them
9112 assert_eq!(
9113 pane.items_len(),
9114 0,
9115 "No more items should be left in the pane"
9116 );
9117 assert!(pane.active_item().is_none());
9118 });
9119 }
9120
9121 #[gpui::test]
9122 async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
9123 cx: &mut TestAppContext,
9124 ) {
9125 init_test(cx);
9126
9127 let fs = FakeFs::new(cx.background_executor.clone());
9128 let project = Project::test(fs, [], cx).await;
9129 let (workspace, cx) =
9130 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
9131 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
9132
9133 let dirty_regular_buffer = cx.new(|cx| {
9134 TestItem::new(cx)
9135 .with_dirty(true)
9136 .with_label("1.txt")
9137 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
9138 });
9139 let dirty_regular_buffer_2 = cx.new(|cx| {
9140 TestItem::new(cx)
9141 .with_dirty(true)
9142 .with_label("2.txt")
9143 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
9144 });
9145 let clear_regular_buffer = cx.new(|cx| {
9146 TestItem::new(cx)
9147 .with_label("3.txt")
9148 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
9149 });
9150
9151 let dirty_multi_buffer_with_both = cx.new(|cx| {
9152 TestItem::new(cx)
9153 .with_dirty(true)
9154 .with_singleton(false)
9155 .with_label("Fake Project Search")
9156 .with_project_items(&[
9157 dirty_regular_buffer.read(cx).project_items[0].clone(),
9158 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
9159 clear_regular_buffer.read(cx).project_items[0].clone(),
9160 ])
9161 });
9162 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
9163 workspace.update_in(cx, |workspace, window, cx| {
9164 workspace.add_item(
9165 pane.clone(),
9166 Box::new(dirty_regular_buffer.clone()),
9167 None,
9168 false,
9169 false,
9170 window,
9171 cx,
9172 );
9173 workspace.add_item(
9174 pane.clone(),
9175 Box::new(dirty_multi_buffer_with_both.clone()),
9176 None,
9177 false,
9178 false,
9179 window,
9180 cx,
9181 );
9182 });
9183
9184 pane.update_in(cx, |pane, window, cx| {
9185 pane.activate_item(1, true, true, window, cx);
9186 assert_eq!(
9187 pane.active_item().unwrap().item_id(),
9188 multi_buffer_with_both_files_id,
9189 "Should select the multi buffer in the pane"
9190 );
9191 });
9192 let _close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
9193 pane.close_active_item(
9194 &CloseActiveItem {
9195 save_intent: None,
9196 close_pinned: false,
9197 },
9198 window,
9199 cx,
9200 )
9201 });
9202 cx.background_executor.run_until_parked();
9203 assert!(
9204 cx.has_pending_prompt(),
9205 "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
9206 );
9207 }
9208
9209 #[gpui::test]
9210 async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
9211 cx: &mut TestAppContext,
9212 ) {
9213 init_test(cx);
9214
9215 let fs = FakeFs::new(cx.background_executor.clone());
9216 let project = Project::test(fs, [], cx).await;
9217 let (workspace, cx) =
9218 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
9219 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
9220
9221 let dirty_regular_buffer = cx.new(|cx| {
9222 TestItem::new(cx)
9223 .with_dirty(true)
9224 .with_label("1.txt")
9225 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
9226 });
9227 let dirty_regular_buffer_2 = cx.new(|cx| {
9228 TestItem::new(cx)
9229 .with_dirty(true)
9230 .with_label("2.txt")
9231 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
9232 });
9233 let clear_regular_buffer = cx.new(|cx| {
9234 TestItem::new(cx)
9235 .with_label("3.txt")
9236 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
9237 });
9238
9239 let dirty_multi_buffer = cx.new(|cx| {
9240 TestItem::new(cx)
9241 .with_dirty(true)
9242 .with_singleton(false)
9243 .with_label("Fake Project Search")
9244 .with_project_items(&[
9245 dirty_regular_buffer.read(cx).project_items[0].clone(),
9246 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
9247 clear_regular_buffer.read(cx).project_items[0].clone(),
9248 ])
9249 });
9250 workspace.update_in(cx, |workspace, window, cx| {
9251 workspace.add_item(
9252 pane.clone(),
9253 Box::new(dirty_regular_buffer.clone()),
9254 None,
9255 false,
9256 false,
9257 window,
9258 cx,
9259 );
9260 workspace.add_item(
9261 pane.clone(),
9262 Box::new(dirty_regular_buffer_2.clone()),
9263 None,
9264 false,
9265 false,
9266 window,
9267 cx,
9268 );
9269 workspace.add_item(
9270 pane.clone(),
9271 Box::new(dirty_multi_buffer.clone()),
9272 None,
9273 false,
9274 false,
9275 window,
9276 cx,
9277 );
9278 });
9279
9280 pane.update_in(cx, |pane, window, cx| {
9281 pane.activate_item(2, true, true, window, cx);
9282 assert_eq!(
9283 pane.active_item().unwrap().item_id(),
9284 dirty_multi_buffer.item_id(),
9285 "Should select the multi buffer in the pane"
9286 );
9287 });
9288 let close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
9289 pane.close_active_item(
9290 &CloseActiveItem {
9291 save_intent: None,
9292 close_pinned: false,
9293 },
9294 window,
9295 cx,
9296 )
9297 });
9298 cx.background_executor.run_until_parked();
9299 assert!(
9300 !cx.has_pending_prompt(),
9301 "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
9302 );
9303 close_multi_buffer_task
9304 .await
9305 .expect("Closing multi buffer failed");
9306 pane.update(cx, |pane, cx| {
9307 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
9308 assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
9309 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
9310 assert_eq!(
9311 pane.items()
9312 .map(|item| item.item_id())
9313 .sorted()
9314 .collect::<Vec<_>>(),
9315 vec![
9316 dirty_regular_buffer.item_id(),
9317 dirty_regular_buffer_2.item_id(),
9318 ],
9319 "Should have no multi buffer left in the pane"
9320 );
9321 assert!(dirty_regular_buffer.read(cx).is_dirty);
9322 assert!(dirty_regular_buffer_2.read(cx).is_dirty);
9323 });
9324 }
9325
9326 #[gpui::test]
9327 async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) {
9328 init_test(cx);
9329 let fs = FakeFs::new(cx.executor());
9330 let project = Project::test(fs, [], cx).await;
9331 let (workspace, cx) =
9332 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
9333
9334 // Add a new panel to the right dock, opening the dock and setting the
9335 // focus to the new panel.
9336 let panel = workspace.update_in(cx, |workspace, window, cx| {
9337 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
9338 workspace.add_panel(panel.clone(), window, cx);
9339
9340 workspace
9341 .right_dock()
9342 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
9343
9344 workspace.toggle_panel_focus::<TestPanel>(window, cx);
9345
9346 panel
9347 });
9348
9349 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
9350 // panel to the next valid position which, in this case, is the left
9351 // dock.
9352 cx.dispatch_action(MoveFocusedPanelToNextPosition);
9353 workspace.update(cx, |workspace, cx| {
9354 assert!(workspace.left_dock().read(cx).is_open());
9355 assert_eq!(panel.read(cx).position, DockPosition::Left);
9356 });
9357
9358 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
9359 // panel to the next valid position which, in this case, is the bottom
9360 // dock.
9361 cx.dispatch_action(MoveFocusedPanelToNextPosition);
9362 workspace.update(cx, |workspace, cx| {
9363 assert!(workspace.bottom_dock().read(cx).is_open());
9364 assert_eq!(panel.read(cx).position, DockPosition::Bottom);
9365 });
9366
9367 // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time
9368 // around moving the panel to its initial position, the right dock.
9369 cx.dispatch_action(MoveFocusedPanelToNextPosition);
9370 workspace.update(cx, |workspace, cx| {
9371 assert!(workspace.right_dock().read(cx).is_open());
9372 assert_eq!(panel.read(cx).position, DockPosition::Right);
9373 });
9374
9375 // Remove focus from the panel, ensuring that, if the panel is not
9376 // focused, the `MoveFocusedPanelToNextPosition` action does not update
9377 // the panel's position, so the panel is still in the right dock.
9378 workspace.update_in(cx, |workspace, window, cx| {
9379 workspace.toggle_panel_focus::<TestPanel>(window, cx);
9380 });
9381
9382 cx.dispatch_action(MoveFocusedPanelToNextPosition);
9383 workspace.update(cx, |workspace, cx| {
9384 assert!(workspace.right_dock().read(cx).is_open());
9385 assert_eq!(panel.read(cx).position, DockPosition::Right);
9386 });
9387 }
9388
9389 #[gpui::test]
9390 async fn test_moving_items_create_panes(cx: &mut TestAppContext) {
9391 init_test(cx);
9392
9393 let fs = FakeFs::new(cx.executor());
9394 let project = Project::test(fs, [], cx).await;
9395 let (workspace, cx) =
9396 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
9397
9398 let item_1 = cx.new(|cx| {
9399 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "first.txt", cx)])
9400 });
9401 workspace.update_in(cx, |workspace, window, cx| {
9402 workspace.add_item_to_active_pane(Box::new(item_1), None, true, window, cx);
9403 workspace.move_item_to_pane_in_direction(
9404 &MoveItemToPaneInDirection {
9405 direction: SplitDirection::Right,
9406 focus: true,
9407 },
9408 window,
9409 cx,
9410 );
9411 workspace.move_item_to_pane_at_index(
9412 &MoveItemToPane {
9413 destination: 3,
9414 focus: true,
9415 },
9416 window,
9417 cx,
9418 );
9419
9420 assert_eq!(workspace.panes.len(), 1, "No new panes were created");
9421 assert_eq!(
9422 pane_items_paths(&workspace.active_pane, cx),
9423 vec!["first.txt".to_string()],
9424 "Single item was not moved anywhere"
9425 );
9426 });
9427
9428 let item_2 = cx.new(|cx| {
9429 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "second.txt", cx)])
9430 });
9431 workspace.update_in(cx, |workspace, window, cx| {
9432 workspace.add_item_to_active_pane(Box::new(item_2), None, true, window, cx);
9433 assert_eq!(
9434 pane_items_paths(&workspace.panes[0], cx),
9435 vec!["first.txt".to_string(), "second.txt".to_string()],
9436 );
9437 workspace.move_item_to_pane_in_direction(
9438 &MoveItemToPaneInDirection {
9439 direction: SplitDirection::Right,
9440 focus: true,
9441 },
9442 window,
9443 cx,
9444 );
9445
9446 assert_eq!(workspace.panes.len(), 2, "A new pane should be created");
9447 assert_eq!(
9448 pane_items_paths(&workspace.panes[0], cx),
9449 vec!["first.txt".to_string()],
9450 "After moving, one item should be left in the original pane"
9451 );
9452 assert_eq!(
9453 pane_items_paths(&workspace.panes[1], cx),
9454 vec!["second.txt".to_string()],
9455 "New item should have been moved to the new pane"
9456 );
9457 });
9458
9459 let item_3 = cx.new(|cx| {
9460 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "third.txt", cx)])
9461 });
9462 workspace.update_in(cx, |workspace, window, cx| {
9463 let original_pane = workspace.panes[0].clone();
9464 workspace.set_active_pane(&original_pane, window, cx);
9465 workspace.add_item_to_active_pane(Box::new(item_3), None, true, window, cx);
9466 assert_eq!(workspace.panes.len(), 2, "No new panes were created");
9467 assert_eq!(
9468 pane_items_paths(&workspace.active_pane, cx),
9469 vec!["first.txt".to_string(), "third.txt".to_string()],
9470 "New pane should be ready to move one item out"
9471 );
9472
9473 workspace.move_item_to_pane_at_index(
9474 &MoveItemToPane {
9475 destination: 3,
9476 focus: true,
9477 },
9478 window,
9479 cx,
9480 );
9481 assert_eq!(workspace.panes.len(), 3, "A new pane should be created");
9482 assert_eq!(
9483 pane_items_paths(&workspace.active_pane, cx),
9484 vec!["first.txt".to_string()],
9485 "After moving, one item should be left in the original pane"
9486 );
9487 assert_eq!(
9488 pane_items_paths(&workspace.panes[1], cx),
9489 vec!["second.txt".to_string()],
9490 "Previously created pane should be unchanged"
9491 );
9492 assert_eq!(
9493 pane_items_paths(&workspace.panes[2], cx),
9494 vec!["third.txt".to_string()],
9495 "New item should have been moved to the new pane"
9496 );
9497 });
9498 }
9499
9500 mod register_project_item_tests {
9501
9502 use super::*;
9503
9504 // View
9505 struct TestPngItemView {
9506 focus_handle: FocusHandle,
9507 }
9508 // Model
9509 struct TestPngItem {}
9510
9511 impl project::ProjectItem for TestPngItem {
9512 fn try_open(
9513 _project: &Entity<Project>,
9514 path: &ProjectPath,
9515 cx: &mut App,
9516 ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
9517 if path.path.extension().unwrap() == "png" {
9518 Some(cx.spawn(async move |cx| cx.new(|_| TestPngItem {})))
9519 } else {
9520 None
9521 }
9522 }
9523
9524 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
9525 None
9526 }
9527
9528 fn project_path(&self, _: &App) -> Option<ProjectPath> {
9529 None
9530 }
9531
9532 fn is_dirty(&self) -> bool {
9533 false
9534 }
9535 }
9536
9537 impl Item for TestPngItemView {
9538 type Event = ();
9539 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
9540 "".into()
9541 }
9542 }
9543 impl EventEmitter<()> for TestPngItemView {}
9544 impl Focusable for TestPngItemView {
9545 fn focus_handle(&self, _cx: &App) -> FocusHandle {
9546 self.focus_handle.clone()
9547 }
9548 }
9549
9550 impl Render for TestPngItemView {
9551 fn render(
9552 &mut self,
9553 _window: &mut Window,
9554 _cx: &mut Context<Self>,
9555 ) -> impl IntoElement {
9556 Empty
9557 }
9558 }
9559
9560 impl ProjectItem for TestPngItemView {
9561 type Item = TestPngItem;
9562
9563 fn for_project_item(
9564 _project: Entity<Project>,
9565 _pane: Option<&Pane>,
9566 _item: Entity<Self::Item>,
9567 _: &mut Window,
9568 cx: &mut Context<Self>,
9569 ) -> Self
9570 where
9571 Self: Sized,
9572 {
9573 Self {
9574 focus_handle: cx.focus_handle(),
9575 }
9576 }
9577 }
9578
9579 // View
9580 struct TestIpynbItemView {
9581 focus_handle: FocusHandle,
9582 }
9583 // Model
9584 struct TestIpynbItem {}
9585
9586 impl project::ProjectItem for TestIpynbItem {
9587 fn try_open(
9588 _project: &Entity<Project>,
9589 path: &ProjectPath,
9590 cx: &mut App,
9591 ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
9592 if path.path.extension().unwrap() == "ipynb" {
9593 Some(cx.spawn(async move |cx| cx.new(|_| TestIpynbItem {})))
9594 } else {
9595 None
9596 }
9597 }
9598
9599 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
9600 None
9601 }
9602
9603 fn project_path(&self, _: &App) -> Option<ProjectPath> {
9604 None
9605 }
9606
9607 fn is_dirty(&self) -> bool {
9608 false
9609 }
9610 }
9611
9612 impl Item for TestIpynbItemView {
9613 type Event = ();
9614 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
9615 "".into()
9616 }
9617 }
9618 impl EventEmitter<()> for TestIpynbItemView {}
9619 impl Focusable for TestIpynbItemView {
9620 fn focus_handle(&self, _cx: &App) -> FocusHandle {
9621 self.focus_handle.clone()
9622 }
9623 }
9624
9625 impl Render for TestIpynbItemView {
9626 fn render(
9627 &mut self,
9628 _window: &mut Window,
9629 _cx: &mut Context<Self>,
9630 ) -> impl IntoElement {
9631 Empty
9632 }
9633 }
9634
9635 impl ProjectItem for TestIpynbItemView {
9636 type Item = TestIpynbItem;
9637
9638 fn for_project_item(
9639 _project: Entity<Project>,
9640 _pane: Option<&Pane>,
9641 _item: Entity<Self::Item>,
9642 _: &mut Window,
9643 cx: &mut Context<Self>,
9644 ) -> Self
9645 where
9646 Self: Sized,
9647 {
9648 Self {
9649 focus_handle: cx.focus_handle(),
9650 }
9651 }
9652 }
9653
9654 struct TestAlternatePngItemView {
9655 focus_handle: FocusHandle,
9656 }
9657
9658 impl Item for TestAlternatePngItemView {
9659 type Event = ();
9660 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
9661 "".into()
9662 }
9663 }
9664
9665 impl EventEmitter<()> for TestAlternatePngItemView {}
9666 impl Focusable for TestAlternatePngItemView {
9667 fn focus_handle(&self, _cx: &App) -> FocusHandle {
9668 self.focus_handle.clone()
9669 }
9670 }
9671
9672 impl Render for TestAlternatePngItemView {
9673 fn render(
9674 &mut self,
9675 _window: &mut Window,
9676 _cx: &mut Context<Self>,
9677 ) -> impl IntoElement {
9678 Empty
9679 }
9680 }
9681
9682 impl ProjectItem for TestAlternatePngItemView {
9683 type Item = TestPngItem;
9684
9685 fn for_project_item(
9686 _project: Entity<Project>,
9687 _pane: Option<&Pane>,
9688 _item: Entity<Self::Item>,
9689 _: &mut Window,
9690 cx: &mut Context<Self>,
9691 ) -> Self
9692 where
9693 Self: Sized,
9694 {
9695 Self {
9696 focus_handle: cx.focus_handle(),
9697 }
9698 }
9699 }
9700
9701 #[gpui::test]
9702 async fn test_register_project_item(cx: &mut TestAppContext) {
9703 init_test(cx);
9704
9705 cx.update(|cx| {
9706 register_project_item::<TestPngItemView>(cx);
9707 register_project_item::<TestIpynbItemView>(cx);
9708 });
9709
9710 let fs = FakeFs::new(cx.executor());
9711 fs.insert_tree(
9712 "/root1",
9713 json!({
9714 "one.png": "BINARYDATAHERE",
9715 "two.ipynb": "{ totally a notebook }",
9716 "three.txt": "editing text, sure why not?"
9717 }),
9718 )
9719 .await;
9720
9721 let project = Project::test(fs, ["root1".as_ref()], cx).await;
9722 let (workspace, cx) =
9723 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
9724
9725 let worktree_id = project.update(cx, |project, cx| {
9726 project.worktrees(cx).next().unwrap().read(cx).id()
9727 });
9728
9729 let handle = workspace
9730 .update_in(cx, |workspace, window, cx| {
9731 let project_path = (worktree_id, "one.png");
9732 workspace.open_path(project_path, None, true, window, cx)
9733 })
9734 .await
9735 .unwrap();
9736
9737 // Now we can check if the handle we got back errored or not
9738 assert_eq!(
9739 handle.to_any().entity_type(),
9740 TypeId::of::<TestPngItemView>()
9741 );
9742
9743 let handle = workspace
9744 .update_in(cx, |workspace, window, cx| {
9745 let project_path = (worktree_id, "two.ipynb");
9746 workspace.open_path(project_path, None, true, window, cx)
9747 })
9748 .await
9749 .unwrap();
9750
9751 assert_eq!(
9752 handle.to_any().entity_type(),
9753 TypeId::of::<TestIpynbItemView>()
9754 );
9755
9756 let handle = workspace
9757 .update_in(cx, |workspace, window, cx| {
9758 let project_path = (worktree_id, "three.txt");
9759 workspace.open_path(project_path, None, true, window, cx)
9760 })
9761 .await;
9762 assert!(handle.is_err());
9763 }
9764
9765 #[gpui::test]
9766 async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
9767 init_test(cx);
9768
9769 cx.update(|cx| {
9770 register_project_item::<TestPngItemView>(cx);
9771 register_project_item::<TestAlternatePngItemView>(cx);
9772 });
9773
9774 let fs = FakeFs::new(cx.executor());
9775 fs.insert_tree(
9776 "/root1",
9777 json!({
9778 "one.png": "BINARYDATAHERE",
9779 "two.ipynb": "{ totally a notebook }",
9780 "three.txt": "editing text, sure why not?"
9781 }),
9782 )
9783 .await;
9784 let project = Project::test(fs, ["root1".as_ref()], cx).await;
9785 let (workspace, cx) =
9786 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
9787 let worktree_id = project.update(cx, |project, cx| {
9788 project.worktrees(cx).next().unwrap().read(cx).id()
9789 });
9790
9791 let handle = workspace
9792 .update_in(cx, |workspace, window, cx| {
9793 let project_path = (worktree_id, "one.png");
9794 workspace.open_path(project_path, None, true, window, cx)
9795 })
9796 .await
9797 .unwrap();
9798
9799 // This _must_ be the second item registered
9800 assert_eq!(
9801 handle.to_any().entity_type(),
9802 TypeId::of::<TestAlternatePngItemView>()
9803 );
9804
9805 let handle = workspace
9806 .update_in(cx, |workspace, window, cx| {
9807 let project_path = (worktree_id, "three.txt");
9808 workspace.open_path(project_path, None, true, window, cx)
9809 })
9810 .await;
9811 assert!(handle.is_err());
9812 }
9813 }
9814
9815 fn pane_items_paths(pane: &Entity<Pane>, cx: &App) -> Vec<String> {
9816 pane.read(cx)
9817 .items()
9818 .flat_map(|item| {
9819 item.project_paths(cx)
9820 .into_iter()
9821 .map(|path| path.path.to_string_lossy().to_string())
9822 })
9823 .collect()
9824 }
9825
9826 pub fn init_test(cx: &mut TestAppContext) {
9827 cx.update(|cx| {
9828 let settings_store = SettingsStore::test(cx);
9829 cx.set_global(settings_store);
9830 theme::init(theme::LoadThemes::JustBase, cx);
9831 language::init(cx);
9832 crate::init_settings(cx);
9833 Project::init_settings(cx);
9834 });
9835 }
9836
9837 fn dirty_project_item(id: u64, path: &str, cx: &mut App) -> Entity<TestProjectItem> {
9838 let item = TestProjectItem::new(id, path, cx);
9839 item.update(cx, |item, _| {
9840 item.is_dirty = true;
9841 });
9842 item
9843 }
9844}