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