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