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