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