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