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