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