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