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