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.emit(Event::ClearActivityIndicator);
5621 cx.propagate();
5622 }
5623 }
5624}
5625
5626fn leader_border_for_pane(
5627 follower_states: &HashMap<CollaboratorId, FollowerState>,
5628 pane: &Entity<Pane>,
5629 _: &Window,
5630 cx: &App,
5631) -> Option<Div> {
5632 let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
5633 if state.pane() == pane {
5634 Some((*leader_id, state))
5635 } else {
5636 None
5637 }
5638 })?;
5639
5640 let mut leader_color = match leader_id {
5641 CollaboratorId::PeerId(leader_peer_id) => {
5642 let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
5643 let leader = room.remote_participant_for_peer_id(leader_peer_id)?;
5644
5645 cx.theme()
5646 .players()
5647 .color_for_participant(leader.participant_index.0)
5648 .cursor
5649 }
5650 CollaboratorId::Agent => cx.theme().players().agent().cursor,
5651 };
5652 leader_color.fade_out(0.3);
5653 Some(
5654 div()
5655 .absolute()
5656 .size_full()
5657 .left_0()
5658 .top_0()
5659 .border_2()
5660 .border_color(leader_color),
5661 )
5662}
5663
5664fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
5665 ZED_WINDOW_POSITION
5666 .zip(*ZED_WINDOW_SIZE)
5667 .map(|(position, size)| Bounds {
5668 origin: position,
5669 size,
5670 })
5671}
5672
5673fn open_items(
5674 serialized_workspace: Option<SerializedWorkspace>,
5675 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
5676 window: &mut Window,
5677 cx: &mut Context<Workspace>,
5678) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> + use<> {
5679 let restored_items = serialized_workspace.map(|serialized_workspace| {
5680 Workspace::load_workspace(
5681 serialized_workspace,
5682 project_paths_to_open
5683 .iter()
5684 .map(|(_, project_path)| project_path)
5685 .cloned()
5686 .collect(),
5687 window,
5688 cx,
5689 )
5690 });
5691
5692 cx.spawn_in(window, async move |workspace, cx| {
5693 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
5694
5695 if let Some(restored_items) = restored_items {
5696 let restored_items = restored_items.await?;
5697
5698 let restored_project_paths = restored_items
5699 .iter()
5700 .filter_map(|item| {
5701 cx.update(|_, cx| item.as_ref()?.project_path(cx))
5702 .ok()
5703 .flatten()
5704 })
5705 .collect::<HashSet<_>>();
5706
5707 for restored_item in restored_items {
5708 opened_items.push(restored_item.map(Ok));
5709 }
5710
5711 project_paths_to_open
5712 .iter_mut()
5713 .for_each(|(_, project_path)| {
5714 if let Some(project_path_to_open) = project_path {
5715 if restored_project_paths.contains(project_path_to_open) {
5716 *project_path = None;
5717 }
5718 }
5719 });
5720 } else {
5721 for _ in 0..project_paths_to_open.len() {
5722 opened_items.push(None);
5723 }
5724 }
5725 assert!(opened_items.len() == project_paths_to_open.len());
5726
5727 let tasks =
5728 project_paths_to_open
5729 .into_iter()
5730 .enumerate()
5731 .map(|(ix, (abs_path, project_path))| {
5732 let workspace = workspace.clone();
5733 cx.spawn(async move |cx| {
5734 let file_project_path = project_path?;
5735 let abs_path_task = workspace.update(cx, |workspace, cx| {
5736 workspace.project().update(cx, |project, cx| {
5737 project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
5738 })
5739 });
5740
5741 // We only want to open file paths here. If one of the items
5742 // here is a directory, it was already opened further above
5743 // with a `find_or_create_worktree`.
5744 if let Ok(task) = abs_path_task {
5745 if task.await.map_or(true, |p| p.is_file()) {
5746 return Some((
5747 ix,
5748 workspace
5749 .update_in(cx, |workspace, window, cx| {
5750 workspace.open_path(
5751 file_project_path,
5752 None,
5753 true,
5754 window,
5755 cx,
5756 )
5757 })
5758 .log_err()?
5759 .await,
5760 ));
5761 }
5762 }
5763 None
5764 })
5765 });
5766
5767 let tasks = tasks.collect::<Vec<_>>();
5768
5769 let tasks = futures::future::join_all(tasks);
5770 for (ix, path_open_result) in tasks.await.into_iter().flatten() {
5771 opened_items[ix] = Some(path_open_result);
5772 }
5773
5774 Ok(opened_items)
5775 })
5776}
5777
5778enum ActivateInDirectionTarget {
5779 Pane(Entity<Pane>),
5780 Dock(Entity<Dock>),
5781}
5782
5783fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncApp) {
5784 workspace
5785 .update(cx, |workspace, _, cx| {
5786 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
5787 struct DatabaseFailedNotification;
5788
5789 workspace.show_notification(
5790 NotificationId::unique::<DatabaseFailedNotification>(),
5791 cx,
5792 |cx| {
5793 cx.new(|cx| {
5794 MessageNotification::new("Failed to load the database file.", cx)
5795 .primary_message("File an Issue")
5796 .primary_icon(IconName::Plus)
5797 .primary_on_click(|window, cx| {
5798 window.dispatch_action(Box::new(FileBugReport), cx)
5799 })
5800 })
5801 },
5802 );
5803 }
5804 })
5805 .log_err();
5806}
5807
5808impl Focusable for Workspace {
5809 fn focus_handle(&self, cx: &App) -> FocusHandle {
5810 self.active_pane.focus_handle(cx)
5811 }
5812}
5813
5814#[derive(Clone)]
5815struct DraggedDock(DockPosition);
5816
5817impl Render for DraggedDock {
5818 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
5819 gpui::Empty
5820 }
5821}
5822
5823impl Render for Workspace {
5824 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
5825 let mut context = KeyContext::new_with_defaults();
5826 context.add("Workspace");
5827 context.set("keyboard_layout", cx.keyboard_layout().name().to_string());
5828 if let Some(status) = self
5829 .debugger_provider
5830 .as_ref()
5831 .and_then(|provider| provider.active_thread_state(cx))
5832 {
5833 match status {
5834 ThreadStatus::Running | ThreadStatus::Stepping => {
5835 context.add("debugger_running");
5836 }
5837 ThreadStatus::Stopped => context.add("debugger_stopped"),
5838 ThreadStatus::Exited | ThreadStatus::Ended => {}
5839 }
5840 }
5841
5842 let centered_layout = self.centered_layout
5843 && self.center.panes().len() == 1
5844 && self.active_item(cx).is_some();
5845 let render_padding = |size| {
5846 (size > 0.0).then(|| {
5847 div()
5848 .h_full()
5849 .w(relative(size))
5850 .bg(cx.theme().colors().editor_background)
5851 .border_color(cx.theme().colors().pane_group_border)
5852 })
5853 };
5854 let paddings = if centered_layout {
5855 let settings = WorkspaceSettings::get_global(cx).centered_layout;
5856 (
5857 render_padding(Self::adjust_padding(settings.left_padding)),
5858 render_padding(Self::adjust_padding(settings.right_padding)),
5859 )
5860 } else {
5861 (None, None)
5862 };
5863 let ui_font = theme::setup_ui_font(window, cx);
5864
5865 let theme = cx.theme().clone();
5866 let colors = theme.colors();
5867 let notification_entities = self
5868 .notifications
5869 .iter()
5870 .map(|(_, notification)| notification.entity_id())
5871 .collect::<Vec<_>>();
5872
5873 client_side_decorations(
5874 self.actions(div(), window, cx)
5875 .key_context(context)
5876 .relative()
5877 .size_full()
5878 .flex()
5879 .flex_col()
5880 .font(ui_font)
5881 .gap_0()
5882 .justify_start()
5883 .items_start()
5884 .text_color(colors.text)
5885 .overflow_hidden()
5886 .children(self.titlebar_item.clone())
5887 .on_modifiers_changed(move |_, _, cx| {
5888 for &id in ¬ification_entities {
5889 cx.notify(id);
5890 }
5891 })
5892 .child(
5893 div()
5894 .size_full()
5895 .relative()
5896 .flex_1()
5897 .flex()
5898 .flex_col()
5899 .child(
5900 div()
5901 .id("workspace")
5902 .bg(colors.background)
5903 .relative()
5904 .flex_1()
5905 .w_full()
5906 .flex()
5907 .flex_col()
5908 .overflow_hidden()
5909 .border_t_1()
5910 .border_b_1()
5911 .border_color(colors.border)
5912 .child({
5913 let this = cx.entity().clone();
5914 canvas(
5915 move |bounds, window, cx| {
5916 this.update(cx, |this, cx| {
5917 let bounds_changed = this.bounds != bounds;
5918 this.bounds = bounds;
5919
5920 if bounds_changed {
5921 this.left_dock.update(cx, |dock, cx| {
5922 dock.clamp_panel_size(
5923 bounds.size.width,
5924 window,
5925 cx,
5926 )
5927 });
5928
5929 this.right_dock.update(cx, |dock, cx| {
5930 dock.clamp_panel_size(
5931 bounds.size.width,
5932 window,
5933 cx,
5934 )
5935 });
5936
5937 this.bottom_dock.update(cx, |dock, cx| {
5938 dock.clamp_panel_size(
5939 bounds.size.height,
5940 window,
5941 cx,
5942 )
5943 });
5944 }
5945 })
5946 },
5947 |_, _, _, _| {},
5948 )
5949 .absolute()
5950 .size_full()
5951 })
5952 .when(self.zoomed.is_none(), |this| {
5953 this.on_drag_move(cx.listener(
5954 move |workspace,
5955 e: &DragMoveEvent<DraggedDock>,
5956 window,
5957 cx| {
5958 if workspace.previous_dock_drag_coordinates
5959 != Some(e.event.position)
5960 {
5961 workspace.previous_dock_drag_coordinates =
5962 Some(e.event.position);
5963 match e.drag(cx).0 {
5964 DockPosition::Left => {
5965 resize_left_dock(
5966 e.event.position.x
5967 - workspace.bounds.left(),
5968 workspace,
5969 window,
5970 cx,
5971 );
5972 }
5973 DockPosition::Right => {
5974 resize_right_dock(
5975 workspace.bounds.right()
5976 - e.event.position.x,
5977 workspace,
5978 window,
5979 cx,
5980 );
5981 }
5982 DockPosition::Bottom => {
5983 resize_bottom_dock(
5984 workspace.bounds.bottom()
5985 - e.event.position.y,
5986 workspace,
5987 window,
5988 cx,
5989 );
5990 }
5991 };
5992 workspace.serialize_workspace(window, cx);
5993 }
5994 },
5995 ))
5996 })
5997 .child({
5998 match self.bottom_dock_layout {
5999 BottomDockLayout::Full => div()
6000 .flex()
6001 .flex_col()
6002 .h_full()
6003 .child(
6004 div()
6005 .flex()
6006 .flex_row()
6007 .flex_1()
6008 .overflow_hidden()
6009 .children(self.render_dock(
6010 DockPosition::Left,
6011 &self.left_dock,
6012 window,
6013 cx,
6014 ))
6015 .child(
6016 div()
6017 .flex()
6018 .flex_col()
6019 .flex_1()
6020 .overflow_hidden()
6021 .child(
6022 h_flex()
6023 .flex_1()
6024 .when_some(
6025 paddings.0,
6026 |this, p| {
6027 this.child(
6028 p.border_r_1(),
6029 )
6030 },
6031 )
6032 .child(self.center.render(
6033 self.zoomed.as_ref(),
6034 &PaneRenderContext {
6035 follower_states:
6036 &self.follower_states,
6037 active_call: self.active_call(),
6038 active_pane: &self.active_pane,
6039 app_state: &self.app_state,
6040 project: &self.project,
6041 workspace: &self.weak_self,
6042 },
6043 window,
6044 cx,
6045 ))
6046 .when_some(
6047 paddings.1,
6048 |this, p| {
6049 this.child(
6050 p.border_l_1(),
6051 )
6052 },
6053 ),
6054 ),
6055 )
6056 .children(self.render_dock(
6057 DockPosition::Right,
6058 &self.right_dock,
6059 window,
6060 cx,
6061 )),
6062 )
6063 .child(div().w_full().children(self.render_dock(
6064 DockPosition::Bottom,
6065 &self.bottom_dock,
6066 window,
6067 cx
6068 ))),
6069
6070 BottomDockLayout::LeftAligned => div()
6071 .flex()
6072 .flex_row()
6073 .h_full()
6074 .child(
6075 div()
6076 .flex()
6077 .flex_col()
6078 .flex_1()
6079 .h_full()
6080 .child(
6081 div()
6082 .flex()
6083 .flex_row()
6084 .flex_1()
6085 .children(self.render_dock(DockPosition::Left, &self.left_dock, window, cx))
6086 .child(
6087 div()
6088 .flex()
6089 .flex_col()
6090 .flex_1()
6091 .overflow_hidden()
6092 .child(
6093 h_flex()
6094 .flex_1()
6095 .when_some(paddings.0, |this, p| this.child(p.border_r_1()))
6096 .child(self.center.render(
6097 self.zoomed.as_ref(),
6098 &PaneRenderContext {
6099 follower_states:
6100 &self.follower_states,
6101 active_call: self.active_call(),
6102 active_pane: &self.active_pane,
6103 app_state: &self.app_state,
6104 project: &self.project,
6105 workspace: &self.weak_self,
6106 },
6107 window,
6108 cx,
6109 ))
6110 .when_some(paddings.1, |this, p| this.child(p.border_l_1())),
6111 )
6112 )
6113 )
6114 .child(
6115 div()
6116 .w_full()
6117 .children(self.render_dock(DockPosition::Bottom, &self.bottom_dock, window, cx))
6118 ),
6119 )
6120 .children(self.render_dock(
6121 DockPosition::Right,
6122 &self.right_dock,
6123 window,
6124 cx,
6125 )),
6126
6127 BottomDockLayout::RightAligned => div()
6128 .flex()
6129 .flex_row()
6130 .h_full()
6131 .children(self.render_dock(
6132 DockPosition::Left,
6133 &self.left_dock,
6134 window,
6135 cx,
6136 ))
6137 .child(
6138 div()
6139 .flex()
6140 .flex_col()
6141 .flex_1()
6142 .h_full()
6143 .child(
6144 div()
6145 .flex()
6146 .flex_row()
6147 .flex_1()
6148 .child(
6149 div()
6150 .flex()
6151 .flex_col()
6152 .flex_1()
6153 .overflow_hidden()
6154 .child(
6155 h_flex()
6156 .flex_1()
6157 .when_some(paddings.0, |this, p| this.child(p.border_r_1()))
6158 .child(self.center.render(
6159 self.zoomed.as_ref(),
6160 &PaneRenderContext {
6161 follower_states:
6162 &self.follower_states,
6163 active_call: self.active_call(),
6164 active_pane: &self.active_pane,
6165 app_state: &self.app_state,
6166 project: &self.project,
6167 workspace: &self.weak_self,
6168 },
6169 window,
6170 cx,
6171 ))
6172 .when_some(paddings.1, |this, p| this.child(p.border_l_1())),
6173 )
6174 )
6175 .children(self.render_dock(DockPosition::Right, &self.right_dock, window, cx))
6176 )
6177 .child(
6178 div()
6179 .w_full()
6180 .children(self.render_dock(DockPosition::Bottom, &self.bottom_dock, window, cx))
6181 ),
6182 ),
6183
6184 BottomDockLayout::Contained => div()
6185 .flex()
6186 .flex_row()
6187 .h_full()
6188 .children(self.render_dock(
6189 DockPosition::Left,
6190 &self.left_dock,
6191 window,
6192 cx,
6193 ))
6194 .child(
6195 div()
6196 .flex()
6197 .flex_col()
6198 .flex_1()
6199 .overflow_hidden()
6200 .child(
6201 h_flex()
6202 .flex_1()
6203 .when_some(paddings.0, |this, p| {
6204 this.child(p.border_r_1())
6205 })
6206 .child(self.center.render(
6207 self.zoomed.as_ref(),
6208 &PaneRenderContext {
6209 follower_states:
6210 &self.follower_states,
6211 active_call: self.active_call(),
6212 active_pane: &self.active_pane,
6213 app_state: &self.app_state,
6214 project: &self.project,
6215 workspace: &self.weak_self,
6216 },
6217 window,
6218 cx,
6219 ))
6220 .when_some(paddings.1, |this, p| {
6221 this.child(p.border_l_1())
6222 }),
6223 )
6224 .children(self.render_dock(
6225 DockPosition::Bottom,
6226 &self.bottom_dock,
6227 window,
6228 cx,
6229 )),
6230 )
6231 .children(self.render_dock(
6232 DockPosition::Right,
6233 &self.right_dock,
6234 window,
6235 cx,
6236 )),
6237 }
6238 })
6239 .children(self.zoomed.as_ref().and_then(|view| {
6240 let zoomed_view = view.upgrade()?;
6241 let div = div()
6242 .occlude()
6243 .absolute()
6244 .overflow_hidden()
6245 .border_color(colors.border)
6246 .bg(colors.background)
6247 .child(zoomed_view)
6248 .inset_0()
6249 .shadow_lg();
6250
6251 Some(match self.zoomed_position {
6252 Some(DockPosition::Left) => div.right_2().border_r_1(),
6253 Some(DockPosition::Right) => div.left_2().border_l_1(),
6254 Some(DockPosition::Bottom) => div.top_2().border_t_1(),
6255 None => {
6256 div.top_2().bottom_2().left_2().right_2().border_1()
6257 }
6258 })
6259 }))
6260 .children(self.render_notifications(window, cx)),
6261 )
6262 .child(self.status_bar.clone())
6263 .child(self.modal_layer.clone())
6264 .child(self.toast_layer.clone()),
6265 ),
6266 window,
6267 cx,
6268 )
6269 }
6270}
6271
6272fn resize_bottom_dock(
6273 new_size: Pixels,
6274 workspace: &mut Workspace,
6275 window: &mut Window,
6276 cx: &mut App,
6277) {
6278 let size =
6279 new_size.min(workspace.bounds.bottom() - RESIZE_HANDLE_SIZE - workspace.bounds.top());
6280 workspace.bottom_dock.update(cx, |bottom_dock, cx| {
6281 if WorkspaceSettings::get_global(cx)
6282 .resize_all_panels_in_dock
6283 .contains(&DockPosition::Bottom)
6284 {
6285 bottom_dock.resize_all_panels(Some(size), window, cx);
6286 } else {
6287 bottom_dock.resize_active_panel(Some(size), window, cx);
6288 }
6289 });
6290}
6291
6292fn resize_right_dock(
6293 new_size: Pixels,
6294 workspace: &mut Workspace,
6295 window: &mut Window,
6296 cx: &mut App,
6297) {
6298 let mut size = new_size.max(workspace.bounds.left() - RESIZE_HANDLE_SIZE);
6299 workspace.left_dock.read_with(cx, |left_dock, cx| {
6300 let left_dock_size = left_dock
6301 .active_panel_size(window, cx)
6302 .unwrap_or(Pixels(0.0));
6303 if left_dock_size + size > workspace.bounds.right() {
6304 size = workspace.bounds.right() - left_dock_size
6305 }
6306 });
6307 workspace.right_dock.update(cx, |right_dock, cx| {
6308 if WorkspaceSettings::get_global(cx)
6309 .resize_all_panels_in_dock
6310 .contains(&DockPosition::Right)
6311 {
6312 right_dock.resize_all_panels(Some(size), window, cx);
6313 } else {
6314 right_dock.resize_active_panel(Some(size), window, cx);
6315 }
6316 });
6317}
6318
6319fn resize_left_dock(
6320 new_size: Pixels,
6321 workspace: &mut Workspace,
6322 window: &mut Window,
6323 cx: &mut App,
6324) {
6325 let size = new_size.min(workspace.bounds.right() - RESIZE_HANDLE_SIZE);
6326
6327 workspace.left_dock.update(cx, |left_dock, cx| {
6328 if WorkspaceSettings::get_global(cx)
6329 .resize_all_panels_in_dock
6330 .contains(&DockPosition::Left)
6331 {
6332 left_dock.resize_all_panels(Some(size), window, cx);
6333 } else {
6334 left_dock.resize_active_panel(Some(size), window, cx);
6335 }
6336 });
6337}
6338
6339impl WorkspaceStore {
6340 pub fn new(client: Arc<Client>, cx: &mut Context<Self>) -> Self {
6341 Self {
6342 workspaces: Default::default(),
6343 _subscriptions: vec![
6344 client.add_request_handler(cx.weak_entity(), Self::handle_follow),
6345 client.add_message_handler(cx.weak_entity(), Self::handle_update_followers),
6346 ],
6347 client,
6348 }
6349 }
6350
6351 pub fn update_followers(
6352 &self,
6353 project_id: Option<u64>,
6354 update: proto::update_followers::Variant,
6355 cx: &App,
6356 ) -> Option<()> {
6357 let active_call = ActiveCall::try_global(cx)?;
6358 let room_id = active_call.read(cx).room()?.read(cx).id();
6359 self.client
6360 .send(proto::UpdateFollowers {
6361 room_id,
6362 project_id,
6363 variant: Some(update),
6364 })
6365 .log_err()
6366 }
6367
6368 pub async fn handle_follow(
6369 this: Entity<Self>,
6370 envelope: TypedEnvelope<proto::Follow>,
6371 mut cx: AsyncApp,
6372 ) -> Result<proto::FollowResponse> {
6373 this.update(&mut cx, |this, cx| {
6374 let follower = Follower {
6375 project_id: envelope.payload.project_id,
6376 peer_id: envelope.original_sender_id()?,
6377 };
6378
6379 let mut response = proto::FollowResponse::default();
6380 this.workspaces.retain(|workspace| {
6381 workspace
6382 .update(cx, |workspace, window, cx| {
6383 let handler_response =
6384 workspace.handle_follow(follower.project_id, window, cx);
6385 if let Some(active_view) = handler_response.active_view.clone() {
6386 if workspace.project.read(cx).remote_id() == follower.project_id {
6387 response.active_view = Some(active_view)
6388 }
6389 }
6390 })
6391 .is_ok()
6392 });
6393
6394 Ok(response)
6395 })?
6396 }
6397
6398 async fn handle_update_followers(
6399 this: Entity<Self>,
6400 envelope: TypedEnvelope<proto::UpdateFollowers>,
6401 mut cx: AsyncApp,
6402 ) -> Result<()> {
6403 let leader_id = envelope.original_sender_id()?;
6404 let update = envelope.payload;
6405
6406 this.update(&mut cx, |this, cx| {
6407 this.workspaces.retain(|workspace| {
6408 workspace
6409 .update(cx, |workspace, window, cx| {
6410 let project_id = workspace.project.read(cx).remote_id();
6411 if update.project_id != project_id && update.project_id.is_some() {
6412 return;
6413 }
6414 workspace.handle_update_followers(leader_id, update.clone(), window, cx);
6415 })
6416 .is_ok()
6417 });
6418 Ok(())
6419 })?
6420 }
6421}
6422
6423impl ViewId {
6424 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
6425 Ok(Self {
6426 creator: message
6427 .creator
6428 .map(CollaboratorId::PeerId)
6429 .context("creator is missing")?,
6430 id: message.id,
6431 })
6432 }
6433
6434 pub(crate) fn to_proto(self) -> Option<proto::ViewId> {
6435 if let CollaboratorId::PeerId(peer_id) = self.creator {
6436 Some(proto::ViewId {
6437 creator: Some(peer_id),
6438 id: self.id,
6439 })
6440 } else {
6441 None
6442 }
6443 }
6444}
6445
6446impl FollowerState {
6447 fn pane(&self) -> &Entity<Pane> {
6448 self.dock_pane.as_ref().unwrap_or(&self.center_pane)
6449 }
6450}
6451
6452pub trait WorkspaceHandle {
6453 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath>;
6454}
6455
6456impl WorkspaceHandle for Entity<Workspace> {
6457 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath> {
6458 self.read(cx)
6459 .worktrees(cx)
6460 .flat_map(|worktree| {
6461 let worktree_id = worktree.read(cx).id();
6462 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
6463 worktree_id,
6464 path: f.path.clone(),
6465 })
6466 })
6467 .collect::<Vec<_>>()
6468 }
6469}
6470
6471impl std::fmt::Debug for OpenPaths {
6472 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
6473 f.debug_struct("OpenPaths")
6474 .field("paths", &self.paths)
6475 .finish()
6476 }
6477}
6478
6479pub async fn last_opened_workspace_location() -> Option<SerializedWorkspaceLocation> {
6480 DB.last_workspace().await.log_err().flatten()
6481}
6482
6483pub fn last_session_workspace_locations(
6484 last_session_id: &str,
6485 last_session_window_stack: Option<Vec<WindowId>>,
6486) -> Option<Vec<SerializedWorkspaceLocation>> {
6487 DB.last_session_workspace_locations(last_session_id, last_session_window_stack)
6488 .log_err()
6489}
6490
6491actions!(
6492 collab,
6493 [
6494 /// Opens the channel notes for the current call.
6495 ///
6496 /// If you want to open a specific channel, use `zed::OpenZedUrl` with a channel notes URL -
6497 /// can be copied via "Copy link to section" in the context menu of the channel notes
6498 /// buffer. These URLs look like `https://zed.dev/channel/channel-name-CHANNEL_ID/notes`.
6499 OpenChannelNotes,
6500 Mute,
6501 Deafen,
6502 LeaveCall,
6503 ShareProject,
6504 ScreenShare
6505 ]
6506);
6507actions!(zed, [OpenLog]);
6508
6509async fn join_channel_internal(
6510 channel_id: ChannelId,
6511 app_state: &Arc<AppState>,
6512 requesting_window: Option<WindowHandle<Workspace>>,
6513 active_call: &Entity<ActiveCall>,
6514 cx: &mut AsyncApp,
6515) -> Result<bool> {
6516 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
6517 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
6518 return (false, None);
6519 };
6520
6521 let already_in_channel = room.channel_id() == Some(channel_id);
6522 let should_prompt = room.is_sharing_project()
6523 && !room.remote_participants().is_empty()
6524 && !already_in_channel;
6525 let open_room = if already_in_channel {
6526 active_call.room().cloned()
6527 } else {
6528 None
6529 };
6530 (should_prompt, open_room)
6531 })?;
6532
6533 if let Some(room) = open_room {
6534 let task = room.update(cx, |room, cx| {
6535 if let Some((project, host)) = room.most_active_project(cx) {
6536 return Some(join_in_room_project(project, host, app_state.clone(), cx));
6537 }
6538
6539 None
6540 })?;
6541 if let Some(task) = task {
6542 task.await?;
6543 }
6544 return anyhow::Ok(true);
6545 }
6546
6547 if should_prompt {
6548 if let Some(workspace) = requesting_window {
6549 let answer = workspace
6550 .update(cx, |_, window, cx| {
6551 window.prompt(
6552 PromptLevel::Warning,
6553 "Do you want to switch channels?",
6554 Some("Leaving this call will unshare your current project."),
6555 &["Yes, Join Channel", "Cancel"],
6556 cx,
6557 )
6558 })?
6559 .await;
6560
6561 if answer == Ok(1) {
6562 return Ok(false);
6563 }
6564 } else {
6565 return Ok(false); // unreachable!() hopefully
6566 }
6567 }
6568
6569 let client = cx.update(|cx| active_call.read(cx).client())?;
6570
6571 let mut client_status = client.status();
6572
6573 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
6574 'outer: loop {
6575 let Some(status) = client_status.recv().await else {
6576 anyhow::bail!("error connecting");
6577 };
6578
6579 match status {
6580 Status::Connecting
6581 | Status::Authenticating
6582 | Status::Reconnecting
6583 | Status::Reauthenticating => continue,
6584 Status::Connected { .. } => break 'outer,
6585 Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
6586 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
6587 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
6588 return Err(ErrorCode::Disconnected.into());
6589 }
6590 }
6591 }
6592
6593 let room = active_call
6594 .update(cx, |active_call, cx| {
6595 active_call.join_channel(channel_id, cx)
6596 })?
6597 .await?;
6598
6599 let Some(room) = room else {
6600 return anyhow::Ok(true);
6601 };
6602
6603 room.update(cx, |room, _| room.room_update_completed())?
6604 .await;
6605
6606 let task = room.update(cx, |room, cx| {
6607 if let Some((project, host)) = room.most_active_project(cx) {
6608 return Some(join_in_room_project(project, host, app_state.clone(), cx));
6609 }
6610
6611 // If you are the first to join a channel, see if you should share your project.
6612 if room.remote_participants().is_empty() && !room.local_participant_is_guest() {
6613 if let Some(workspace) = requesting_window {
6614 let project = workspace.update(cx, |workspace, _, cx| {
6615 let project = workspace.project.read(cx);
6616
6617 if !CallSettings::get_global(cx).share_on_join {
6618 return None;
6619 }
6620
6621 if (project.is_local() || project.is_via_ssh())
6622 && project.visible_worktrees(cx).any(|tree| {
6623 tree.read(cx)
6624 .root_entry()
6625 .map_or(false, |entry| entry.is_dir())
6626 })
6627 {
6628 Some(workspace.project.clone())
6629 } else {
6630 None
6631 }
6632 });
6633 if let Ok(Some(project)) = project {
6634 return Some(cx.spawn(async move |room, cx| {
6635 room.update(cx, |room, cx| room.share_project(project, cx))?
6636 .await?;
6637 Ok(())
6638 }));
6639 }
6640 }
6641 }
6642
6643 None
6644 })?;
6645 if let Some(task) = task {
6646 task.await?;
6647 return anyhow::Ok(true);
6648 }
6649 anyhow::Ok(false)
6650}
6651
6652pub fn join_channel(
6653 channel_id: ChannelId,
6654 app_state: Arc<AppState>,
6655 requesting_window: Option<WindowHandle<Workspace>>,
6656 cx: &mut App,
6657) -> Task<Result<()>> {
6658 let active_call = ActiveCall::global(cx);
6659 cx.spawn(async move |cx| {
6660 let result = join_channel_internal(
6661 channel_id,
6662 &app_state,
6663 requesting_window,
6664 &active_call,
6665 cx,
6666 )
6667 .await;
6668
6669 // join channel succeeded, and opened a window
6670 if matches!(result, Ok(true)) {
6671 return anyhow::Ok(());
6672 }
6673
6674 // find an existing workspace to focus and show call controls
6675 let mut active_window =
6676 requesting_window.or_else(|| activate_any_workspace_window( cx));
6677 if active_window.is_none() {
6678 // no open workspaces, make one to show the error in (blergh)
6679 let (window_handle, _) = cx
6680 .update(|cx| {
6681 Workspace::new_local(vec![], app_state.clone(), requesting_window, None, cx)
6682 })?
6683 .await?;
6684
6685 if result.is_ok() {
6686 cx.update(|cx| {
6687 cx.dispatch_action(&OpenChannelNotes);
6688 }).log_err();
6689 }
6690
6691 active_window = Some(window_handle);
6692 }
6693
6694 if let Err(err) = result {
6695 log::error!("failed to join channel: {}", err);
6696 if let Some(active_window) = active_window {
6697 active_window
6698 .update(cx, |_, window, cx| {
6699 let detail: SharedString = match err.error_code() {
6700 ErrorCode::SignedOut => {
6701 "Please sign in to continue.".into()
6702 }
6703 ErrorCode::UpgradeRequired => {
6704 "Your are running an unsupported version of Zed. Please update to continue.".into()
6705 }
6706 ErrorCode::NoSuchChannel => {
6707 "No matching channel was found. Please check the link and try again.".into()
6708 }
6709 ErrorCode::Forbidden => {
6710 "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
6711 }
6712 ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
6713 _ => format!("{}\n\nPlease try again.", err).into(),
6714 };
6715 window.prompt(
6716 PromptLevel::Critical,
6717 "Failed to join channel",
6718 Some(&detail),
6719 &["Ok"],
6720 cx)
6721 })?
6722 .await
6723 .ok();
6724 }
6725 }
6726
6727 // return ok, we showed the error to the user.
6728 anyhow::Ok(())
6729 })
6730}
6731
6732pub async fn get_any_active_workspace(
6733 app_state: Arc<AppState>,
6734 mut cx: AsyncApp,
6735) -> anyhow::Result<WindowHandle<Workspace>> {
6736 // find an existing workspace to focus and show call controls
6737 let active_window = activate_any_workspace_window(&mut cx);
6738 if active_window.is_none() {
6739 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, cx))?
6740 .await?;
6741 }
6742 activate_any_workspace_window(&mut cx).context("could not open zed")
6743}
6744
6745fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<Workspace>> {
6746 cx.update(|cx| {
6747 if let Some(workspace_window) = cx
6748 .active_window()
6749 .and_then(|window| window.downcast::<Workspace>())
6750 {
6751 return Some(workspace_window);
6752 }
6753
6754 for window in cx.windows() {
6755 if let Some(workspace_window) = window.downcast::<Workspace>() {
6756 workspace_window
6757 .update(cx, |_, window, _| window.activate_window())
6758 .ok();
6759 return Some(workspace_window);
6760 }
6761 }
6762 None
6763 })
6764 .ok()
6765 .flatten()
6766}
6767
6768pub fn local_workspace_windows(cx: &App) -> Vec<WindowHandle<Workspace>> {
6769 cx.windows()
6770 .into_iter()
6771 .filter_map(|window| window.downcast::<Workspace>())
6772 .filter(|workspace| {
6773 workspace
6774 .read(cx)
6775 .is_ok_and(|workspace| workspace.project.read(cx).is_local())
6776 })
6777 .collect()
6778}
6779
6780#[derive(Default)]
6781pub struct OpenOptions {
6782 pub visible: Option<OpenVisible>,
6783 pub focus: Option<bool>,
6784 pub open_new_workspace: Option<bool>,
6785 pub replace_window: Option<WindowHandle<Workspace>>,
6786 pub env: Option<HashMap<String, String>>,
6787}
6788
6789#[allow(clippy::type_complexity)]
6790pub fn open_paths(
6791 abs_paths: &[PathBuf],
6792 app_state: Arc<AppState>,
6793 open_options: OpenOptions,
6794 cx: &mut App,
6795) -> Task<
6796 anyhow::Result<(
6797 WindowHandle<Workspace>,
6798 Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>>,
6799 )>,
6800> {
6801 let abs_paths = abs_paths.to_vec();
6802 let mut existing = None;
6803 let mut best_match = None;
6804 let mut open_visible = OpenVisible::All;
6805
6806 cx.spawn(async move |cx| {
6807 if open_options.open_new_workspace != Some(true) {
6808 let all_paths = abs_paths.iter().map(|path| app_state.fs.metadata(path));
6809 let all_metadatas = futures::future::join_all(all_paths)
6810 .await
6811 .into_iter()
6812 .filter_map(|result| result.ok().flatten())
6813 .collect::<Vec<_>>();
6814
6815 cx.update(|cx| {
6816 for window in local_workspace_windows(&cx) {
6817 if let Ok(workspace) = window.read(&cx) {
6818 let m = workspace.project.read(&cx).visibility_for_paths(
6819 &abs_paths,
6820 &all_metadatas,
6821 open_options.open_new_workspace == None,
6822 cx,
6823 );
6824 if m > best_match {
6825 existing = Some(window);
6826 best_match = m;
6827 } else if best_match.is_none()
6828 && open_options.open_new_workspace == Some(false)
6829 {
6830 existing = Some(window)
6831 }
6832 }
6833 }
6834 })?;
6835
6836 if open_options.open_new_workspace.is_none() && existing.is_none() {
6837 if all_metadatas.iter().all(|file| !file.is_dir) {
6838 cx.update(|cx| {
6839 if let Some(window) = cx
6840 .active_window()
6841 .and_then(|window| window.downcast::<Workspace>())
6842 {
6843 if let Ok(workspace) = window.read(cx) {
6844 let project = workspace.project().read(cx);
6845 if project.is_local() && !project.is_via_collab() {
6846 existing = Some(window);
6847 open_visible = OpenVisible::None;
6848 return;
6849 }
6850 }
6851 }
6852 for window in local_workspace_windows(cx) {
6853 if let Ok(workspace) = window.read(cx) {
6854 let project = workspace.project().read(cx);
6855 if project.is_via_collab() {
6856 continue;
6857 }
6858 existing = Some(window);
6859 open_visible = OpenVisible::None;
6860 break;
6861 }
6862 }
6863 })?;
6864 }
6865 }
6866 }
6867
6868 if let Some(existing) = existing {
6869 let open_task = existing
6870 .update(cx, |workspace, window, cx| {
6871 window.activate_window();
6872 workspace.open_paths(
6873 abs_paths,
6874 OpenOptions {
6875 visible: Some(open_visible),
6876 ..Default::default()
6877 },
6878 None,
6879 window,
6880 cx,
6881 )
6882 })?
6883 .await;
6884
6885 _ = existing.update(cx, |workspace, _, cx| {
6886 for item in open_task.iter().flatten() {
6887 if let Err(e) = item {
6888 workspace.show_error(&e, cx);
6889 }
6890 }
6891 });
6892
6893 Ok((existing, open_task))
6894 } else {
6895 cx.update(move |cx| {
6896 Workspace::new_local(
6897 abs_paths,
6898 app_state.clone(),
6899 open_options.replace_window,
6900 open_options.env,
6901 cx,
6902 )
6903 })?
6904 .await
6905 }
6906 })
6907}
6908
6909pub fn open_new(
6910 open_options: OpenOptions,
6911 app_state: Arc<AppState>,
6912 cx: &mut App,
6913 init: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + 'static + Send,
6914) -> Task<anyhow::Result<()>> {
6915 let task = Workspace::new_local(Vec::new(), app_state, None, open_options.env, cx);
6916 cx.spawn(async move |cx| {
6917 let (workspace, opened_paths) = task.await?;
6918 workspace.update(cx, |workspace, window, cx| {
6919 if opened_paths.is_empty() {
6920 init(workspace, window, cx)
6921 }
6922 })?;
6923 Ok(())
6924 })
6925}
6926
6927pub fn create_and_open_local_file(
6928 path: &'static Path,
6929 window: &mut Window,
6930 cx: &mut Context<Workspace>,
6931 default_content: impl 'static + Send + FnOnce() -> Rope,
6932) -> Task<Result<Box<dyn ItemHandle>>> {
6933 cx.spawn_in(window, async move |workspace, cx| {
6934 let fs = workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?;
6935 if !fs.is_file(path).await {
6936 fs.create_file(path, Default::default()).await?;
6937 fs.save(path, &default_content(), Default::default())
6938 .await?;
6939 }
6940
6941 let mut items = workspace
6942 .update_in(cx, |workspace, window, cx| {
6943 workspace.with_local_workspace(window, cx, |workspace, window, cx| {
6944 workspace.open_paths(
6945 vec![path.to_path_buf()],
6946 OpenOptions {
6947 visible: Some(OpenVisible::None),
6948 ..Default::default()
6949 },
6950 None,
6951 window,
6952 cx,
6953 )
6954 })
6955 })?
6956 .await?
6957 .await;
6958
6959 let item = items.pop().flatten();
6960 item.with_context(|| format!("path {path:?} is not a file"))?
6961 })
6962}
6963
6964pub fn open_ssh_project_with_new_connection(
6965 window: WindowHandle<Workspace>,
6966 connection_options: SshConnectionOptions,
6967 cancel_rx: oneshot::Receiver<()>,
6968 delegate: Arc<dyn SshClientDelegate>,
6969 app_state: Arc<AppState>,
6970 paths: Vec<PathBuf>,
6971 cx: &mut App,
6972) -> Task<Result<()>> {
6973 cx.spawn(async move |cx| {
6974 let (serialized_ssh_project, workspace_id, serialized_workspace) =
6975 serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
6976
6977 let session = match cx
6978 .update(|cx| {
6979 remote::SshRemoteClient::new(
6980 ConnectionIdentifier::Workspace(workspace_id.0),
6981 connection_options,
6982 cancel_rx,
6983 delegate,
6984 cx,
6985 )
6986 })?
6987 .await?
6988 {
6989 Some(result) => result,
6990 None => return Ok(()),
6991 };
6992
6993 let project = cx.update(|cx| {
6994 project::Project::ssh(
6995 session,
6996 app_state.client.clone(),
6997 app_state.node_runtime.clone(),
6998 app_state.user_store.clone(),
6999 app_state.languages.clone(),
7000 app_state.fs.clone(),
7001 cx,
7002 )
7003 })?;
7004
7005 open_ssh_project_inner(
7006 project,
7007 paths,
7008 serialized_ssh_project,
7009 workspace_id,
7010 serialized_workspace,
7011 app_state,
7012 window,
7013 cx,
7014 )
7015 .await
7016 })
7017}
7018
7019pub fn open_ssh_project_with_existing_connection(
7020 connection_options: SshConnectionOptions,
7021 project: Entity<Project>,
7022 paths: Vec<PathBuf>,
7023 app_state: Arc<AppState>,
7024 window: WindowHandle<Workspace>,
7025 cx: &mut AsyncApp,
7026) -> Task<Result<()>> {
7027 cx.spawn(async move |cx| {
7028 let (serialized_ssh_project, workspace_id, serialized_workspace) =
7029 serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
7030
7031 open_ssh_project_inner(
7032 project,
7033 paths,
7034 serialized_ssh_project,
7035 workspace_id,
7036 serialized_workspace,
7037 app_state,
7038 window,
7039 cx,
7040 )
7041 .await
7042 })
7043}
7044
7045async fn open_ssh_project_inner(
7046 project: Entity<Project>,
7047 paths: Vec<PathBuf>,
7048 serialized_ssh_project: SerializedSshProject,
7049 workspace_id: WorkspaceId,
7050 serialized_workspace: Option<SerializedWorkspace>,
7051 app_state: Arc<AppState>,
7052 window: WindowHandle<Workspace>,
7053 cx: &mut AsyncApp,
7054) -> Result<()> {
7055 let toolchains = DB.toolchains(workspace_id).await?;
7056 for (toolchain, worktree_id, path) in toolchains {
7057 project
7058 .update(cx, |this, cx| {
7059 this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx)
7060 })?
7061 .await;
7062 }
7063 let mut project_paths_to_open = vec![];
7064 let mut project_path_errors = vec![];
7065
7066 for path in paths {
7067 let result = cx
7068 .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))?
7069 .await;
7070 match result {
7071 Ok((_, project_path)) => {
7072 project_paths_to_open.push((path.clone(), Some(project_path)));
7073 }
7074 Err(error) => {
7075 project_path_errors.push(error);
7076 }
7077 };
7078 }
7079
7080 if project_paths_to_open.is_empty() {
7081 return Err(project_path_errors.pop().context("no paths given")?);
7082 }
7083
7084 cx.update_window(window.into(), |_, window, cx| {
7085 window.replace_root(cx, |window, cx| {
7086 telemetry::event!("SSH Project Opened");
7087
7088 let mut workspace =
7089 Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
7090 workspace.set_serialized_ssh_project(serialized_ssh_project);
7091 workspace.update_history(cx);
7092
7093 if let Some(ref serialized) = serialized_workspace {
7094 workspace.centered_layout = serialized.centered_layout;
7095 }
7096
7097 workspace
7098 });
7099 })?;
7100
7101 window
7102 .update(cx, |_, window, cx| {
7103 window.activate_window();
7104 open_items(serialized_workspace, project_paths_to_open, window, cx)
7105 })?
7106 .await?;
7107
7108 window.update(cx, |workspace, _, cx| {
7109 for error in project_path_errors {
7110 if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
7111 if let Some(path) = error.error_tag("path") {
7112 workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
7113 }
7114 } else {
7115 workspace.show_error(&error, cx)
7116 }
7117 }
7118 })?;
7119
7120 Ok(())
7121}
7122
7123fn serialize_ssh_project(
7124 connection_options: SshConnectionOptions,
7125 paths: Vec<PathBuf>,
7126 cx: &AsyncApp,
7127) -> Task<
7128 Result<(
7129 SerializedSshProject,
7130 WorkspaceId,
7131 Option<SerializedWorkspace>,
7132 )>,
7133> {
7134 cx.background_spawn(async move {
7135 let serialized_ssh_project = persistence::DB
7136 .get_or_create_ssh_project(
7137 connection_options.host.clone(),
7138 connection_options.port,
7139 paths
7140 .iter()
7141 .map(|path| path.to_string_lossy().to_string())
7142 .collect::<Vec<_>>(),
7143 connection_options.username.clone(),
7144 )
7145 .await?;
7146
7147 let serialized_workspace =
7148 persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
7149
7150 let workspace_id = if let Some(workspace_id) =
7151 serialized_workspace.as_ref().map(|workspace| workspace.id)
7152 {
7153 workspace_id
7154 } else {
7155 persistence::DB.next_id().await?
7156 };
7157
7158 Ok((serialized_ssh_project, workspace_id, serialized_workspace))
7159 })
7160}
7161
7162pub fn join_in_room_project(
7163 project_id: u64,
7164 follow_user_id: u64,
7165 app_state: Arc<AppState>,
7166 cx: &mut App,
7167) -> Task<Result<()>> {
7168 let windows = cx.windows();
7169 cx.spawn(async move |cx| {
7170 let existing_workspace = windows.into_iter().find_map(|window_handle| {
7171 window_handle
7172 .downcast::<Workspace>()
7173 .and_then(|window_handle| {
7174 window_handle
7175 .update(cx, |workspace, _window, cx| {
7176 if workspace.project().read(cx).remote_id() == Some(project_id) {
7177 Some(window_handle)
7178 } else {
7179 None
7180 }
7181 })
7182 .unwrap_or(None)
7183 })
7184 });
7185
7186 let workspace = if let Some(existing_workspace) = existing_workspace {
7187 existing_workspace
7188 } else {
7189 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
7190 let room = active_call
7191 .read_with(cx, |call, _| call.room().cloned())?
7192 .context("not in a call")?;
7193 let project = room
7194 .update(cx, |room, cx| {
7195 room.join_project(
7196 project_id,
7197 app_state.languages.clone(),
7198 app_state.fs.clone(),
7199 cx,
7200 )
7201 })?
7202 .await?;
7203
7204 let window_bounds_override = window_bounds_env_override();
7205 cx.update(|cx| {
7206 let mut options = (app_state.build_window_options)(None, cx);
7207 options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
7208 cx.open_window(options, |window, cx| {
7209 cx.new(|cx| {
7210 Workspace::new(Default::default(), project, app_state.clone(), window, cx)
7211 })
7212 })
7213 })??
7214 };
7215
7216 workspace.update(cx, |workspace, window, cx| {
7217 cx.activate(true);
7218 window.activate_window();
7219
7220 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
7221 let follow_peer_id = room
7222 .read(cx)
7223 .remote_participants()
7224 .iter()
7225 .find(|(_, participant)| participant.user.id == follow_user_id)
7226 .map(|(_, p)| p.peer_id)
7227 .or_else(|| {
7228 // If we couldn't follow the given user, follow the host instead.
7229 let collaborator = workspace
7230 .project()
7231 .read(cx)
7232 .collaborators()
7233 .values()
7234 .find(|collaborator| collaborator.is_host)?;
7235 Some(collaborator.peer_id)
7236 });
7237
7238 if let Some(follow_peer_id) = follow_peer_id {
7239 workspace.follow(follow_peer_id, window, cx);
7240 }
7241 }
7242 })?;
7243
7244 anyhow::Ok(())
7245 })
7246}
7247
7248pub fn reload(reload: &Reload, cx: &mut App) {
7249 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
7250 let mut workspace_windows = cx
7251 .windows()
7252 .into_iter()
7253 .filter_map(|window| window.downcast::<Workspace>())
7254 .collect::<Vec<_>>();
7255
7256 // If multiple windows have unsaved changes, and need a save prompt,
7257 // prompt in the active window before switching to a different window.
7258 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
7259
7260 let mut prompt = None;
7261 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
7262 prompt = window
7263 .update(cx, |_, window, cx| {
7264 window.prompt(
7265 PromptLevel::Info,
7266 "Are you sure you want to restart?",
7267 None,
7268 &["Restart", "Cancel"],
7269 cx,
7270 )
7271 })
7272 .ok();
7273 }
7274
7275 let binary_path = reload.binary_path.clone();
7276 cx.spawn(async move |cx| {
7277 if let Some(prompt) = prompt {
7278 let answer = prompt.await?;
7279 if answer != 0 {
7280 return Ok(());
7281 }
7282 }
7283
7284 // If the user cancels any save prompt, then keep the app open.
7285 for window in workspace_windows {
7286 if let Ok(should_close) = window.update(cx, |workspace, window, cx| {
7287 workspace.prepare_to_close(CloseIntent::Quit, window, cx)
7288 }) {
7289 if !should_close.await? {
7290 return Ok(());
7291 }
7292 }
7293 }
7294
7295 cx.update(|cx| cx.restart(binary_path))
7296 })
7297 .detach_and_log_err(cx);
7298}
7299
7300fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
7301 let mut parts = value.split(',');
7302 let x: usize = parts.next()?.parse().ok()?;
7303 let y: usize = parts.next()?.parse().ok()?;
7304 Some(point(px(x as f32), px(y as f32)))
7305}
7306
7307fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
7308 let mut parts = value.split(',');
7309 let width: usize = parts.next()?.parse().ok()?;
7310 let height: usize = parts.next()?.parse().ok()?;
7311 Some(size(px(width as f32), px(height as f32)))
7312}
7313
7314pub fn client_side_decorations(
7315 element: impl IntoElement,
7316 window: &mut Window,
7317 cx: &mut App,
7318) -> Stateful<Div> {
7319 const BORDER_SIZE: Pixels = px(1.0);
7320 let decorations = window.window_decorations();
7321
7322 if matches!(decorations, Decorations::Client { .. }) {
7323 window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
7324 }
7325
7326 struct GlobalResizeEdge(ResizeEdge);
7327 impl Global for GlobalResizeEdge {}
7328
7329 div()
7330 .id("window-backdrop")
7331 .bg(transparent_black())
7332 .map(|div| match decorations {
7333 Decorations::Server => div,
7334 Decorations::Client { tiling, .. } => div
7335 .when(!(tiling.top || tiling.right), |div| {
7336 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7337 })
7338 .when(!(tiling.top || tiling.left), |div| {
7339 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7340 })
7341 .when(!(tiling.bottom || tiling.right), |div| {
7342 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7343 })
7344 .when(!(tiling.bottom || tiling.left), |div| {
7345 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7346 })
7347 .when(!tiling.top, |div| {
7348 div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
7349 })
7350 .when(!tiling.bottom, |div| {
7351 div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
7352 })
7353 .when(!tiling.left, |div| {
7354 div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
7355 })
7356 .when(!tiling.right, |div| {
7357 div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
7358 })
7359 .on_mouse_move(move |e, window, cx| {
7360 let size = window.window_bounds().get_bounds().size;
7361 let pos = e.position;
7362
7363 let new_edge =
7364 resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
7365
7366 let edge = cx.try_global::<GlobalResizeEdge>();
7367 if new_edge != edge.map(|edge| edge.0) {
7368 window
7369 .window_handle()
7370 .update(cx, |workspace, _, cx| {
7371 cx.notify(workspace.entity_id());
7372 })
7373 .ok();
7374 }
7375 })
7376 .on_mouse_down(MouseButton::Left, move |e, window, _| {
7377 let size = window.window_bounds().get_bounds().size;
7378 let pos = e.position;
7379
7380 let edge = match resize_edge(
7381 pos,
7382 theme::CLIENT_SIDE_DECORATION_SHADOW,
7383 size,
7384 tiling,
7385 ) {
7386 Some(value) => value,
7387 None => return,
7388 };
7389
7390 window.start_window_resize(edge);
7391 }),
7392 })
7393 .size_full()
7394 .child(
7395 div()
7396 .cursor(CursorStyle::Arrow)
7397 .map(|div| match decorations {
7398 Decorations::Server => div,
7399 Decorations::Client { tiling } => div
7400 .border_color(cx.theme().colors().border)
7401 .when(!(tiling.top || tiling.right), |div| {
7402 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7403 })
7404 .when(!(tiling.top || tiling.left), |div| {
7405 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7406 })
7407 .when(!(tiling.bottom || tiling.right), |div| {
7408 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7409 })
7410 .when(!(tiling.bottom || tiling.left), |div| {
7411 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
7412 })
7413 .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
7414 .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
7415 .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
7416 .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
7417 .when(!tiling.is_tiled(), |div| {
7418 div.shadow(vec![gpui::BoxShadow {
7419 color: Hsla {
7420 h: 0.,
7421 s: 0.,
7422 l: 0.,
7423 a: 0.4,
7424 },
7425 blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
7426 spread_radius: px(0.),
7427 offset: point(px(0.0), px(0.0)),
7428 }])
7429 }),
7430 })
7431 .on_mouse_move(|_e, _, cx| {
7432 cx.stop_propagation();
7433 })
7434 .size_full()
7435 .child(element),
7436 )
7437 .map(|div| match decorations {
7438 Decorations::Server => div,
7439 Decorations::Client { tiling, .. } => div.child(
7440 canvas(
7441 |_bounds, window, _| {
7442 window.insert_hitbox(
7443 Bounds::new(
7444 point(px(0.0), px(0.0)),
7445 window.window_bounds().get_bounds().size,
7446 ),
7447 HitboxBehavior::Normal,
7448 )
7449 },
7450 move |_bounds, hitbox, window, cx| {
7451 let mouse = window.mouse_position();
7452 let size = window.window_bounds().get_bounds().size;
7453 let Some(edge) =
7454 resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
7455 else {
7456 return;
7457 };
7458 cx.set_global(GlobalResizeEdge(edge));
7459 window.set_cursor_style(
7460 match edge {
7461 ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
7462 ResizeEdge::Left | ResizeEdge::Right => {
7463 CursorStyle::ResizeLeftRight
7464 }
7465 ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
7466 CursorStyle::ResizeUpLeftDownRight
7467 }
7468 ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
7469 CursorStyle::ResizeUpRightDownLeft
7470 }
7471 },
7472 &hitbox,
7473 );
7474 },
7475 )
7476 .size_full()
7477 .absolute(),
7478 ),
7479 })
7480}
7481
7482fn resize_edge(
7483 pos: Point<Pixels>,
7484 shadow_size: Pixels,
7485 window_size: Size<Pixels>,
7486 tiling: Tiling,
7487) -> Option<ResizeEdge> {
7488 let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
7489 if bounds.contains(&pos) {
7490 return None;
7491 }
7492
7493 let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
7494 let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
7495 if !tiling.top && top_left_bounds.contains(&pos) {
7496 return Some(ResizeEdge::TopLeft);
7497 }
7498
7499 let top_right_bounds = Bounds::new(
7500 Point::new(window_size.width - corner_size.width, px(0.)),
7501 corner_size,
7502 );
7503 if !tiling.top && top_right_bounds.contains(&pos) {
7504 return Some(ResizeEdge::TopRight);
7505 }
7506
7507 let bottom_left_bounds = Bounds::new(
7508 Point::new(px(0.), window_size.height - corner_size.height),
7509 corner_size,
7510 );
7511 if !tiling.bottom && bottom_left_bounds.contains(&pos) {
7512 return Some(ResizeEdge::BottomLeft);
7513 }
7514
7515 let bottom_right_bounds = Bounds::new(
7516 Point::new(
7517 window_size.width - corner_size.width,
7518 window_size.height - corner_size.height,
7519 ),
7520 corner_size,
7521 );
7522 if !tiling.bottom && bottom_right_bounds.contains(&pos) {
7523 return Some(ResizeEdge::BottomRight);
7524 }
7525
7526 if !tiling.top && pos.y < shadow_size {
7527 Some(ResizeEdge::Top)
7528 } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
7529 Some(ResizeEdge::Bottom)
7530 } else if !tiling.left && pos.x < shadow_size {
7531 Some(ResizeEdge::Left)
7532 } else if !tiling.right && pos.x > window_size.width - shadow_size {
7533 Some(ResizeEdge::Right)
7534 } else {
7535 None
7536 }
7537}
7538
7539fn join_pane_into_active(
7540 active_pane: &Entity<Pane>,
7541 pane: &Entity<Pane>,
7542 window: &mut Window,
7543 cx: &mut App,
7544) {
7545 if pane == active_pane {
7546 return;
7547 } else if pane.read(cx).items_len() == 0 {
7548 pane.update(cx, |_, cx| {
7549 cx.emit(pane::Event::Remove {
7550 focus_on_pane: None,
7551 });
7552 })
7553 } else {
7554 move_all_items(pane, active_pane, window, cx);
7555 }
7556}
7557
7558fn move_all_items(
7559 from_pane: &Entity<Pane>,
7560 to_pane: &Entity<Pane>,
7561 window: &mut Window,
7562 cx: &mut App,
7563) {
7564 let destination_is_different = from_pane != to_pane;
7565 let mut moved_items = 0;
7566 for (item_ix, item_handle) in from_pane
7567 .read(cx)
7568 .items()
7569 .enumerate()
7570 .map(|(ix, item)| (ix, item.clone()))
7571 .collect::<Vec<_>>()
7572 {
7573 let ix = item_ix - moved_items;
7574 if destination_is_different {
7575 // Close item from previous pane
7576 from_pane.update(cx, |source, cx| {
7577 source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), window, cx);
7578 });
7579 moved_items += 1;
7580 }
7581
7582 // This automatically removes duplicate items in the pane
7583 to_pane.update(cx, |destination, cx| {
7584 destination.add_item(item_handle, true, true, None, window, cx);
7585 window.focus(&destination.focus_handle(cx))
7586 });
7587 }
7588}
7589
7590pub fn move_item(
7591 source: &Entity<Pane>,
7592 destination: &Entity<Pane>,
7593 item_id_to_move: EntityId,
7594 destination_index: usize,
7595 activate: bool,
7596 window: &mut Window,
7597 cx: &mut App,
7598) {
7599 let Some((item_ix, item_handle)) = source
7600 .read(cx)
7601 .items()
7602 .enumerate()
7603 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
7604 .map(|(ix, item)| (ix, item.clone()))
7605 else {
7606 // Tab was closed during drag
7607 return;
7608 };
7609
7610 if source != destination {
7611 // Close item from previous pane
7612 source.update(cx, |source, cx| {
7613 source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), window, cx);
7614 });
7615 }
7616
7617 // This automatically removes duplicate items in the pane
7618 destination.update(cx, |destination, cx| {
7619 destination.add_item_inner(
7620 item_handle,
7621 activate,
7622 activate,
7623 activate,
7624 Some(destination_index),
7625 window,
7626 cx,
7627 );
7628 if activate {
7629 window.focus(&destination.focus_handle(cx))
7630 }
7631 });
7632}
7633
7634pub fn move_active_item(
7635 source: &Entity<Pane>,
7636 destination: &Entity<Pane>,
7637 focus_destination: bool,
7638 close_if_empty: bool,
7639 window: &mut Window,
7640 cx: &mut App,
7641) {
7642 if source == destination {
7643 return;
7644 }
7645 let Some(active_item) = source.read(cx).active_item() else {
7646 return;
7647 };
7648 source.update(cx, |source_pane, cx| {
7649 let item_id = active_item.item_id();
7650 source_pane.remove_item(item_id, false, close_if_empty, window, cx);
7651 destination.update(cx, |target_pane, cx| {
7652 target_pane.add_item(
7653 active_item,
7654 focus_destination,
7655 focus_destination,
7656 Some(target_pane.items_len()),
7657 window,
7658 cx,
7659 );
7660 });
7661 });
7662}
7663
7664pub fn clone_active_item(
7665 workspace_id: Option<WorkspaceId>,
7666 source: &Entity<Pane>,
7667 destination: &Entity<Pane>,
7668 focus_destination: bool,
7669 window: &mut Window,
7670 cx: &mut App,
7671) {
7672 if source == destination {
7673 return;
7674 }
7675 let Some(active_item) = source.read(cx).active_item() else {
7676 return;
7677 };
7678 destination.update(cx, |target_pane, cx| {
7679 let Some(clone) = active_item.clone_on_split(workspace_id, window, cx) else {
7680 return;
7681 };
7682 target_pane.add_item(
7683 clone,
7684 focus_destination,
7685 focus_destination,
7686 Some(target_pane.items_len()),
7687 window,
7688 cx,
7689 );
7690 });
7691}
7692
7693#[derive(Debug)]
7694pub struct WorkspacePosition {
7695 pub window_bounds: Option<WindowBounds>,
7696 pub display: Option<Uuid>,
7697 pub centered_layout: bool,
7698}
7699
7700pub fn ssh_workspace_position_from_db(
7701 host: String,
7702 port: Option<u16>,
7703 user: Option<String>,
7704 paths_to_open: &[PathBuf],
7705 cx: &App,
7706) -> Task<Result<WorkspacePosition>> {
7707 let paths = paths_to_open
7708 .iter()
7709 .map(|path| path.to_string_lossy().to_string())
7710 .collect::<Vec<_>>();
7711
7712 cx.background_spawn(async move {
7713 let serialized_ssh_project = persistence::DB
7714 .get_or_create_ssh_project(host, port, paths, user)
7715 .await
7716 .context("fetching serialized ssh project")?;
7717 let serialized_workspace =
7718 persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
7719
7720 let (window_bounds, display) = if let Some(bounds) = window_bounds_env_override() {
7721 (Some(WindowBounds::Windowed(bounds)), None)
7722 } else {
7723 let restorable_bounds = serialized_workspace
7724 .as_ref()
7725 .and_then(|workspace| Some((workspace.display?, workspace.window_bounds?)))
7726 .or_else(|| {
7727 let (display, window_bounds) = DB.last_window().log_err()?;
7728 Some((display?, window_bounds?))
7729 });
7730
7731 if let Some((serialized_display, serialized_status)) = restorable_bounds {
7732 (Some(serialized_status.0), Some(serialized_display))
7733 } else {
7734 (None, None)
7735 }
7736 };
7737
7738 let centered_layout = serialized_workspace
7739 .as_ref()
7740 .map(|w| w.centered_layout)
7741 .unwrap_or(false);
7742
7743 Ok(WorkspacePosition {
7744 window_bounds,
7745 display,
7746 centered_layout,
7747 })
7748 })
7749}
7750
7751pub fn with_active_or_new_workspace(
7752 cx: &mut App,
7753 f: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send + 'static,
7754) {
7755 match cx.active_window().and_then(|w| w.downcast::<Workspace>()) {
7756 Some(workspace) => {
7757 cx.defer(move |cx| {
7758 workspace
7759 .update(cx, |workspace, window, cx| f(workspace, window, cx))
7760 .log_err();
7761 });
7762 }
7763 None => {
7764 let app_state = AppState::global(cx);
7765 if let Some(app_state) = app_state.upgrade() {
7766 open_new(
7767 OpenOptions::default(),
7768 app_state,
7769 cx,
7770 move |workspace, window, cx| f(workspace, window, cx),
7771 )
7772 .detach_and_log_err(cx);
7773 }
7774 }
7775 }
7776}
7777
7778#[cfg(test)]
7779mod tests {
7780 use std::{cell::RefCell, rc::Rc};
7781
7782 use super::*;
7783 use crate::{
7784 dock::{PanelEvent, test::TestPanel},
7785 item::{
7786 ItemEvent,
7787 test::{TestItem, TestProjectItem},
7788 },
7789 };
7790 use fs::FakeFs;
7791 use gpui::{
7792 DismissEvent, Empty, EventEmitter, FocusHandle, Focusable, Render, TestAppContext,
7793 UpdateGlobal, VisualTestContext, px,
7794 };
7795 use project::{Project, ProjectEntryId};
7796 use serde_json::json;
7797 use settings::SettingsStore;
7798
7799 #[gpui::test]
7800 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
7801 init_test(cx);
7802
7803 let fs = FakeFs::new(cx.executor());
7804 let project = Project::test(fs, [], cx).await;
7805 let (workspace, cx) =
7806 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7807
7808 // Adding an item with no ambiguity renders the tab without detail.
7809 let item1 = cx.new(|cx| {
7810 let mut item = TestItem::new(cx);
7811 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
7812 item
7813 });
7814 workspace.update_in(cx, |workspace, window, cx| {
7815 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7816 });
7817 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
7818
7819 // Adding an item that creates ambiguity increases the level of detail on
7820 // both tabs.
7821 let item2 = cx.new_window_entity(|_window, cx| {
7822 let mut item = TestItem::new(cx);
7823 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
7824 item
7825 });
7826 workspace.update_in(cx, |workspace, window, cx| {
7827 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7828 });
7829 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
7830 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
7831
7832 // Adding an item that creates ambiguity increases the level of detail only
7833 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
7834 // we stop at the highest detail available.
7835 let item3 = cx.new(|cx| {
7836 let mut item = TestItem::new(cx);
7837 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
7838 item
7839 });
7840 workspace.update_in(cx, |workspace, window, cx| {
7841 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7842 });
7843 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
7844 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
7845 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
7846 }
7847
7848 #[gpui::test]
7849 async fn test_tracking_active_path(cx: &mut TestAppContext) {
7850 init_test(cx);
7851
7852 let fs = FakeFs::new(cx.executor());
7853 fs.insert_tree(
7854 "/root1",
7855 json!({
7856 "one.txt": "",
7857 "two.txt": "",
7858 }),
7859 )
7860 .await;
7861 fs.insert_tree(
7862 "/root2",
7863 json!({
7864 "three.txt": "",
7865 }),
7866 )
7867 .await;
7868
7869 let project = Project::test(fs, ["root1".as_ref()], cx).await;
7870 let (workspace, cx) =
7871 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7872 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
7873 let worktree_id = project.update(cx, |project, cx| {
7874 project.worktrees(cx).next().unwrap().read(cx).id()
7875 });
7876
7877 let item1 = cx.new(|cx| {
7878 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
7879 });
7880 let item2 = cx.new(|cx| {
7881 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
7882 });
7883
7884 // Add an item to an empty pane
7885 workspace.update_in(cx, |workspace, window, cx| {
7886 workspace.add_item_to_active_pane(Box::new(item1), None, true, window, cx)
7887 });
7888 project.update(cx, |project, cx| {
7889 assert_eq!(
7890 project.active_entry(),
7891 project
7892 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
7893 .map(|e| e.id)
7894 );
7895 });
7896 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
7897
7898 // Add a second item to a non-empty pane
7899 workspace.update_in(cx, |workspace, window, cx| {
7900 workspace.add_item_to_active_pane(Box::new(item2), None, true, window, cx)
7901 });
7902 assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
7903 project.update(cx, |project, cx| {
7904 assert_eq!(
7905 project.active_entry(),
7906 project
7907 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
7908 .map(|e| e.id)
7909 );
7910 });
7911
7912 // Close the active item
7913 pane.update_in(cx, |pane, window, cx| {
7914 pane.close_active_item(&Default::default(), window, cx)
7915 })
7916 .await
7917 .unwrap();
7918 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
7919 project.update(cx, |project, cx| {
7920 assert_eq!(
7921 project.active_entry(),
7922 project
7923 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
7924 .map(|e| e.id)
7925 );
7926 });
7927
7928 // Add a project folder
7929 project
7930 .update(cx, |project, cx| {
7931 project.find_or_create_worktree("root2", true, cx)
7932 })
7933 .await
7934 .unwrap();
7935 assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
7936
7937 // Remove a project folder
7938 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
7939 assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
7940 }
7941
7942 #[gpui::test]
7943 async fn test_close_window(cx: &mut TestAppContext) {
7944 init_test(cx);
7945
7946 let fs = FakeFs::new(cx.executor());
7947 fs.insert_tree("/root", json!({ "one": "" })).await;
7948
7949 let project = Project::test(fs, ["root".as_ref()], cx).await;
7950 let (workspace, cx) =
7951 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7952
7953 // When there are no dirty items, there's nothing to do.
7954 let item1 = cx.new(TestItem::new);
7955 workspace.update_in(cx, |w, window, cx| {
7956 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx)
7957 });
7958 let task = workspace.update_in(cx, |w, window, cx| {
7959 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
7960 });
7961 assert!(task.await.unwrap());
7962
7963 // When there are dirty untitled items, prompt to save each one. If the user
7964 // cancels any prompt, then abort.
7965 let item2 = cx.new(|cx| TestItem::new(cx).with_dirty(true));
7966 let item3 = cx.new(|cx| {
7967 TestItem::new(cx)
7968 .with_dirty(true)
7969 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7970 });
7971 workspace.update_in(cx, |w, window, cx| {
7972 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7973 w.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7974 });
7975 let task = workspace.update_in(cx, |w, window, cx| {
7976 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
7977 });
7978 cx.executor().run_until_parked();
7979 cx.simulate_prompt_answer("Cancel"); // cancel save all
7980 cx.executor().run_until_parked();
7981 assert!(!cx.has_pending_prompt());
7982 assert!(!task.await.unwrap());
7983 }
7984
7985 #[gpui::test]
7986 async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
7987 init_test(cx);
7988
7989 // Register TestItem as a serializable item
7990 cx.update(|cx| {
7991 register_serializable_item::<TestItem>(cx);
7992 });
7993
7994 let fs = FakeFs::new(cx.executor());
7995 fs.insert_tree("/root", json!({ "one": "" })).await;
7996
7997 let project = Project::test(fs, ["root".as_ref()], cx).await;
7998 let (workspace, cx) =
7999 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8000
8001 // When there are dirty untitled items, but they can serialize, then there is no prompt.
8002 let item1 = cx.new(|cx| {
8003 TestItem::new(cx)
8004 .with_dirty(true)
8005 .with_serialize(|| Some(Task::ready(Ok(()))))
8006 });
8007 let item2 = cx.new(|cx| {
8008 TestItem::new(cx)
8009 .with_dirty(true)
8010 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
8011 .with_serialize(|| Some(Task::ready(Ok(()))))
8012 });
8013 workspace.update_in(cx, |w, window, cx| {
8014 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
8015 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
8016 });
8017 let task = workspace.update_in(cx, |w, window, cx| {
8018 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
8019 });
8020 assert!(task.await.unwrap());
8021 }
8022
8023 #[gpui::test]
8024 async fn test_close_pane_items(cx: &mut TestAppContext) {
8025 init_test(cx);
8026
8027 let fs = FakeFs::new(cx.executor());
8028
8029 let project = Project::test(fs, None, cx).await;
8030 let (workspace, cx) =
8031 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8032
8033 let item1 = cx.new(|cx| {
8034 TestItem::new(cx)
8035 .with_dirty(true)
8036 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8037 });
8038 let item2 = cx.new(|cx| {
8039 TestItem::new(cx)
8040 .with_dirty(true)
8041 .with_conflict(true)
8042 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8043 });
8044 let item3 = cx.new(|cx| {
8045 TestItem::new(cx)
8046 .with_dirty(true)
8047 .with_conflict(true)
8048 .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
8049 });
8050 let item4 = cx.new(|cx| {
8051 TestItem::new(cx).with_dirty(true).with_project_items(&[{
8052 let project_item = TestProjectItem::new_untitled(cx);
8053 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
8054 project_item
8055 }])
8056 });
8057 let pane = workspace.update_in(cx, |workspace, window, cx| {
8058 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
8059 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
8060 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
8061 workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, window, cx);
8062 workspace.active_pane().clone()
8063 });
8064
8065 let close_items = pane.update_in(cx, |pane, window, cx| {
8066 pane.activate_item(1, true, true, window, cx);
8067 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
8068 let item1_id = item1.item_id();
8069 let item3_id = item3.item_id();
8070 let item4_id = item4.item_id();
8071 pane.close_items(window, cx, SaveIntent::Close, move |id| {
8072 [item1_id, item3_id, item4_id].contains(&id)
8073 })
8074 });
8075 cx.executor().run_until_parked();
8076
8077 assert!(cx.has_pending_prompt());
8078 cx.simulate_prompt_answer("Save all");
8079
8080 cx.executor().run_until_parked();
8081
8082 // Item 1 is saved. There's a prompt to save item 3.
8083 pane.update(cx, |pane, cx| {
8084 assert_eq!(item1.read(cx).save_count, 1);
8085 assert_eq!(item1.read(cx).save_as_count, 0);
8086 assert_eq!(item1.read(cx).reload_count, 0);
8087 assert_eq!(pane.items_len(), 3);
8088 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
8089 });
8090 assert!(cx.has_pending_prompt());
8091
8092 // Cancel saving item 3.
8093 cx.simulate_prompt_answer("Discard");
8094 cx.executor().run_until_parked();
8095
8096 // Item 3 is reloaded. There's a prompt to save item 4.
8097 pane.update(cx, |pane, cx| {
8098 assert_eq!(item3.read(cx).save_count, 0);
8099 assert_eq!(item3.read(cx).save_as_count, 0);
8100 assert_eq!(item3.read(cx).reload_count, 1);
8101 assert_eq!(pane.items_len(), 2);
8102 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
8103 });
8104
8105 // There's a prompt for a path for item 4.
8106 cx.simulate_new_path_selection(|_| Some(Default::default()));
8107 close_items.await.unwrap();
8108
8109 // The requested items are closed.
8110 pane.update(cx, |pane, cx| {
8111 assert_eq!(item4.read(cx).save_count, 0);
8112 assert_eq!(item4.read(cx).save_as_count, 1);
8113 assert_eq!(item4.read(cx).reload_count, 0);
8114 assert_eq!(pane.items_len(), 1);
8115 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
8116 });
8117 }
8118
8119 #[gpui::test]
8120 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
8121 init_test(cx);
8122
8123 let fs = FakeFs::new(cx.executor());
8124 let project = Project::test(fs, [], cx).await;
8125 let (workspace, cx) =
8126 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8127
8128 // Create several workspace items with single project entries, and two
8129 // workspace items with multiple project entries.
8130 let single_entry_items = (0..=4)
8131 .map(|project_entry_id| {
8132 cx.new(|cx| {
8133 TestItem::new(cx)
8134 .with_dirty(true)
8135 .with_project_items(&[dirty_project_item(
8136 project_entry_id,
8137 &format!("{project_entry_id}.txt"),
8138 cx,
8139 )])
8140 })
8141 })
8142 .collect::<Vec<_>>();
8143 let item_2_3 = cx.new(|cx| {
8144 TestItem::new(cx)
8145 .with_dirty(true)
8146 .with_singleton(false)
8147 .with_project_items(&[
8148 single_entry_items[2].read(cx).project_items[0].clone(),
8149 single_entry_items[3].read(cx).project_items[0].clone(),
8150 ])
8151 });
8152 let item_3_4 = cx.new(|cx| {
8153 TestItem::new(cx)
8154 .with_dirty(true)
8155 .with_singleton(false)
8156 .with_project_items(&[
8157 single_entry_items[3].read(cx).project_items[0].clone(),
8158 single_entry_items[4].read(cx).project_items[0].clone(),
8159 ])
8160 });
8161
8162 // Create two panes that contain the following project entries:
8163 // left pane:
8164 // multi-entry items: (2, 3)
8165 // single-entry items: 0, 2, 3, 4
8166 // right pane:
8167 // single-entry items: 4, 1
8168 // multi-entry items: (3, 4)
8169 let (left_pane, right_pane) = workspace.update_in(cx, |workspace, window, cx| {
8170 let left_pane = workspace.active_pane().clone();
8171 workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, window, cx);
8172 workspace.add_item_to_active_pane(
8173 single_entry_items[0].boxed_clone(),
8174 None,
8175 true,
8176 window,
8177 cx,
8178 );
8179 workspace.add_item_to_active_pane(
8180 single_entry_items[2].boxed_clone(),
8181 None,
8182 true,
8183 window,
8184 cx,
8185 );
8186 workspace.add_item_to_active_pane(
8187 single_entry_items[3].boxed_clone(),
8188 None,
8189 true,
8190 window,
8191 cx,
8192 );
8193 workspace.add_item_to_active_pane(
8194 single_entry_items[4].boxed_clone(),
8195 None,
8196 true,
8197 window,
8198 cx,
8199 );
8200
8201 let right_pane = workspace
8202 .split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx)
8203 .unwrap();
8204
8205 right_pane.update(cx, |pane, cx| {
8206 pane.add_item(
8207 single_entry_items[1].boxed_clone(),
8208 true,
8209 true,
8210 None,
8211 window,
8212 cx,
8213 );
8214 pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx);
8215 });
8216
8217 (left_pane, right_pane)
8218 });
8219
8220 cx.focus(&right_pane);
8221
8222 let mut close = right_pane.update_in(cx, |pane, window, cx| {
8223 pane.close_all_items(&CloseAllItems::default(), window, cx)
8224 .unwrap()
8225 });
8226 cx.executor().run_until_parked();
8227
8228 let msg = cx.pending_prompt().unwrap().0;
8229 assert!(msg.contains("1.txt"));
8230 assert!(!msg.contains("2.txt"));
8231 assert!(!msg.contains("3.txt"));
8232 assert!(!msg.contains("4.txt"));
8233
8234 cx.simulate_prompt_answer("Cancel");
8235 close.await;
8236
8237 left_pane
8238 .update_in(cx, |left_pane, window, cx| {
8239 left_pane.close_item_by_id(
8240 single_entry_items[3].entity_id(),
8241 SaveIntent::Skip,
8242 window,
8243 cx,
8244 )
8245 })
8246 .await
8247 .unwrap();
8248
8249 close = right_pane.update_in(cx, |pane, window, cx| {
8250 pane.close_all_items(&CloseAllItems::default(), window, cx)
8251 .unwrap()
8252 });
8253 cx.executor().run_until_parked();
8254
8255 let details = cx.pending_prompt().unwrap().1;
8256 assert!(details.contains("1.txt"));
8257 assert!(!details.contains("2.txt"));
8258 assert!(details.contains("3.txt"));
8259 // ideally this assertion could be made, but today we can only
8260 // save whole items not project items, so the orphaned item 3 causes
8261 // 4 to be saved too.
8262 // assert!(!details.contains("4.txt"));
8263
8264 cx.simulate_prompt_answer("Save all");
8265
8266 cx.executor().run_until_parked();
8267 close.await;
8268 right_pane.read_with(cx, |pane, _| {
8269 assert_eq!(pane.items_len(), 0);
8270 });
8271 }
8272
8273 #[gpui::test]
8274 async fn test_autosave(cx: &mut gpui::TestAppContext) {
8275 init_test(cx);
8276
8277 let fs = FakeFs::new(cx.executor());
8278 let project = Project::test(fs, [], cx).await;
8279 let (workspace, cx) =
8280 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8281 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
8282
8283 let item = cx.new(|cx| {
8284 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
8285 });
8286 let item_id = item.entity_id();
8287 workspace.update_in(cx, |workspace, window, cx| {
8288 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
8289 });
8290
8291 // Autosave on window change.
8292 item.update(cx, |item, cx| {
8293 SettingsStore::update_global(cx, |settings, cx| {
8294 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
8295 settings.autosave = Some(AutosaveSetting::OnWindowChange);
8296 })
8297 });
8298 item.is_dirty = true;
8299 });
8300
8301 // Deactivating the window saves the file.
8302 cx.deactivate_window();
8303 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
8304
8305 // Re-activating the window doesn't save the file.
8306 cx.update(|window, _| window.activate_window());
8307 cx.executor().run_until_parked();
8308 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
8309
8310 // Autosave on focus change.
8311 item.update_in(cx, |item, window, cx| {
8312 cx.focus_self(window);
8313 SettingsStore::update_global(cx, |settings, cx| {
8314 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
8315 settings.autosave = Some(AutosaveSetting::OnFocusChange);
8316 })
8317 });
8318 item.is_dirty = true;
8319 });
8320
8321 // Blurring the item saves the file.
8322 item.update_in(cx, |_, window, _| window.blur());
8323 cx.executor().run_until_parked();
8324 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
8325
8326 // Deactivating the window still saves the file.
8327 item.update_in(cx, |item, window, cx| {
8328 cx.focus_self(window);
8329 item.is_dirty = true;
8330 });
8331 cx.deactivate_window();
8332 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
8333
8334 // Autosave after delay.
8335 item.update(cx, |item, cx| {
8336 SettingsStore::update_global(cx, |settings, cx| {
8337 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
8338 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
8339 })
8340 });
8341 item.is_dirty = true;
8342 cx.emit(ItemEvent::Edit);
8343 });
8344
8345 // Delay hasn't fully expired, so the file is still dirty and unsaved.
8346 cx.executor().advance_clock(Duration::from_millis(250));
8347 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
8348
8349 // After delay expires, the file is saved.
8350 cx.executor().advance_clock(Duration::from_millis(250));
8351 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
8352
8353 // Autosave on focus change, ensuring closing the tab counts as such.
8354 item.update(cx, |item, cx| {
8355 SettingsStore::update_global(cx, |settings, cx| {
8356 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
8357 settings.autosave = Some(AutosaveSetting::OnFocusChange);
8358 })
8359 });
8360 item.is_dirty = true;
8361 for project_item in &mut item.project_items {
8362 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
8363 }
8364 });
8365
8366 pane.update_in(cx, |pane, window, cx| {
8367 pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
8368 })
8369 .await
8370 .unwrap();
8371 assert!(!cx.has_pending_prompt());
8372 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
8373
8374 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
8375 workspace.update_in(cx, |workspace, window, cx| {
8376 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
8377 });
8378 item.update_in(cx, |item, window, cx| {
8379 item.project_items[0].update(cx, |item, _| {
8380 item.entry_id = None;
8381 });
8382 item.is_dirty = true;
8383 window.blur();
8384 });
8385 cx.run_until_parked();
8386 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
8387
8388 // Ensure autosave is prevented for deleted files also when closing the buffer.
8389 let _close_items = pane.update_in(cx, |pane, window, cx| {
8390 pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
8391 });
8392 cx.run_until_parked();
8393 assert!(cx.has_pending_prompt());
8394 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
8395 }
8396
8397 #[gpui::test]
8398 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
8399 init_test(cx);
8400
8401 let fs = FakeFs::new(cx.executor());
8402
8403 let project = Project::test(fs, [], cx).await;
8404 let (workspace, cx) =
8405 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8406
8407 let item = cx.new(|cx| {
8408 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
8409 });
8410 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
8411 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
8412 let toolbar_notify_count = Rc::new(RefCell::new(0));
8413
8414 workspace.update_in(cx, |workspace, window, cx| {
8415 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
8416 let toolbar_notification_count = toolbar_notify_count.clone();
8417 cx.observe_in(&toolbar, window, move |_, _, _, _| {
8418 *toolbar_notification_count.borrow_mut() += 1
8419 })
8420 .detach();
8421 });
8422
8423 pane.read_with(cx, |pane, _| {
8424 assert!(!pane.can_navigate_backward());
8425 assert!(!pane.can_navigate_forward());
8426 });
8427
8428 item.update_in(cx, |item, _, cx| {
8429 item.set_state("one".to_string(), cx);
8430 });
8431
8432 // Toolbar must be notified to re-render the navigation buttons
8433 assert_eq!(*toolbar_notify_count.borrow(), 1);
8434
8435 pane.read_with(cx, |pane, _| {
8436 assert!(pane.can_navigate_backward());
8437 assert!(!pane.can_navigate_forward());
8438 });
8439
8440 workspace
8441 .update_in(cx, |workspace, window, cx| {
8442 workspace.go_back(pane.downgrade(), window, cx)
8443 })
8444 .await
8445 .unwrap();
8446
8447 assert_eq!(*toolbar_notify_count.borrow(), 2);
8448 pane.read_with(cx, |pane, _| {
8449 assert!(!pane.can_navigate_backward());
8450 assert!(pane.can_navigate_forward());
8451 });
8452 }
8453
8454 #[gpui::test]
8455 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
8456 init_test(cx);
8457 let fs = FakeFs::new(cx.executor());
8458
8459 let project = Project::test(fs, [], cx).await;
8460 let (workspace, cx) =
8461 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8462
8463 let panel = workspace.update_in(cx, |workspace, window, cx| {
8464 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
8465 workspace.add_panel(panel.clone(), window, cx);
8466
8467 workspace
8468 .right_dock()
8469 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
8470
8471 panel
8472 });
8473
8474 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
8475 pane.update_in(cx, |pane, window, cx| {
8476 let item = cx.new(TestItem::new);
8477 pane.add_item(Box::new(item), true, true, None, window, cx);
8478 });
8479
8480 // Transfer focus from center to panel
8481 workspace.update_in(cx, |workspace, window, cx| {
8482 workspace.toggle_panel_focus::<TestPanel>(window, cx);
8483 });
8484
8485 workspace.update_in(cx, |workspace, window, cx| {
8486 assert!(workspace.right_dock().read(cx).is_open());
8487 assert!(!panel.is_zoomed(window, cx));
8488 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8489 });
8490
8491 // Transfer focus from panel to center
8492 workspace.update_in(cx, |workspace, window, cx| {
8493 workspace.toggle_panel_focus::<TestPanel>(window, cx);
8494 });
8495
8496 workspace.update_in(cx, |workspace, window, cx| {
8497 assert!(workspace.right_dock().read(cx).is_open());
8498 assert!(!panel.is_zoomed(window, cx));
8499 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8500 });
8501
8502 // Close the dock
8503 workspace.update_in(cx, |workspace, window, cx| {
8504 workspace.toggle_dock(DockPosition::Right, window, cx);
8505 });
8506
8507 workspace.update_in(cx, |workspace, window, cx| {
8508 assert!(!workspace.right_dock().read(cx).is_open());
8509 assert!(!panel.is_zoomed(window, cx));
8510 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8511 });
8512
8513 // Open the dock
8514 workspace.update_in(cx, |workspace, window, cx| {
8515 workspace.toggle_dock(DockPosition::Right, window, cx);
8516 });
8517
8518 workspace.update_in(cx, |workspace, window, cx| {
8519 assert!(workspace.right_dock().read(cx).is_open());
8520 assert!(!panel.is_zoomed(window, cx));
8521 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8522 });
8523
8524 // Focus and zoom panel
8525 panel.update_in(cx, |panel, window, cx| {
8526 cx.focus_self(window);
8527 panel.set_zoomed(true, window, cx)
8528 });
8529
8530 workspace.update_in(cx, |workspace, window, cx| {
8531 assert!(workspace.right_dock().read(cx).is_open());
8532 assert!(panel.is_zoomed(window, cx));
8533 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8534 });
8535
8536 // Transfer focus to the center closes the dock
8537 workspace.update_in(cx, |workspace, window, cx| {
8538 workspace.toggle_panel_focus::<TestPanel>(window, cx);
8539 });
8540
8541 workspace.update_in(cx, |workspace, window, cx| {
8542 assert!(!workspace.right_dock().read(cx).is_open());
8543 assert!(panel.is_zoomed(window, cx));
8544 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8545 });
8546
8547 // Transferring focus back to the panel keeps it zoomed
8548 workspace.update_in(cx, |workspace, window, cx| {
8549 workspace.toggle_panel_focus::<TestPanel>(window, cx);
8550 });
8551
8552 workspace.update_in(cx, |workspace, window, cx| {
8553 assert!(workspace.right_dock().read(cx).is_open());
8554 assert!(panel.is_zoomed(window, cx));
8555 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8556 });
8557
8558 // Close the dock while it is zoomed
8559 workspace.update_in(cx, |workspace, window, cx| {
8560 workspace.toggle_dock(DockPosition::Right, window, cx)
8561 });
8562
8563 workspace.update_in(cx, |workspace, window, cx| {
8564 assert!(!workspace.right_dock().read(cx).is_open());
8565 assert!(panel.is_zoomed(window, cx));
8566 assert!(workspace.zoomed.is_none());
8567 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8568 });
8569
8570 // Opening the dock, when it's zoomed, retains focus
8571 workspace.update_in(cx, |workspace, window, cx| {
8572 workspace.toggle_dock(DockPosition::Right, window, cx)
8573 });
8574
8575 workspace.update_in(cx, |workspace, window, cx| {
8576 assert!(workspace.right_dock().read(cx).is_open());
8577 assert!(panel.is_zoomed(window, cx));
8578 assert!(workspace.zoomed.is_some());
8579 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
8580 });
8581
8582 // Unzoom and close the panel, zoom the active pane.
8583 panel.update_in(cx, |panel, window, cx| panel.set_zoomed(false, window, cx));
8584 workspace.update_in(cx, |workspace, window, cx| {
8585 workspace.toggle_dock(DockPosition::Right, window, cx)
8586 });
8587 pane.update_in(cx, |pane, window, cx| {
8588 pane.toggle_zoom(&Default::default(), window, cx)
8589 });
8590
8591 // Opening a dock unzooms the pane.
8592 workspace.update_in(cx, |workspace, window, cx| {
8593 workspace.toggle_dock(DockPosition::Right, window, cx)
8594 });
8595 workspace.update_in(cx, |workspace, window, cx| {
8596 let pane = pane.read(cx);
8597 assert!(!pane.is_zoomed());
8598 assert!(!pane.focus_handle(cx).is_focused(window));
8599 assert!(workspace.right_dock().read(cx).is_open());
8600 assert!(workspace.zoomed.is_none());
8601 });
8602 }
8603
8604 #[gpui::test]
8605 async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
8606 init_test(cx);
8607
8608 let fs = FakeFs::new(cx.executor());
8609
8610 let project = Project::test(fs, None, cx).await;
8611 let (workspace, cx) =
8612 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8613
8614 // Let's arrange the panes like this:
8615 //
8616 // +-----------------------+
8617 // | top |
8618 // +------+--------+-------+
8619 // | left | center | right |
8620 // +------+--------+-------+
8621 // | bottom |
8622 // +-----------------------+
8623
8624 let top_item = cx.new(|cx| {
8625 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
8626 });
8627 let bottom_item = cx.new(|cx| {
8628 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
8629 });
8630 let left_item = cx.new(|cx| {
8631 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
8632 });
8633 let right_item = cx.new(|cx| {
8634 TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
8635 });
8636 let center_item = cx.new(|cx| {
8637 TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
8638 });
8639
8640 let top_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8641 let top_pane_id = workspace.active_pane().entity_id();
8642 workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, window, cx);
8643 workspace.split_pane(
8644 workspace.active_pane().clone(),
8645 SplitDirection::Down,
8646 window,
8647 cx,
8648 );
8649 top_pane_id
8650 });
8651 let bottom_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8652 let bottom_pane_id = workspace.active_pane().entity_id();
8653 workspace.add_item_to_active_pane(
8654 Box::new(bottom_item.clone()),
8655 None,
8656 false,
8657 window,
8658 cx,
8659 );
8660 workspace.split_pane(
8661 workspace.active_pane().clone(),
8662 SplitDirection::Up,
8663 window,
8664 cx,
8665 );
8666 bottom_pane_id
8667 });
8668 let left_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8669 let left_pane_id = workspace.active_pane().entity_id();
8670 workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, window, cx);
8671 workspace.split_pane(
8672 workspace.active_pane().clone(),
8673 SplitDirection::Right,
8674 window,
8675 cx,
8676 );
8677 left_pane_id
8678 });
8679 let right_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8680 let right_pane_id = workspace.active_pane().entity_id();
8681 workspace.add_item_to_active_pane(
8682 Box::new(right_item.clone()),
8683 None,
8684 false,
8685 window,
8686 cx,
8687 );
8688 workspace.split_pane(
8689 workspace.active_pane().clone(),
8690 SplitDirection::Left,
8691 window,
8692 cx,
8693 );
8694 right_pane_id
8695 });
8696 let center_pane_id = workspace.update_in(cx, |workspace, window, cx| {
8697 let center_pane_id = workspace.active_pane().entity_id();
8698 workspace.add_item_to_active_pane(
8699 Box::new(center_item.clone()),
8700 None,
8701 false,
8702 window,
8703 cx,
8704 );
8705 center_pane_id
8706 });
8707 cx.executor().run_until_parked();
8708
8709 workspace.update_in(cx, |workspace, window, cx| {
8710 assert_eq!(center_pane_id, workspace.active_pane().entity_id());
8711
8712 // Join into next from center pane into right
8713 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
8714 });
8715
8716 workspace.update_in(cx, |workspace, window, cx| {
8717 let active_pane = workspace.active_pane();
8718 assert_eq!(right_pane_id, active_pane.entity_id());
8719 assert_eq!(2, active_pane.read(cx).items_len());
8720 let item_ids_in_pane =
8721 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
8722 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
8723 assert!(item_ids_in_pane.contains(&right_item.item_id()));
8724
8725 // Join into next from right pane into bottom
8726 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
8727 });
8728
8729 workspace.update_in(cx, |workspace, window, cx| {
8730 let active_pane = workspace.active_pane();
8731 assert_eq!(bottom_pane_id, active_pane.entity_id());
8732 assert_eq!(3, active_pane.read(cx).items_len());
8733 let item_ids_in_pane =
8734 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
8735 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
8736 assert!(item_ids_in_pane.contains(&right_item.item_id()));
8737 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
8738
8739 // Join into next from bottom pane into left
8740 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
8741 });
8742
8743 workspace.update_in(cx, |workspace, window, cx| {
8744 let active_pane = workspace.active_pane();
8745 assert_eq!(left_pane_id, active_pane.entity_id());
8746 assert_eq!(4, active_pane.read(cx).items_len());
8747 let item_ids_in_pane =
8748 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
8749 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
8750 assert!(item_ids_in_pane.contains(&right_item.item_id()));
8751 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
8752 assert!(item_ids_in_pane.contains(&left_item.item_id()));
8753
8754 // Join into next from left pane into top
8755 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
8756 });
8757
8758 workspace.update_in(cx, |workspace, window, cx| {
8759 let active_pane = workspace.active_pane();
8760 assert_eq!(top_pane_id, active_pane.entity_id());
8761 assert_eq!(5, active_pane.read(cx).items_len());
8762 let item_ids_in_pane =
8763 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
8764 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
8765 assert!(item_ids_in_pane.contains(&right_item.item_id()));
8766 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
8767 assert!(item_ids_in_pane.contains(&left_item.item_id()));
8768 assert!(item_ids_in_pane.contains(&top_item.item_id()));
8769
8770 // Single pane left: no-op
8771 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx)
8772 });
8773
8774 workspace.update(cx, |workspace, _cx| {
8775 let active_pane = workspace.active_pane();
8776 assert_eq!(top_pane_id, active_pane.entity_id());
8777 });
8778 }
8779
8780 fn add_an_item_to_active_pane(
8781 cx: &mut VisualTestContext,
8782 workspace: &Entity<Workspace>,
8783 item_id: u64,
8784 ) -> Entity<TestItem> {
8785 let item = cx.new(|cx| {
8786 TestItem::new(cx).with_project_items(&[TestProjectItem::new(
8787 item_id,
8788 "item{item_id}.txt",
8789 cx,
8790 )])
8791 });
8792 workspace.update_in(cx, |workspace, window, cx| {
8793 workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx);
8794 });
8795 return item;
8796 }
8797
8798 fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> {
8799 return workspace.update_in(cx, |workspace, window, cx| {
8800 let new_pane = workspace.split_pane(
8801 workspace.active_pane().clone(),
8802 SplitDirection::Right,
8803 window,
8804 cx,
8805 );
8806 new_pane
8807 });
8808 }
8809
8810 #[gpui::test]
8811 async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
8812 init_test(cx);
8813 let fs = FakeFs::new(cx.executor());
8814 let project = Project::test(fs, None, cx).await;
8815 let (workspace, cx) =
8816 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8817
8818 add_an_item_to_active_pane(cx, &workspace, 1);
8819 split_pane(cx, &workspace);
8820 add_an_item_to_active_pane(cx, &workspace, 2);
8821 split_pane(cx, &workspace); // empty pane
8822 split_pane(cx, &workspace);
8823 let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
8824
8825 cx.executor().run_until_parked();
8826
8827 workspace.update(cx, |workspace, cx| {
8828 let num_panes = workspace.panes().len();
8829 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
8830 let active_item = workspace
8831 .active_pane()
8832 .read(cx)
8833 .active_item()
8834 .expect("item is in focus");
8835
8836 assert_eq!(num_panes, 4);
8837 assert_eq!(num_items_in_current_pane, 1);
8838 assert_eq!(active_item.item_id(), last_item.item_id());
8839 });
8840
8841 workspace.update_in(cx, |workspace, window, cx| {
8842 workspace.join_all_panes(window, cx);
8843 });
8844
8845 workspace.update(cx, |workspace, cx| {
8846 let num_panes = workspace.panes().len();
8847 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
8848 let active_item = workspace
8849 .active_pane()
8850 .read(cx)
8851 .active_item()
8852 .expect("item is in focus");
8853
8854 assert_eq!(num_panes, 1);
8855 assert_eq!(num_items_in_current_pane, 3);
8856 assert_eq!(active_item.item_id(), last_item.item_id());
8857 });
8858 }
8859 struct TestModal(FocusHandle);
8860
8861 impl TestModal {
8862 fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
8863 Self(cx.focus_handle())
8864 }
8865 }
8866
8867 impl EventEmitter<DismissEvent> for TestModal {}
8868
8869 impl Focusable for TestModal {
8870 fn focus_handle(&self, _cx: &App) -> FocusHandle {
8871 self.0.clone()
8872 }
8873 }
8874
8875 impl ModalView for TestModal {}
8876
8877 impl Render for TestModal {
8878 fn render(
8879 &mut self,
8880 _window: &mut Window,
8881 _cx: &mut Context<TestModal>,
8882 ) -> impl IntoElement {
8883 div().track_focus(&self.0)
8884 }
8885 }
8886
8887 #[gpui::test]
8888 async fn test_panels(cx: &mut gpui::TestAppContext) {
8889 init_test(cx);
8890 let fs = FakeFs::new(cx.executor());
8891
8892 let project = Project::test(fs, [], cx).await;
8893 let (workspace, cx) =
8894 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8895
8896 let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| {
8897 let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, cx));
8898 workspace.add_panel(panel_1.clone(), window, cx);
8899 workspace.toggle_dock(DockPosition::Left, window, cx);
8900 let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
8901 workspace.add_panel(panel_2.clone(), window, cx);
8902 workspace.toggle_dock(DockPosition::Right, window, cx);
8903
8904 let left_dock = workspace.left_dock();
8905 assert_eq!(
8906 left_dock.read(cx).visible_panel().unwrap().panel_id(),
8907 panel_1.panel_id()
8908 );
8909 assert_eq!(
8910 left_dock.read(cx).active_panel_size(window, cx).unwrap(),
8911 panel_1.size(window, cx)
8912 );
8913
8914 left_dock.update(cx, |left_dock, cx| {
8915 left_dock.resize_active_panel(Some(px(1337.)), window, cx)
8916 });
8917 assert_eq!(
8918 workspace
8919 .right_dock()
8920 .read(cx)
8921 .visible_panel()
8922 .unwrap()
8923 .panel_id(),
8924 panel_2.panel_id(),
8925 );
8926
8927 (panel_1, panel_2)
8928 });
8929
8930 // Move panel_1 to the right
8931 panel_1.update_in(cx, |panel_1, window, cx| {
8932 panel_1.set_position(DockPosition::Right, window, cx)
8933 });
8934
8935 workspace.update_in(cx, |workspace, window, cx| {
8936 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
8937 // Since it was the only panel on the left, the left dock should now be closed.
8938 assert!(!workspace.left_dock().read(cx).is_open());
8939 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
8940 let right_dock = workspace.right_dock();
8941 assert_eq!(
8942 right_dock.read(cx).visible_panel().unwrap().panel_id(),
8943 panel_1.panel_id()
8944 );
8945 assert_eq!(
8946 right_dock.read(cx).active_panel_size(window, cx).unwrap(),
8947 px(1337.)
8948 );
8949
8950 // Now we move panel_2 to the left
8951 panel_2.set_position(DockPosition::Left, window, cx);
8952 });
8953
8954 workspace.update(cx, |workspace, cx| {
8955 // Since panel_2 was not visible on the right, we don't open the left dock.
8956 assert!(!workspace.left_dock().read(cx).is_open());
8957 // And the right dock is unaffected in its displaying of panel_1
8958 assert!(workspace.right_dock().read(cx).is_open());
8959 assert_eq!(
8960 workspace
8961 .right_dock()
8962 .read(cx)
8963 .visible_panel()
8964 .unwrap()
8965 .panel_id(),
8966 panel_1.panel_id(),
8967 );
8968 });
8969
8970 // Move panel_1 back to the left
8971 panel_1.update_in(cx, |panel_1, window, cx| {
8972 panel_1.set_position(DockPosition::Left, window, cx)
8973 });
8974
8975 workspace.update_in(cx, |workspace, window, cx| {
8976 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
8977 let left_dock = workspace.left_dock();
8978 assert!(left_dock.read(cx).is_open());
8979 assert_eq!(
8980 left_dock.read(cx).visible_panel().unwrap().panel_id(),
8981 panel_1.panel_id()
8982 );
8983 assert_eq!(
8984 left_dock.read(cx).active_panel_size(window, cx).unwrap(),
8985 px(1337.)
8986 );
8987 // And the right dock should be closed as it no longer has any panels.
8988 assert!(!workspace.right_dock().read(cx).is_open());
8989
8990 // Now we move panel_1 to the bottom
8991 panel_1.set_position(DockPosition::Bottom, window, cx);
8992 });
8993
8994 workspace.update_in(cx, |workspace, window, cx| {
8995 // Since panel_1 was visible on the left, we close the left dock.
8996 assert!(!workspace.left_dock().read(cx).is_open());
8997 // The bottom dock is sized based on the panel's default size,
8998 // since the panel orientation changed from vertical to horizontal.
8999 let bottom_dock = workspace.bottom_dock();
9000 assert_eq!(
9001 bottom_dock.read(cx).active_panel_size(window, cx).unwrap(),
9002 panel_1.size(window, cx),
9003 );
9004 // Close bottom dock and move panel_1 back to the left.
9005 bottom_dock.update(cx, |bottom_dock, cx| {
9006 bottom_dock.set_open(false, window, cx)
9007 });
9008 panel_1.set_position(DockPosition::Left, window, cx);
9009 });
9010
9011 // Emit activated event on panel 1
9012 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
9013
9014 // Now the left dock is open and panel_1 is active and focused.
9015 workspace.update_in(cx, |workspace, window, cx| {
9016 let left_dock = workspace.left_dock();
9017 assert!(left_dock.read(cx).is_open());
9018 assert_eq!(
9019 left_dock.read(cx).visible_panel().unwrap().panel_id(),
9020 panel_1.panel_id(),
9021 );
9022 assert!(panel_1.focus_handle(cx).is_focused(window));
9023 });
9024
9025 // Emit closed event on panel 2, which is not active
9026 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
9027
9028 // Wo don't close the left dock, because panel_2 wasn't the active panel
9029 workspace.update(cx, |workspace, cx| {
9030 let left_dock = workspace.left_dock();
9031 assert!(left_dock.read(cx).is_open());
9032 assert_eq!(
9033 left_dock.read(cx).visible_panel().unwrap().panel_id(),
9034 panel_1.panel_id(),
9035 );
9036 });
9037
9038 // Emitting a ZoomIn event shows the panel as zoomed.
9039 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
9040 workspace.read_with(cx, |workspace, _| {
9041 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
9042 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
9043 });
9044
9045 // Move panel to another dock while it is zoomed
9046 panel_1.update_in(cx, |panel, window, cx| {
9047 panel.set_position(DockPosition::Right, window, cx)
9048 });
9049 workspace.read_with(cx, |workspace, _| {
9050 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
9051
9052 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
9053 });
9054
9055 // This is a helper for getting a:
9056 // - valid focus on an element,
9057 // - that isn't a part of the panes and panels system of the Workspace,
9058 // - and doesn't trigger the 'on_focus_lost' API.
9059 let focus_other_view = {
9060 let workspace = workspace.clone();
9061 move |cx: &mut VisualTestContext| {
9062 workspace.update_in(cx, |workspace, window, cx| {
9063 if let Some(_) = workspace.active_modal::<TestModal>(cx) {
9064 workspace.toggle_modal(window, cx, TestModal::new);
9065 workspace.toggle_modal(window, cx, TestModal::new);
9066 } else {
9067 workspace.toggle_modal(window, cx, TestModal::new);
9068 }
9069 })
9070 }
9071 };
9072
9073 // If focus is transferred to another view that's not a panel or another pane, we still show
9074 // the panel as zoomed.
9075 focus_other_view(cx);
9076 workspace.read_with(cx, |workspace, _| {
9077 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
9078 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
9079 });
9080
9081 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
9082 workspace.update_in(cx, |_workspace, window, cx| {
9083 cx.focus_self(window);
9084 });
9085 workspace.read_with(cx, |workspace, _| {
9086 assert_eq!(workspace.zoomed, None);
9087 assert_eq!(workspace.zoomed_position, None);
9088 });
9089
9090 // If focus is transferred again to another view that's not a panel or a pane, we won't
9091 // show the panel as zoomed because it wasn't zoomed before.
9092 focus_other_view(cx);
9093 workspace.read_with(cx, |workspace, _| {
9094 assert_eq!(workspace.zoomed, None);
9095 assert_eq!(workspace.zoomed_position, None);
9096 });
9097
9098 // When the panel is activated, it is zoomed again.
9099 cx.dispatch_action(ToggleRightDock);
9100 workspace.read_with(cx, |workspace, _| {
9101 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
9102 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
9103 });
9104
9105 // Emitting a ZoomOut event unzooms the panel.
9106 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
9107 workspace.read_with(cx, |workspace, _| {
9108 assert_eq!(workspace.zoomed, None);
9109 assert_eq!(workspace.zoomed_position, None);
9110 });
9111
9112 // Emit closed event on panel 1, which is active
9113 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
9114
9115 // Now the left dock is closed, because panel_1 was the active panel
9116 workspace.update(cx, |workspace, cx| {
9117 let right_dock = workspace.right_dock();
9118 assert!(!right_dock.read(cx).is_open());
9119 });
9120 }
9121
9122 #[gpui::test]
9123 async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
9124 init_test(cx);
9125
9126 let fs = FakeFs::new(cx.background_executor.clone());
9127 let project = Project::test(fs, [], cx).await;
9128 let (workspace, cx) =
9129 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
9130 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
9131
9132 let dirty_regular_buffer = cx.new(|cx| {
9133 TestItem::new(cx)
9134 .with_dirty(true)
9135 .with_label("1.txt")
9136 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
9137 });
9138 let dirty_regular_buffer_2 = cx.new(|cx| {
9139 TestItem::new(cx)
9140 .with_dirty(true)
9141 .with_label("2.txt")
9142 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
9143 });
9144 let dirty_multi_buffer_with_both = cx.new(|cx| {
9145 TestItem::new(cx)
9146 .with_dirty(true)
9147 .with_singleton(false)
9148 .with_label("Fake Project Search")
9149 .with_project_items(&[
9150 dirty_regular_buffer.read(cx).project_items[0].clone(),
9151 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
9152 ])
9153 });
9154 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
9155 workspace.update_in(cx, |workspace, window, cx| {
9156 workspace.add_item(
9157 pane.clone(),
9158 Box::new(dirty_regular_buffer.clone()),
9159 None,
9160 false,
9161 false,
9162 window,
9163 cx,
9164 );
9165 workspace.add_item(
9166 pane.clone(),
9167 Box::new(dirty_regular_buffer_2.clone()),
9168 None,
9169 false,
9170 false,
9171 window,
9172 cx,
9173 );
9174 workspace.add_item(
9175 pane.clone(),
9176 Box::new(dirty_multi_buffer_with_both.clone()),
9177 None,
9178 false,
9179 false,
9180 window,
9181 cx,
9182 );
9183 });
9184
9185 pane.update_in(cx, |pane, window, cx| {
9186 pane.activate_item(2, true, true, window, cx);
9187 assert_eq!(
9188 pane.active_item().unwrap().item_id(),
9189 multi_buffer_with_both_files_id,
9190 "Should select the multi buffer in the pane"
9191 );
9192 });
9193 let close_all_but_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
9194 pane.close_inactive_items(
9195 &CloseInactiveItems {
9196 save_intent: Some(SaveIntent::Save),
9197 close_pinned: true,
9198 },
9199 window,
9200 cx,
9201 )
9202 });
9203 cx.background_executor.run_until_parked();
9204 assert!(!cx.has_pending_prompt());
9205 close_all_but_multi_buffer_task
9206 .await
9207 .expect("Closing all buffers but the multi buffer failed");
9208 pane.update(cx, |pane, cx| {
9209 assert_eq!(dirty_regular_buffer.read(cx).save_count, 1);
9210 assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
9211 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 1);
9212 assert_eq!(pane.items_len(), 1);
9213 assert_eq!(
9214 pane.active_item().unwrap().item_id(),
9215 multi_buffer_with_both_files_id,
9216 "Should have only the multi buffer left in the pane"
9217 );
9218 assert!(
9219 dirty_multi_buffer_with_both.read(cx).is_dirty,
9220 "The multi buffer containing the unsaved buffer should still be dirty"
9221 );
9222 });
9223
9224 dirty_regular_buffer.update(cx, |buffer, cx| {
9225 buffer.project_items[0].update(cx, |pi, _| pi.is_dirty = true)
9226 });
9227
9228 let close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
9229 pane.close_active_item(
9230 &CloseActiveItem {
9231 save_intent: Some(SaveIntent::Close),
9232 close_pinned: false,
9233 },
9234 window,
9235 cx,
9236 )
9237 });
9238 cx.background_executor.run_until_parked();
9239 assert!(
9240 cx.has_pending_prompt(),
9241 "Dirty multi buffer should prompt a save dialog"
9242 );
9243 cx.simulate_prompt_answer("Save");
9244 cx.background_executor.run_until_parked();
9245 close_multi_buffer_task
9246 .await
9247 .expect("Closing the multi buffer failed");
9248 pane.update(cx, |pane, cx| {
9249 assert_eq!(
9250 dirty_multi_buffer_with_both.read(cx).save_count,
9251 1,
9252 "Multi buffer item should get be saved"
9253 );
9254 // Test impl does not save inner items, so we do not assert them
9255 assert_eq!(
9256 pane.items_len(),
9257 0,
9258 "No more items should be left in the pane"
9259 );
9260 assert!(pane.active_item().is_none());
9261 });
9262 }
9263
9264 #[gpui::test]
9265 async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
9266 cx: &mut TestAppContext,
9267 ) {
9268 init_test(cx);
9269
9270 let fs = FakeFs::new(cx.background_executor.clone());
9271 let project = Project::test(fs, [], cx).await;
9272 let (workspace, cx) =
9273 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
9274 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
9275
9276 let dirty_regular_buffer = cx.new(|cx| {
9277 TestItem::new(cx)
9278 .with_dirty(true)
9279 .with_label("1.txt")
9280 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
9281 });
9282 let dirty_regular_buffer_2 = cx.new(|cx| {
9283 TestItem::new(cx)
9284 .with_dirty(true)
9285 .with_label("2.txt")
9286 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
9287 });
9288 let clear_regular_buffer = cx.new(|cx| {
9289 TestItem::new(cx)
9290 .with_label("3.txt")
9291 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
9292 });
9293
9294 let dirty_multi_buffer_with_both = cx.new(|cx| {
9295 TestItem::new(cx)
9296 .with_dirty(true)
9297 .with_singleton(false)
9298 .with_label("Fake Project Search")
9299 .with_project_items(&[
9300 dirty_regular_buffer.read(cx).project_items[0].clone(),
9301 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
9302 clear_regular_buffer.read(cx).project_items[0].clone(),
9303 ])
9304 });
9305 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
9306 workspace.update_in(cx, |workspace, window, cx| {
9307 workspace.add_item(
9308 pane.clone(),
9309 Box::new(dirty_regular_buffer.clone()),
9310 None,
9311 false,
9312 false,
9313 window,
9314 cx,
9315 );
9316 workspace.add_item(
9317 pane.clone(),
9318 Box::new(dirty_multi_buffer_with_both.clone()),
9319 None,
9320 false,
9321 false,
9322 window,
9323 cx,
9324 );
9325 });
9326
9327 pane.update_in(cx, |pane, window, cx| {
9328 pane.activate_item(1, true, true, window, cx);
9329 assert_eq!(
9330 pane.active_item().unwrap().item_id(),
9331 multi_buffer_with_both_files_id,
9332 "Should select the multi buffer in the pane"
9333 );
9334 });
9335 let _close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
9336 pane.close_active_item(
9337 &CloseActiveItem {
9338 save_intent: None,
9339 close_pinned: false,
9340 },
9341 window,
9342 cx,
9343 )
9344 });
9345 cx.background_executor.run_until_parked();
9346 assert!(
9347 cx.has_pending_prompt(),
9348 "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
9349 );
9350 }
9351
9352 /// Tests that when `close_on_file_delete` is enabled, files are automatically
9353 /// closed when they are deleted from disk.
9354 #[gpui::test]
9355 async fn test_close_on_disk_deletion_enabled(cx: &mut TestAppContext) {
9356 init_test(cx);
9357
9358 // Enable the close_on_disk_deletion setting
9359 cx.update_global(|store: &mut SettingsStore, cx| {
9360 store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
9361 settings.close_on_file_delete = Some(true);
9362 });
9363 });
9364
9365 let fs = FakeFs::new(cx.background_executor.clone());
9366 let project = Project::test(fs, [], cx).await;
9367 let (workspace, cx) =
9368 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
9369 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
9370
9371 // Create a test item that simulates a file
9372 let item = cx.new(|cx| {
9373 TestItem::new(cx)
9374 .with_label("test.txt")
9375 .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
9376 });
9377
9378 // Add item to workspace
9379 workspace.update_in(cx, |workspace, window, cx| {
9380 workspace.add_item(
9381 pane.clone(),
9382 Box::new(item.clone()),
9383 None,
9384 false,
9385 false,
9386 window,
9387 cx,
9388 );
9389 });
9390
9391 // Verify the item is in the pane
9392 pane.read_with(cx, |pane, _| {
9393 assert_eq!(pane.items().count(), 1);
9394 });
9395
9396 // Simulate file deletion by setting the item's deleted state
9397 item.update(cx, |item, _| {
9398 item.set_has_deleted_file(true);
9399 });
9400
9401 // Emit UpdateTab event to trigger the close behavior
9402 cx.run_until_parked();
9403 item.update(cx, |_, cx| {
9404 cx.emit(ItemEvent::UpdateTab);
9405 });
9406
9407 // Allow the close operation to complete
9408 cx.run_until_parked();
9409
9410 // Verify the item was automatically closed
9411 pane.read_with(cx, |pane, _| {
9412 assert_eq!(
9413 pane.items().count(),
9414 0,
9415 "Item should be automatically closed when file is deleted"
9416 );
9417 });
9418 }
9419
9420 /// Tests that when `close_on_file_delete` is disabled (default), files remain
9421 /// open with a strikethrough when they are deleted from disk.
9422 #[gpui::test]
9423 async fn test_close_on_disk_deletion_disabled(cx: &mut TestAppContext) {
9424 init_test(cx);
9425
9426 // Ensure close_on_disk_deletion is disabled (default)
9427 cx.update_global(|store: &mut SettingsStore, cx| {
9428 store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
9429 settings.close_on_file_delete = Some(false);
9430 });
9431 });
9432
9433 let fs = FakeFs::new(cx.background_executor.clone());
9434 let project = Project::test(fs, [], cx).await;
9435 let (workspace, cx) =
9436 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
9437 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
9438
9439 // Create a test item that simulates a file
9440 let item = cx.new(|cx| {
9441 TestItem::new(cx)
9442 .with_label("test.txt")
9443 .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
9444 });
9445
9446 // Add item to workspace
9447 workspace.update_in(cx, |workspace, window, cx| {
9448 workspace.add_item(
9449 pane.clone(),
9450 Box::new(item.clone()),
9451 None,
9452 false,
9453 false,
9454 window,
9455 cx,
9456 );
9457 });
9458
9459 // Verify the item is in the pane
9460 pane.read_with(cx, |pane, _| {
9461 assert_eq!(pane.items().count(), 1);
9462 });
9463
9464 // Simulate file deletion
9465 item.update(cx, |item, _| {
9466 item.set_has_deleted_file(true);
9467 });
9468
9469 // Emit UpdateTab event
9470 cx.run_until_parked();
9471 item.update(cx, |_, cx| {
9472 cx.emit(ItemEvent::UpdateTab);
9473 });
9474
9475 // Allow any potential close operation to complete
9476 cx.run_until_parked();
9477
9478 // Verify the item remains open (with strikethrough)
9479 pane.read_with(cx, |pane, _| {
9480 assert_eq!(
9481 pane.items().count(),
9482 1,
9483 "Item should remain open when close_on_disk_deletion is disabled"
9484 );
9485 });
9486
9487 // Verify the item shows as deleted
9488 item.read_with(cx, |item, _| {
9489 assert!(
9490 item.has_deleted_file,
9491 "Item should be marked as having deleted file"
9492 );
9493 });
9494 }
9495
9496 /// Tests that dirty files are not automatically closed when deleted from disk,
9497 /// even when `close_on_file_delete` is enabled. This ensures users don't lose
9498 /// unsaved changes without being prompted.
9499 #[gpui::test]
9500 async fn test_close_on_disk_deletion_with_dirty_file(cx: &mut TestAppContext) {
9501 init_test(cx);
9502
9503 // Enable the close_on_file_delete setting
9504 cx.update_global(|store: &mut SettingsStore, cx| {
9505 store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
9506 settings.close_on_file_delete = Some(true);
9507 });
9508 });
9509
9510 let fs = FakeFs::new(cx.background_executor.clone());
9511 let project = Project::test(fs, [], cx).await;
9512 let (workspace, cx) =
9513 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
9514 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
9515
9516 // Create a dirty test item
9517 let item = cx.new(|cx| {
9518 TestItem::new(cx)
9519 .with_dirty(true)
9520 .with_label("test.txt")
9521 .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
9522 });
9523
9524 // Add item to workspace
9525 workspace.update_in(cx, |workspace, window, cx| {
9526 workspace.add_item(
9527 pane.clone(),
9528 Box::new(item.clone()),
9529 None,
9530 false,
9531 false,
9532 window,
9533 cx,
9534 );
9535 });
9536
9537 // Simulate file deletion
9538 item.update(cx, |item, _| {
9539 item.set_has_deleted_file(true);
9540 });
9541
9542 // Emit UpdateTab event to trigger the close behavior
9543 cx.run_until_parked();
9544 item.update(cx, |_, cx| {
9545 cx.emit(ItemEvent::UpdateTab);
9546 });
9547
9548 // Allow any potential close operation to complete
9549 cx.run_until_parked();
9550
9551 // Verify the item remains open (dirty files are not auto-closed)
9552 pane.read_with(cx, |pane, _| {
9553 assert_eq!(
9554 pane.items().count(),
9555 1,
9556 "Dirty items should not be automatically closed even when file is deleted"
9557 );
9558 });
9559
9560 // Verify the item is marked as deleted and still dirty
9561 item.read_with(cx, |item, _| {
9562 assert!(
9563 item.has_deleted_file,
9564 "Item should be marked as having deleted file"
9565 );
9566 assert!(item.is_dirty, "Item should still be dirty");
9567 });
9568 }
9569
9570 /// Tests that navigation history is cleaned up when files are auto-closed
9571 /// due to deletion from disk.
9572 #[gpui::test]
9573 async fn test_close_on_disk_deletion_cleans_navigation_history(cx: &mut TestAppContext) {
9574 init_test(cx);
9575
9576 // Enable the close_on_file_delete setting
9577 cx.update_global(|store: &mut SettingsStore, cx| {
9578 store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
9579 settings.close_on_file_delete = Some(true);
9580 });
9581 });
9582
9583 let fs = FakeFs::new(cx.background_executor.clone());
9584 let project = Project::test(fs, [], cx).await;
9585 let (workspace, cx) =
9586 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
9587 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
9588
9589 // Create test items
9590 let item1 = cx.new(|cx| {
9591 TestItem::new(cx)
9592 .with_label("test1.txt")
9593 .with_project_items(&[TestProjectItem::new(1, "test1.txt", cx)])
9594 });
9595 let item1_id = item1.item_id();
9596
9597 let item2 = cx.new(|cx| {
9598 TestItem::new(cx)
9599 .with_label("test2.txt")
9600 .with_project_items(&[TestProjectItem::new(2, "test2.txt", cx)])
9601 });
9602
9603 // Add items to workspace
9604 workspace.update_in(cx, |workspace, window, cx| {
9605 workspace.add_item(
9606 pane.clone(),
9607 Box::new(item1.clone()),
9608 None,
9609 false,
9610 false,
9611 window,
9612 cx,
9613 );
9614 workspace.add_item(
9615 pane.clone(),
9616 Box::new(item2.clone()),
9617 None,
9618 false,
9619 false,
9620 window,
9621 cx,
9622 );
9623 });
9624
9625 // Activate item1 to ensure it gets navigation entries
9626 pane.update_in(cx, |pane, window, cx| {
9627 pane.activate_item(0, true, true, window, cx);
9628 });
9629
9630 // Switch to item2 and back to create navigation history
9631 pane.update_in(cx, |pane, window, cx| {
9632 pane.activate_item(1, true, true, window, cx);
9633 });
9634 cx.run_until_parked();
9635
9636 pane.update_in(cx, |pane, window, cx| {
9637 pane.activate_item(0, true, true, window, cx);
9638 });
9639 cx.run_until_parked();
9640
9641 // Simulate file deletion for item1
9642 item1.update(cx, |item, _| {
9643 item.set_has_deleted_file(true);
9644 });
9645
9646 // Emit UpdateTab event to trigger the close behavior
9647 item1.update(cx, |_, cx| {
9648 cx.emit(ItemEvent::UpdateTab);
9649 });
9650 cx.run_until_parked();
9651
9652 // Verify item1 was closed
9653 pane.read_with(cx, |pane, _| {
9654 assert_eq!(
9655 pane.items().count(),
9656 1,
9657 "Should have 1 item remaining after auto-close"
9658 );
9659 });
9660
9661 // Check navigation history after close
9662 let has_item = pane.read_with(cx, |pane, cx| {
9663 let mut has_item = false;
9664 pane.nav_history().for_each_entry(cx, |entry, _| {
9665 if entry.item.id() == item1_id {
9666 has_item = true;
9667 }
9668 });
9669 has_item
9670 });
9671
9672 assert!(
9673 !has_item,
9674 "Navigation history should not contain closed item entries"
9675 );
9676 }
9677
9678 #[gpui::test]
9679 async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
9680 cx: &mut TestAppContext,
9681 ) {
9682 init_test(cx);
9683
9684 let fs = FakeFs::new(cx.background_executor.clone());
9685 let project = Project::test(fs, [], cx).await;
9686 let (workspace, cx) =
9687 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
9688 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
9689
9690 let dirty_regular_buffer = cx.new(|cx| {
9691 TestItem::new(cx)
9692 .with_dirty(true)
9693 .with_label("1.txt")
9694 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
9695 });
9696 let dirty_regular_buffer_2 = cx.new(|cx| {
9697 TestItem::new(cx)
9698 .with_dirty(true)
9699 .with_label("2.txt")
9700 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
9701 });
9702 let clear_regular_buffer = cx.new(|cx| {
9703 TestItem::new(cx)
9704 .with_label("3.txt")
9705 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
9706 });
9707
9708 let dirty_multi_buffer = cx.new(|cx| {
9709 TestItem::new(cx)
9710 .with_dirty(true)
9711 .with_singleton(false)
9712 .with_label("Fake Project Search")
9713 .with_project_items(&[
9714 dirty_regular_buffer.read(cx).project_items[0].clone(),
9715 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
9716 clear_regular_buffer.read(cx).project_items[0].clone(),
9717 ])
9718 });
9719 workspace.update_in(cx, |workspace, window, cx| {
9720 workspace.add_item(
9721 pane.clone(),
9722 Box::new(dirty_regular_buffer.clone()),
9723 None,
9724 false,
9725 false,
9726 window,
9727 cx,
9728 );
9729 workspace.add_item(
9730 pane.clone(),
9731 Box::new(dirty_regular_buffer_2.clone()),
9732 None,
9733 false,
9734 false,
9735 window,
9736 cx,
9737 );
9738 workspace.add_item(
9739 pane.clone(),
9740 Box::new(dirty_multi_buffer.clone()),
9741 None,
9742 false,
9743 false,
9744 window,
9745 cx,
9746 );
9747 });
9748
9749 pane.update_in(cx, |pane, window, cx| {
9750 pane.activate_item(2, true, true, window, cx);
9751 assert_eq!(
9752 pane.active_item().unwrap().item_id(),
9753 dirty_multi_buffer.item_id(),
9754 "Should select the multi buffer in the pane"
9755 );
9756 });
9757 let close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
9758 pane.close_active_item(
9759 &CloseActiveItem {
9760 save_intent: None,
9761 close_pinned: false,
9762 },
9763 window,
9764 cx,
9765 )
9766 });
9767 cx.background_executor.run_until_parked();
9768 assert!(
9769 !cx.has_pending_prompt(),
9770 "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
9771 );
9772 close_multi_buffer_task
9773 .await
9774 .expect("Closing multi buffer failed");
9775 pane.update(cx, |pane, cx| {
9776 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
9777 assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
9778 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
9779 assert_eq!(
9780 pane.items()
9781 .map(|item| item.item_id())
9782 .sorted()
9783 .collect::<Vec<_>>(),
9784 vec![
9785 dirty_regular_buffer.item_id(),
9786 dirty_regular_buffer_2.item_id(),
9787 ],
9788 "Should have no multi buffer left in the pane"
9789 );
9790 assert!(dirty_regular_buffer.read(cx).is_dirty);
9791 assert!(dirty_regular_buffer_2.read(cx).is_dirty);
9792 });
9793 }
9794
9795 #[gpui::test]
9796 async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) {
9797 init_test(cx);
9798 let fs = FakeFs::new(cx.executor());
9799 let project = Project::test(fs, [], cx).await;
9800 let (workspace, cx) =
9801 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
9802
9803 // Add a new panel to the right dock, opening the dock and setting the
9804 // focus to the new panel.
9805 let panel = workspace.update_in(cx, |workspace, window, cx| {
9806 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
9807 workspace.add_panel(panel.clone(), window, cx);
9808
9809 workspace
9810 .right_dock()
9811 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
9812
9813 workspace.toggle_panel_focus::<TestPanel>(window, cx);
9814
9815 panel
9816 });
9817
9818 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
9819 // panel to the next valid position which, in this case, is the left
9820 // dock.
9821 cx.dispatch_action(MoveFocusedPanelToNextPosition);
9822 workspace.update(cx, |workspace, cx| {
9823 assert!(workspace.left_dock().read(cx).is_open());
9824 assert_eq!(panel.read(cx).position, DockPosition::Left);
9825 });
9826
9827 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
9828 // panel to the next valid position which, in this case, is the bottom
9829 // dock.
9830 cx.dispatch_action(MoveFocusedPanelToNextPosition);
9831 workspace.update(cx, |workspace, cx| {
9832 assert!(workspace.bottom_dock().read(cx).is_open());
9833 assert_eq!(panel.read(cx).position, DockPosition::Bottom);
9834 });
9835
9836 // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time
9837 // around moving the panel to its initial position, the right dock.
9838 cx.dispatch_action(MoveFocusedPanelToNextPosition);
9839 workspace.update(cx, |workspace, cx| {
9840 assert!(workspace.right_dock().read(cx).is_open());
9841 assert_eq!(panel.read(cx).position, DockPosition::Right);
9842 });
9843
9844 // Remove focus from the panel, ensuring that, if the panel is not
9845 // focused, the `MoveFocusedPanelToNextPosition` action does not update
9846 // the panel's position, so the panel is still in the right dock.
9847 workspace.update_in(cx, |workspace, window, cx| {
9848 workspace.toggle_panel_focus::<TestPanel>(window, cx);
9849 });
9850
9851 cx.dispatch_action(MoveFocusedPanelToNextPosition);
9852 workspace.update(cx, |workspace, cx| {
9853 assert!(workspace.right_dock().read(cx).is_open());
9854 assert_eq!(panel.read(cx).position, DockPosition::Right);
9855 });
9856 }
9857
9858 #[gpui::test]
9859 async fn test_moving_items_create_panes(cx: &mut TestAppContext) {
9860 init_test(cx);
9861
9862 let fs = FakeFs::new(cx.executor());
9863 let project = Project::test(fs, [], cx).await;
9864 let (workspace, cx) =
9865 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
9866
9867 let item_1 = cx.new(|cx| {
9868 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "first.txt", cx)])
9869 });
9870 workspace.update_in(cx, |workspace, window, cx| {
9871 workspace.add_item_to_active_pane(Box::new(item_1), None, true, window, cx);
9872 workspace.move_item_to_pane_in_direction(
9873 &MoveItemToPaneInDirection {
9874 direction: SplitDirection::Right,
9875 focus: true,
9876 clone: false,
9877 },
9878 window,
9879 cx,
9880 );
9881 workspace.move_item_to_pane_at_index(
9882 &MoveItemToPane {
9883 destination: 3,
9884 focus: true,
9885 clone: false,
9886 },
9887 window,
9888 cx,
9889 );
9890
9891 assert_eq!(workspace.panes.len(), 1, "No new panes were created");
9892 assert_eq!(
9893 pane_items_paths(&workspace.active_pane, cx),
9894 vec!["first.txt".to_string()],
9895 "Single item was not moved anywhere"
9896 );
9897 });
9898
9899 let item_2 = cx.new(|cx| {
9900 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "second.txt", cx)])
9901 });
9902 workspace.update_in(cx, |workspace, window, cx| {
9903 workspace.add_item_to_active_pane(Box::new(item_2), None, true, window, cx);
9904 assert_eq!(
9905 pane_items_paths(&workspace.panes[0], cx),
9906 vec!["first.txt".to_string(), "second.txt".to_string()],
9907 );
9908 workspace.move_item_to_pane_in_direction(
9909 &MoveItemToPaneInDirection {
9910 direction: SplitDirection::Right,
9911 focus: true,
9912 clone: false,
9913 },
9914 window,
9915 cx,
9916 );
9917
9918 assert_eq!(workspace.panes.len(), 2, "A new pane should be created");
9919 assert_eq!(
9920 pane_items_paths(&workspace.panes[0], cx),
9921 vec!["first.txt".to_string()],
9922 "After moving, one item should be left in the original pane"
9923 );
9924 assert_eq!(
9925 pane_items_paths(&workspace.panes[1], cx),
9926 vec!["second.txt".to_string()],
9927 "New item should have been moved to the new pane"
9928 );
9929 });
9930
9931 let item_3 = cx.new(|cx| {
9932 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "third.txt", cx)])
9933 });
9934 workspace.update_in(cx, |workspace, window, cx| {
9935 let original_pane = workspace.panes[0].clone();
9936 workspace.set_active_pane(&original_pane, window, cx);
9937 workspace.add_item_to_active_pane(Box::new(item_3), None, true, window, cx);
9938 assert_eq!(workspace.panes.len(), 2, "No new panes were created");
9939 assert_eq!(
9940 pane_items_paths(&workspace.active_pane, cx),
9941 vec!["first.txt".to_string(), "third.txt".to_string()],
9942 "New pane should be ready to move one item out"
9943 );
9944
9945 workspace.move_item_to_pane_at_index(
9946 &MoveItemToPane {
9947 destination: 3,
9948 focus: true,
9949 clone: false,
9950 },
9951 window,
9952 cx,
9953 );
9954 assert_eq!(workspace.panes.len(), 3, "A new pane should be created");
9955 assert_eq!(
9956 pane_items_paths(&workspace.active_pane, cx),
9957 vec!["first.txt".to_string()],
9958 "After moving, one item should be left in the original pane"
9959 );
9960 assert_eq!(
9961 pane_items_paths(&workspace.panes[1], cx),
9962 vec!["second.txt".to_string()],
9963 "Previously created pane should be unchanged"
9964 );
9965 assert_eq!(
9966 pane_items_paths(&workspace.panes[2], cx),
9967 vec!["third.txt".to_string()],
9968 "New item should have been moved to the new pane"
9969 );
9970 });
9971 }
9972
9973 #[gpui::test]
9974 async fn test_moving_items_can_clone_panes(cx: &mut TestAppContext) {
9975 init_test(cx);
9976
9977 let fs = FakeFs::new(cx.executor());
9978 let project = Project::test(fs, [], cx).await;
9979 let (workspace, cx) =
9980 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
9981
9982 let item_1 = cx.new(|cx| {
9983 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "first.txt", cx)])
9984 });
9985 workspace.update_in(cx, |workspace, window, cx| {
9986 workspace.add_item_to_active_pane(Box::new(item_1), None, true, window, cx);
9987 workspace.move_item_to_pane_in_direction(
9988 &MoveItemToPaneInDirection {
9989 direction: SplitDirection::Right,
9990 focus: true,
9991 clone: true,
9992 },
9993 window,
9994 cx,
9995 );
9996 workspace.move_item_to_pane_at_index(
9997 &MoveItemToPane {
9998 destination: 3,
9999 focus: true,
10000 clone: true,
10001 },
10002 window,
10003 cx,
10004 );
10005
10006 assert_eq!(workspace.panes.len(), 3, "Two new panes were created");
10007 for pane in workspace.panes() {
10008 assert_eq!(
10009 pane_items_paths(pane, cx),
10010 vec!["first.txt".to_string()],
10011 "Single item exists in all panes"
10012 );
10013 }
10014 });
10015
10016 // verify that the active pane has been updated after waiting for the
10017 // pane focus event to fire and resolve
10018 workspace.read_with(cx, |workspace, _app| {
10019 assert_eq!(
10020 workspace.active_pane(),
10021 &workspace.panes[2],
10022 "The third pane should be the active one: {:?}",
10023 workspace.panes
10024 );
10025 })
10026 }
10027
10028 mod register_project_item_tests {
10029
10030 use super::*;
10031
10032 // View
10033 struct TestPngItemView {
10034 focus_handle: FocusHandle,
10035 }
10036 // Model
10037 struct TestPngItem {}
10038
10039 impl project::ProjectItem for TestPngItem {
10040 fn try_open(
10041 _project: &Entity<Project>,
10042 path: &ProjectPath,
10043 cx: &mut App,
10044 ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
10045 if path.path.extension().unwrap() == "png" {
10046 Some(cx.spawn(async move |cx| cx.new(|_| TestPngItem {})))
10047 } else {
10048 None
10049 }
10050 }
10051
10052 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
10053 None
10054 }
10055
10056 fn project_path(&self, _: &App) -> Option<ProjectPath> {
10057 None
10058 }
10059
10060 fn is_dirty(&self) -> bool {
10061 false
10062 }
10063 }
10064
10065 impl Item for TestPngItemView {
10066 type Event = ();
10067 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
10068 "".into()
10069 }
10070 }
10071 impl EventEmitter<()> for TestPngItemView {}
10072 impl Focusable for TestPngItemView {
10073 fn focus_handle(&self, _cx: &App) -> FocusHandle {
10074 self.focus_handle.clone()
10075 }
10076 }
10077
10078 impl Render for TestPngItemView {
10079 fn render(
10080 &mut self,
10081 _window: &mut Window,
10082 _cx: &mut Context<Self>,
10083 ) -> impl IntoElement {
10084 Empty
10085 }
10086 }
10087
10088 impl ProjectItem for TestPngItemView {
10089 type Item = TestPngItem;
10090
10091 fn for_project_item(
10092 _project: Entity<Project>,
10093 _pane: Option<&Pane>,
10094 _item: Entity<Self::Item>,
10095 _: &mut Window,
10096 cx: &mut Context<Self>,
10097 ) -> Self
10098 where
10099 Self: Sized,
10100 {
10101 Self {
10102 focus_handle: cx.focus_handle(),
10103 }
10104 }
10105 }
10106
10107 // View
10108 struct TestIpynbItemView {
10109 focus_handle: FocusHandle,
10110 }
10111 // Model
10112 struct TestIpynbItem {}
10113
10114 impl project::ProjectItem for TestIpynbItem {
10115 fn try_open(
10116 _project: &Entity<Project>,
10117 path: &ProjectPath,
10118 cx: &mut App,
10119 ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
10120 if path.path.extension().unwrap() == "ipynb" {
10121 Some(cx.spawn(async move |cx| cx.new(|_| TestIpynbItem {})))
10122 } else {
10123 None
10124 }
10125 }
10126
10127 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
10128 None
10129 }
10130
10131 fn project_path(&self, _: &App) -> Option<ProjectPath> {
10132 None
10133 }
10134
10135 fn is_dirty(&self) -> bool {
10136 false
10137 }
10138 }
10139
10140 impl Item for TestIpynbItemView {
10141 type Event = ();
10142 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
10143 "".into()
10144 }
10145 }
10146 impl EventEmitter<()> for TestIpynbItemView {}
10147 impl Focusable for TestIpynbItemView {
10148 fn focus_handle(&self, _cx: &App) -> FocusHandle {
10149 self.focus_handle.clone()
10150 }
10151 }
10152
10153 impl Render for TestIpynbItemView {
10154 fn render(
10155 &mut self,
10156 _window: &mut Window,
10157 _cx: &mut Context<Self>,
10158 ) -> impl IntoElement {
10159 Empty
10160 }
10161 }
10162
10163 impl ProjectItem for TestIpynbItemView {
10164 type Item = TestIpynbItem;
10165
10166 fn for_project_item(
10167 _project: Entity<Project>,
10168 _pane: Option<&Pane>,
10169 _item: Entity<Self::Item>,
10170 _: &mut Window,
10171 cx: &mut Context<Self>,
10172 ) -> Self
10173 where
10174 Self: Sized,
10175 {
10176 Self {
10177 focus_handle: cx.focus_handle(),
10178 }
10179 }
10180 }
10181
10182 struct TestAlternatePngItemView {
10183 focus_handle: FocusHandle,
10184 }
10185
10186 impl Item for TestAlternatePngItemView {
10187 type Event = ();
10188 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
10189 "".into()
10190 }
10191 }
10192
10193 impl EventEmitter<()> for TestAlternatePngItemView {}
10194 impl Focusable for TestAlternatePngItemView {
10195 fn focus_handle(&self, _cx: &App) -> FocusHandle {
10196 self.focus_handle.clone()
10197 }
10198 }
10199
10200 impl Render for TestAlternatePngItemView {
10201 fn render(
10202 &mut self,
10203 _window: &mut Window,
10204 _cx: &mut Context<Self>,
10205 ) -> impl IntoElement {
10206 Empty
10207 }
10208 }
10209
10210 impl ProjectItem for TestAlternatePngItemView {
10211 type Item = TestPngItem;
10212
10213 fn for_project_item(
10214 _project: Entity<Project>,
10215 _pane: Option<&Pane>,
10216 _item: Entity<Self::Item>,
10217 _: &mut Window,
10218 cx: &mut Context<Self>,
10219 ) -> Self
10220 where
10221 Self: Sized,
10222 {
10223 Self {
10224 focus_handle: cx.focus_handle(),
10225 }
10226 }
10227 }
10228
10229 #[gpui::test]
10230 async fn test_register_project_item(cx: &mut TestAppContext) {
10231 init_test(cx);
10232
10233 cx.update(|cx| {
10234 register_project_item::<TestPngItemView>(cx);
10235 register_project_item::<TestIpynbItemView>(cx);
10236 });
10237
10238 let fs = FakeFs::new(cx.executor());
10239 fs.insert_tree(
10240 "/root1",
10241 json!({
10242 "one.png": "BINARYDATAHERE",
10243 "two.ipynb": "{ totally a notebook }",
10244 "three.txt": "editing text, sure why not?"
10245 }),
10246 )
10247 .await;
10248
10249 let project = Project::test(fs, ["root1".as_ref()], cx).await;
10250 let (workspace, cx) =
10251 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
10252
10253 let worktree_id = project.update(cx, |project, cx| {
10254 project.worktrees(cx).next().unwrap().read(cx).id()
10255 });
10256
10257 let handle = workspace
10258 .update_in(cx, |workspace, window, cx| {
10259 let project_path = (worktree_id, "one.png");
10260 workspace.open_path(project_path, None, true, window, cx)
10261 })
10262 .await
10263 .unwrap();
10264
10265 // Now we can check if the handle we got back errored or not
10266 assert_eq!(
10267 handle.to_any().entity_type(),
10268 TypeId::of::<TestPngItemView>()
10269 );
10270
10271 let handle = workspace
10272 .update_in(cx, |workspace, window, cx| {
10273 let project_path = (worktree_id, "two.ipynb");
10274 workspace.open_path(project_path, None, true, window, cx)
10275 })
10276 .await
10277 .unwrap();
10278
10279 assert_eq!(
10280 handle.to_any().entity_type(),
10281 TypeId::of::<TestIpynbItemView>()
10282 );
10283
10284 let handle = workspace
10285 .update_in(cx, |workspace, window, cx| {
10286 let project_path = (worktree_id, "three.txt");
10287 workspace.open_path(project_path, None, true, window, cx)
10288 })
10289 .await;
10290 assert!(handle.is_err());
10291 }
10292
10293 #[gpui::test]
10294 async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
10295 init_test(cx);
10296
10297 cx.update(|cx| {
10298 register_project_item::<TestPngItemView>(cx);
10299 register_project_item::<TestAlternatePngItemView>(cx);
10300 });
10301
10302 let fs = FakeFs::new(cx.executor());
10303 fs.insert_tree(
10304 "/root1",
10305 json!({
10306 "one.png": "BINARYDATAHERE",
10307 "two.ipynb": "{ totally a notebook }",
10308 "three.txt": "editing text, sure why not?"
10309 }),
10310 )
10311 .await;
10312 let project = Project::test(fs, ["root1".as_ref()], cx).await;
10313 let (workspace, cx) =
10314 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
10315 let worktree_id = project.update(cx, |project, cx| {
10316 project.worktrees(cx).next().unwrap().read(cx).id()
10317 });
10318
10319 let handle = workspace
10320 .update_in(cx, |workspace, window, cx| {
10321 let project_path = (worktree_id, "one.png");
10322 workspace.open_path(project_path, None, true, window, cx)
10323 })
10324 .await
10325 .unwrap();
10326
10327 // This _must_ be the second item registered
10328 assert_eq!(
10329 handle.to_any().entity_type(),
10330 TypeId::of::<TestAlternatePngItemView>()
10331 );
10332
10333 let handle = workspace
10334 .update_in(cx, |workspace, window, cx| {
10335 let project_path = (worktree_id, "three.txt");
10336 workspace.open_path(project_path, None, true, window, cx)
10337 })
10338 .await;
10339 assert!(handle.is_err());
10340 }
10341 }
10342
10343 fn pane_items_paths(pane: &Entity<Pane>, cx: &App) -> Vec<String> {
10344 pane.read(cx)
10345 .items()
10346 .flat_map(|item| {
10347 item.project_paths(cx)
10348 .into_iter()
10349 .map(|path| path.path.to_string_lossy().to_string())
10350 })
10351 .collect()
10352 }
10353
10354 pub fn init_test(cx: &mut TestAppContext) {
10355 cx.update(|cx| {
10356 let settings_store = SettingsStore::test(cx);
10357 cx.set_global(settings_store);
10358 theme::init(theme::LoadThemes::JustBase, cx);
10359 language::init(cx);
10360 crate::init_settings(cx);
10361 Project::init_settings(cx);
10362 });
10363 }
10364
10365 fn dirty_project_item(id: u64, path: &str, cx: &mut App) -> Entity<TestProjectItem> {
10366 let item = TestProjectItem::new(id, path, cx);
10367 item.update(cx, |item, _| {
10368 item.is_dirty = true;
10369 });
10370 item
10371 }
10372}