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