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