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