@vertesia/ui 1.0.0 → 1.1.0-dev.20260427.060440Z

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (293) hide show
  1. package/lib/esm/core/components/ComboBox.js +23 -24
  2. package/lib/esm/core/components/ComboBox.js.map +1 -1
  3. package/lib/esm/core/components/FormItem.js +2 -2
  4. package/lib/esm/core/components/FormItem.js.map +1 -1
  5. package/lib/esm/core/components/SidePanel.js +1 -1
  6. package/lib/esm/core/components/SidePanel.js.map +1 -1
  7. package/lib/esm/core/components/shadcn/collaspible.js +3 -4
  8. package/lib/esm/core/components/shadcn/collaspible.js.map +1 -1
  9. package/lib/esm/core/components/shadcn/dropdown.js +37 -12
  10. package/lib/esm/core/components/shadcn/dropdown.js.map +1 -1
  11. package/lib/esm/core/components/shadcn/filters/comboBox/DateCombobox.js +6 -6
  12. package/lib/esm/core/components/shadcn/filters/comboBox/DateCombobox.js.map +1 -1
  13. package/lib/esm/core/components/shadcn/filters/filterBar.js +5 -3
  14. package/lib/esm/core/components/shadcn/filters/filterBar.js.map +1 -1
  15. package/lib/esm/core/components/shadcn/radioGroup.js +1 -2
  16. package/lib/esm/core/components/shadcn/radioGroup.js.map +1 -1
  17. package/lib/esm/core/components/shadcn/switch.js +0 -1
  18. package/lib/esm/core/components/shadcn/switch.js.map +1 -1
  19. package/lib/esm/core/components/shadcn/tabs.js +2 -2
  20. package/lib/esm/core/components/shadcn/tabs.js.map +1 -1
  21. package/lib/esm/core/components/shadcn/tooltip.js +17 -1
  22. package/lib/esm/core/components/shadcn/tooltip.js.map +1 -1
  23. package/lib/esm/core/hooks/PortalContainerProvider.js +9 -3
  24. package/lib/esm/core/hooks/PortalContainerProvider.js.map +1 -1
  25. package/lib/esm/env/index.js +5 -8
  26. package/lib/esm/env/index.js.map +1 -1
  27. package/lib/esm/features/agent/chat/AgentRightPanel.js +21 -11
  28. package/lib/esm/features/agent/chat/AgentRightPanel.js.map +1 -1
  29. package/lib/esm/features/agent/chat/AskUserWidget.js +2 -6
  30. package/lib/esm/features/agent/chat/AskUserWidget.js.map +1 -1
  31. package/lib/esm/features/agent/chat/DocumentPanel.js +8 -5
  32. package/lib/esm/features/agent/chat/DocumentPanel.js.map +1 -1
  33. package/lib/esm/features/agent/chat/DocumentTabBar.js +5 -13
  34. package/lib/esm/features/agent/chat/DocumentTabBar.js.map +1 -1
  35. package/lib/esm/features/agent/chat/ModernAgentConversation.js +57 -26
  36. package/lib/esm/features/agent/chat/ModernAgentConversation.js.map +1 -1
  37. package/lib/esm/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js +20 -16
  38. package/lib/esm/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js.map +1 -1
  39. package/lib/esm/features/agent/chat/ModernAgentOutput/Header.js +56 -45
  40. package/lib/esm/features/agent/chat/ModernAgentOutput/Header.js.map +1 -1
  41. package/lib/esm/features/agent/chat/ModernAgentOutput/MessageItem.js +1 -1
  42. package/lib/esm/features/agent/chat/ModernAgentOutput/MessageItem.js.map +1 -1
  43. package/lib/esm/features/agent/chat/SlidingThinkingIndicator.js +3 -9
  44. package/lib/esm/features/agent/chat/SlidingThinkingIndicator.js.map +1 -1
  45. package/lib/esm/features/agent/chat/hooks/useAgentStream.js +9 -5
  46. package/lib/esm/features/agent/chat/hooks/useAgentStream.js.map +1 -1
  47. package/lib/esm/features/agent/chat/hooks/useDocumentPanel.js +4 -0
  48. package/lib/esm/features/agent/chat/hooks/useDocumentPanel.js.map +1 -1
  49. package/lib/esm/features/facets/AgentRunnerFacetsNav.js +1 -1
  50. package/lib/esm/features/facets/AgentRunnerFacetsNav.js.map +1 -1
  51. package/lib/esm/features/facets/DocumentsFacetsNav.js +3 -2
  52. package/lib/esm/features/facets/DocumentsFacetsNav.js.map +1 -1
  53. package/lib/esm/features/facets/RunsFacetsNav.js +8 -1
  54. package/lib/esm/features/facets/RunsFacetsNav.js.map +1 -1
  55. package/lib/esm/features/facets/WorkflowExecutionsFacetsNav.js +1 -1
  56. package/lib/esm/features/facets/WorkflowExecutionsFacetsNav.js.map +1 -1
  57. package/lib/esm/features/facets/index.js +1 -0
  58. package/lib/esm/features/facets/index.js.map +1 -1
  59. package/lib/esm/features/facets/utils/VInteractionFacet.js +5 -5
  60. package/lib/esm/features/facets/utils/VInteractionFacet.js.map +1 -1
  61. package/lib/esm/features/index.js +1 -0
  62. package/lib/esm/features/index.js.map +1 -1
  63. package/lib/esm/features/oauth/OAuthProviderConnectButton.js +85 -0
  64. package/lib/esm/features/oauth/OAuthProviderConnectButton.js.map +1 -0
  65. package/lib/esm/features/oauth/RemoteMcpConnectionButton.js +119 -0
  66. package/lib/esm/features/oauth/RemoteMcpConnectionButton.js.map +1 -0
  67. package/lib/esm/features/oauth/index.js +4 -0
  68. package/lib/esm/features/oauth/index.js.map +1 -0
  69. package/lib/esm/features/oauth/useOAuthPopup.js +89 -0
  70. package/lib/esm/features/oauth/useOAuthPopup.js.map +1 -0
  71. package/lib/esm/features/store/collections/BrowseCollectionView.js.map +1 -1
  72. package/lib/esm/features/store/collections/EditCollectionView.js +9 -2
  73. package/lib/esm/features/store/collections/EditCollectionView.js.map +1 -1
  74. package/lib/esm/features/store/objects/components/ContentOverview.js +19 -7
  75. package/lib/esm/features/store/objects/components/ContentOverview.js.map +1 -1
  76. package/lib/esm/features/store/objects/components/useContentPanelHooks.js +35 -15
  77. package/lib/esm/features/store/objects/components/useContentPanelHooks.js.map +1 -1
  78. package/lib/esm/features/store/objects/selection/ObjectsActionContext.js +3 -3
  79. package/lib/esm/features/store/objects/selection/ObjectsActionContext.js.map +1 -1
  80. package/lib/esm/features/store/objects/selection/SelectionActions.js +4 -3
  81. package/lib/esm/features/store/objects/selection/SelectionActions.js.map +1 -1
  82. package/lib/esm/features/store/objects/selection/actions/ExportPropertiesAction.js +11 -3
  83. package/lib/esm/features/store/objects/selection/actions/ExportPropertiesAction.js.map +1 -1
  84. package/lib/esm/features/store/objects/upload/DocumentUploadModal.js +1 -5
  85. package/lib/esm/features/store/objects/upload/DocumentUploadModal.js.map +1 -1
  86. package/lib/esm/features/user/UserInfo.js +33 -10
  87. package/lib/esm/features/user/UserInfo.js.map +1 -1
  88. package/lib/esm/i18n/locales/ar.json +81 -98
  89. package/lib/esm/i18n/locales/de.json +44 -73
  90. package/lib/esm/i18n/locales/en.json +31 -61
  91. package/lib/esm/i18n/locales/es.json +55 -79
  92. package/lib/esm/i18n/locales/fr.json +55 -81
  93. package/lib/esm/i18n/locales/it.json +55 -79
  94. package/lib/esm/i18n/locales/ja.json +46 -75
  95. package/lib/esm/i18n/locales/ko.json +44 -73
  96. package/lib/esm/i18n/locales/pt.json +55 -79
  97. package/lib/esm/i18n/locales/ru.json +58 -81
  98. package/lib/esm/i18n/locales/tr.json +46 -75
  99. package/lib/esm/i18n/locales/zh-TW.json +46 -75
  100. package/lib/esm/i18n/locales/zh.json +46 -75
  101. package/lib/esm/session/UserSession.js +2 -4
  102. package/lib/esm/session/UserSession.js.map +1 -1
  103. package/lib/esm/session/UserSessionProvider.js +22 -17
  104. package/lib/esm/session/UserSessionProvider.js.map +1 -1
  105. package/lib/esm/session/auth/composable.js +20 -2
  106. package/lib/esm/session/auth/composable.js.map +1 -1
  107. package/lib/esm/session/auth/domainRouting.js +7 -0
  108. package/lib/esm/session/auth/domainRouting.js.map +1 -0
  109. package/lib/esm/shell/login/InviteAcceptModal.js +1 -0
  110. package/lib/esm/shell/login/InviteAcceptModal.js.map +1 -1
  111. package/lib/esm/widgets/form/Form.js +2 -2
  112. package/lib/esm/widgets/form/Form.js.map +1 -1
  113. package/lib/esm/widgets/markdown/MarkdownRenderer.js +2 -1
  114. package/lib/esm/widgets/markdown/MarkdownRenderer.js.map +1 -1
  115. package/lib/esm/widgets/markdown/preprocessMathDelimiters.js +226 -0
  116. package/lib/esm/widgets/markdown/preprocessMathDelimiters.js.map +1 -0
  117. package/lib/esm/widgets/monacoEditor/MonacoEditor.js +40 -5
  118. package/lib/esm/widgets/monacoEditor/MonacoEditor.js.map +1 -1
  119. package/lib/esm/widgets/monacoEditor/foldingProviders.js +132 -0
  120. package/lib/esm/widgets/monacoEditor/foldingProviders.js.map +1 -0
  121. package/lib/tsconfig.tsbuildinfo +1 -1
  122. package/lib/types/core/components/ComboBox.d.ts +12 -2
  123. package/lib/types/core/components/ComboBox.d.ts.map +1 -1
  124. package/lib/types/core/components/FormItem.d.ts +5 -2
  125. package/lib/types/core/components/FormItem.d.ts.map +1 -1
  126. package/lib/types/core/components/SidePanel.d.ts.map +1 -1
  127. package/lib/types/core/components/shadcn/badge.d.ts +2 -2
  128. package/lib/types/core/components/shadcn/collaspible.d.ts +3 -3
  129. package/lib/types/core/components/shadcn/collaspible.d.ts.map +1 -1
  130. package/lib/types/core/components/shadcn/dropdown.d.ts +11 -3
  131. package/lib/types/core/components/shadcn/dropdown.d.ts.map +1 -1
  132. package/lib/types/core/components/shadcn/filters/filterBar.d.ts +2 -1
  133. package/lib/types/core/components/shadcn/filters/filterBar.d.ts.map +1 -1
  134. package/lib/types/core/components/shadcn/input.d.ts +1 -1
  135. package/lib/types/core/components/shadcn/radioGroup.d.ts.map +1 -1
  136. package/lib/types/core/components/shadcn/switch.d.ts.map +1 -1
  137. package/lib/types/core/components/shadcn/tabs.d.ts +4 -2
  138. package/lib/types/core/components/shadcn/tabs.d.ts.map +1 -1
  139. package/lib/types/core/components/shadcn/text.d.ts +1 -1
  140. package/lib/types/core/components/shadcn/tooltip.d.ts +1 -1
  141. package/lib/types/core/components/shadcn/tooltip.d.ts.map +1 -1
  142. package/lib/types/core/hooks/PortalContainerProvider.d.ts +1 -0
  143. package/lib/types/core/hooks/PortalContainerProvider.d.ts.map +1 -1
  144. package/lib/types/env/index.d.ts +2 -2
  145. package/lib/types/env/index.d.ts.map +1 -1
  146. package/lib/types/features/agent/chat/AgentChart.d.ts +1 -1
  147. package/lib/types/features/agent/chat/AgentChart.d.ts.map +1 -1
  148. package/lib/types/features/agent/chat/AgentRightPanel.d.ts +7 -2
  149. package/lib/types/features/agent/chat/AgentRightPanel.d.ts.map +1 -1
  150. package/lib/types/features/agent/chat/DocumentPanel.d.ts +2 -2
  151. package/lib/types/features/agent/chat/DocumentPanel.d.ts.map +1 -1
  152. package/lib/types/features/agent/chat/DocumentTabBar.d.ts +1 -2
  153. package/lib/types/features/agent/chat/DocumentTabBar.d.ts.map +1 -1
  154. package/lib/types/features/agent/chat/ModernAgentConversation.d.ts +5 -3
  155. package/lib/types/features/agent/chat/ModernAgentConversation.d.ts.map +1 -1
  156. package/lib/types/features/agent/chat/ModernAgentOutput/AllMessagesMixed.d.ts +4 -2
  157. package/lib/types/features/agent/chat/ModernAgentOutput/AllMessagesMixed.d.ts.map +1 -1
  158. package/lib/types/features/agent/chat/ModernAgentOutput/Header.d.ts +4 -4
  159. package/lib/types/features/agent/chat/ModernAgentOutput/Header.d.ts.map +1 -1
  160. package/lib/types/features/agent/chat/ModernAgentOutput/MessageItem.d.ts.map +1 -1
  161. package/lib/types/features/agent/chat/VegaLiteChart.d.ts +1 -1
  162. package/lib/types/features/agent/chat/VegaLiteChart.d.ts.map +1 -1
  163. package/lib/types/features/agent/chat/hooks/useAgentStream.d.ts +4 -2
  164. package/lib/types/features/agent/chat/hooks/useAgentStream.d.ts.map +1 -1
  165. package/lib/types/features/agent/chat/hooks/useDocumentPanel.d.ts +1 -0
  166. package/lib/types/features/agent/chat/hooks/useDocumentPanel.d.ts.map +1 -1
  167. package/lib/types/features/facets/DocumentsFacetsNav.d.ts.map +1 -1
  168. package/lib/types/features/facets/RunsFacetsNav.d.ts.map +1 -1
  169. package/lib/types/features/facets/index.d.ts +1 -0
  170. package/lib/types/features/facets/index.d.ts.map +1 -1
  171. package/lib/types/features/index.d.ts +1 -0
  172. package/lib/types/features/index.d.ts.map +1 -1
  173. package/lib/types/features/oauth/OAuthProviderConnectButton.d.ts +11 -0
  174. package/lib/types/features/oauth/OAuthProviderConnectButton.d.ts.map +1 -0
  175. package/lib/types/features/oauth/RemoteMcpConnectionButton.d.ts +25 -0
  176. package/lib/types/features/oauth/RemoteMcpConnectionButton.d.ts.map +1 -0
  177. package/lib/types/features/oauth/index.d.ts +4 -0
  178. package/lib/types/features/oauth/index.d.ts.map +1 -0
  179. package/lib/types/features/oauth/useOAuthPopup.d.ts +12 -0
  180. package/lib/types/features/oauth/useOAuthPopup.d.ts.map +1 -0
  181. package/lib/types/features/store/collections/BrowseCollectionView.d.ts.map +1 -1
  182. package/lib/types/features/store/collections/EditCollectionView.d.ts.map +1 -1
  183. package/lib/types/features/store/objects/components/useContentPanelHooks.d.ts.map +1 -1
  184. package/lib/types/features/store/objects/selection/ObjectsActionContext.d.ts +3 -3
  185. package/lib/types/features/store/objects/selection/ObjectsActionContext.d.ts.map +1 -1
  186. package/lib/types/features/store/objects/selection/ObjectsActionSpec.d.ts +2 -1
  187. package/lib/types/features/store/objects/selection/ObjectsActionSpec.d.ts.map +1 -1
  188. package/lib/types/features/store/objects/selection/SelectionActions.d.ts +3 -3
  189. package/lib/types/features/store/objects/selection/SelectionActions.d.ts.map +1 -1
  190. package/lib/types/features/store/objects/selection/actions/ExportPropertiesAction.d.ts.map +1 -1
  191. package/lib/types/features/user/UserInfo.d.ts +2 -1
  192. package/lib/types/features/user/UserInfo.d.ts.map +1 -1
  193. package/lib/types/session/UserSession.d.ts.map +1 -1
  194. package/lib/types/session/UserSessionProvider.d.ts +0 -1
  195. package/lib/types/session/UserSessionProvider.d.ts.map +1 -1
  196. package/lib/types/session/auth/composable.d.ts +4 -0
  197. package/lib/types/session/auth/composable.d.ts.map +1 -1
  198. package/lib/types/session/auth/domainRouting.d.ts +8 -0
  199. package/lib/types/session/auth/domainRouting.d.ts.map +1 -0
  200. package/lib/types/shell/login/InviteAcceptModal.d.ts.map +1 -1
  201. package/lib/types/widgets/markdown/MarkdownRenderer.d.ts.map +1 -1
  202. package/lib/types/widgets/markdown/preprocessMathDelimiters.d.ts +24 -0
  203. package/lib/types/widgets/markdown/preprocessMathDelimiters.d.ts.map +1 -0
  204. package/lib/types/widgets/monacoEditor/MonacoEditor.d.ts +2 -1
  205. package/lib/types/widgets/monacoEditor/MonacoEditor.d.ts.map +1 -1
  206. package/lib/types/widgets/monacoEditor/foldingProviders.d.ts +2 -0
  207. package/lib/types/widgets/monacoEditor/foldingProviders.d.ts.map +1 -0
  208. package/lib/vertesia-ui-core.js +1 -1
  209. package/lib/vertesia-ui-core.js.map +1 -1
  210. package/lib/vertesia-ui-env.js +1 -1
  211. package/lib/vertesia-ui-env.js.map +1 -1
  212. package/lib/vertesia-ui-features.js +1 -1
  213. package/lib/vertesia-ui-features.js.map +1 -1
  214. package/lib/vertesia-ui-i18n.js +1 -1
  215. package/lib/vertesia-ui-i18n.js.map +1 -1
  216. package/lib/vertesia-ui-layout.js +1 -1
  217. package/lib/vertesia-ui-layout.js.map +1 -1
  218. package/lib/vertesia-ui-session.js +1 -1
  219. package/lib/vertesia-ui-session.js.map +1 -1
  220. package/lib/vertesia-ui-shell.js +1 -1
  221. package/lib/vertesia-ui-shell.js.map +1 -1
  222. package/lib/vertesia-ui-widgets.js +1 -1
  223. package/lib/vertesia-ui-widgets.js.map +1 -1
  224. package/package.json +15 -15
  225. package/src/core/components/ComboBox.tsx +66 -29
  226. package/src/core/components/FormItem.tsx +9 -6
  227. package/src/core/components/SidePanel.tsx +5 -3
  228. package/src/core/components/shadcn/collaspible.tsx +5 -7
  229. package/src/core/components/shadcn/dropdown.tsx +68 -13
  230. package/src/core/components/shadcn/filters/comboBox/DateCombobox.tsx +6 -6
  231. package/src/core/components/shadcn/filters/filterBar.tsx +5 -3
  232. package/src/core/components/shadcn/radioGroup.tsx +1 -3
  233. package/src/core/components/shadcn/switch.tsx +0 -2
  234. package/src/core/components/shadcn/tabs.tsx +15 -2
  235. package/src/core/components/shadcn/tooltip.tsx +20 -3
  236. package/src/core/hooks/PortalContainerProvider.tsx +11 -3
  237. package/src/env/index.ts +7 -10
  238. package/src/features/agent/chat/AgentRightPanel.tsx +43 -23
  239. package/src/features/agent/chat/DocumentPanel.tsx +21 -19
  240. package/src/features/agent/chat/DocumentTabBar.tsx +21 -32
  241. package/src/features/agent/chat/ModernAgentConversation.tsx +72 -27
  242. package/src/features/agent/chat/ModernAgentOutput/AllMessagesMixed.tsx +21 -9
  243. package/src/features/agent/chat/ModernAgentOutput/Header.tsx +136 -115
  244. package/src/features/agent/chat/ModernAgentOutput/MessageItem.tsx +0 -3
  245. package/src/features/agent/chat/hooks/useAgentStream.ts +13 -7
  246. package/src/features/agent/chat/hooks/useDocumentPanel.ts +8 -0
  247. package/src/features/facets/AgentRunnerFacetsNav.tsx +1 -1
  248. package/src/features/facets/DocumentsFacetsNav.tsx +3 -1
  249. package/src/features/facets/RunsFacetsNav.tsx +9 -1
  250. package/src/features/facets/WorkflowExecutionsFacetsNav.tsx +1 -1
  251. package/src/features/facets/index.ts +1 -0
  252. package/src/features/facets/utils/VInteractionFacet.tsx +12 -12
  253. package/src/features/index.ts +1 -0
  254. package/src/features/oauth/OAuthProviderConnectButton.tsx +125 -0
  255. package/src/features/oauth/RemoteMcpConnectionButton.tsx +274 -0
  256. package/src/features/oauth/index.ts +3 -0
  257. package/src/features/oauth/useOAuthPopup.ts +125 -0
  258. package/src/features/store/collections/BrowseCollectionView.tsx +3 -3
  259. package/src/features/store/collections/EditCollectionView.tsx +10 -1
  260. package/src/features/store/objects/components/ContentOverview.tsx +108 -87
  261. package/src/features/store/objects/components/useContentPanelHooks.ts +50 -15
  262. package/src/features/store/objects/selection/ObjectsActionContext.tsx +5 -5
  263. package/src/features/store/objects/selection/ObjectsActionSpec.ts +2 -1
  264. package/src/features/store/objects/selection/SelectionActions.tsx +6 -5
  265. package/src/features/store/objects/selection/actions/ExportPropertiesAction.tsx +12 -3
  266. package/src/features/user/UserInfo.tsx +82 -10
  267. package/src/i18n/locales/ar.json +81 -98
  268. package/src/i18n/locales/de.json +44 -73
  269. package/src/i18n/locales/en.json +31 -61
  270. package/src/i18n/locales/es.json +55 -79
  271. package/src/i18n/locales/fr.json +55 -81
  272. package/src/i18n/locales/it.json +55 -79
  273. package/src/i18n/locales/ja.json +46 -75
  274. package/src/i18n/locales/ko.json +44 -73
  275. package/src/i18n/locales/pt.json +55 -79
  276. package/src/i18n/locales/ru.json +58 -81
  277. package/src/i18n/locales/tr.json +46 -75
  278. package/src/i18n/locales/zh-TW.json +46 -75
  279. package/src/i18n/locales/zh.json +46 -75
  280. package/src/session/UserSession.ts +2 -5
  281. package/src/session/UserSessionProvider.tsx +23 -18
  282. package/src/session/auth/auth-flow.md +1 -1
  283. package/src/session/auth/composable.ts +21 -2
  284. package/src/session/auth/domainRouting.test.ts +26 -0
  285. package/src/session/auth/domainRouting.ts +13 -0
  286. package/src/shell/login/InviteAcceptModal.tsx +1 -0
  287. package/src/widgets/form/Form.tsx +2 -2
  288. package/src/widgets/markdown/MarkdownRenderer.tsx +2 -1
  289. package/src/widgets/markdown/markdown.css +12 -0
  290. package/src/widgets/markdown/preprocessMathDelimiters.test.ts +87 -0
  291. package/src/widgets/markdown/preprocessMathDelimiters.ts +229 -0
  292. package/src/widgets/monacoEditor/MonacoEditor.tsx +47 -4
  293. package/src/widgets/monacoEditor/foldingProviders.ts +122 -0
@@ -0,0 +1,87 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { preprocessMathDelimiters } from "./preprocessMathDelimiters";
3
+
4
+ describe("preprocessMathDelimiters", () => {
5
+ it("preserves LaTeX patterns (commands, subscripts, superscripts, braces)", () => {
6
+ expect(preprocessMathDelimiters("$x = \\frac{-b}{2a}$")).toBe("$x = \\frac{-b}{2a}$");
7
+ expect(preprocessMathDelimiters("$n^2$")).toBe("$n^2$");
8
+ expect(preprocessMathDelimiters("$a_{ij}$")).toBe("$a_{ij}$");
9
+ });
10
+
11
+ it("preserves single-letter variables and variable assignments", () => {
12
+ expect(preprocessMathDelimiters("rate $r$ can be expressed")).toBe("rate $r$ can be expressed");
13
+ expect(preprocessMathDelimiters("where $r = 0.235$ (growth)")).toBe("where $r = 0.235$ (growth)");
14
+ });
15
+
16
+ it("preserves ion notation (^+ and ^-)", () => {
17
+ expect(preprocessMathDelimiters("$Ca^+$")).toBe("$Ca^+$");
18
+ expect(preprocessMathDelimiters("$Cl^-$")).toBe("$Cl^-$");
19
+ expect(preprocessMathDelimiters("$Ca^{2+}$")).toBe("$Ca^{2+}$");
20
+ });
21
+
22
+ it("preserves display math ($$...$$)", () => {
23
+ expect(preprocessMathDelimiters("$$E = mc^2$$")).toBe("$$E = mc^2$$");
24
+ expect(preprocessMathDelimiters("$$\nx = \\frac{-b}{2a}\n$$")).toBe("$$\nx = \\frac{-b}{2a}\n$$");
25
+ });
26
+
27
+ it("escapes currency amounts", () => {
28
+ expect(preprocessMathDelimiters("between $100M and $500M")).toBe("between \\$100M and \\$500M");
29
+ expect(preprocessMathDelimiters("all $ figures by $500k")).toBe("all \\$ figures by \\$500k");
30
+ expect(preprocessMathDelimiters("$100K-$500K range")).toBe("\\$100K-\\$500K range");
31
+ });
32
+
33
+ it("preserves uncertain content as fallback", () => {
34
+ expect(preprocessMathDelimiters("$100 + 200$")).toBe("$100 + 200$");
35
+ });
36
+
37
+ it("skips inline code and fenced code blocks", () => {
38
+ expect(preprocessMathDelimiters("use `$100 and $200` as values")).toBe("use `$100 and $200` as values");
39
+ const fenced = "costs $100M and $500M\n```\nprice = $200\n```";
40
+ expect(preprocessMathDelimiters(fenced)).toBe("costs \\$100M and \\$500M\n```\nprice = $200\n```");
41
+ });
42
+
43
+ it("no-ops on empty string and strings without $", () => {
44
+ expect(preprocessMathDelimiters("")).toBe("");
45
+ expect(preprocessMathDelimiters("no dollars here")).toBe("no dollars here");
46
+ });
47
+
48
+ it("does not double-escape already escaped \\$", () => {
49
+ expect(preprocessMathDelimiters("costs \\$100")).toBe("costs \\$100");
50
+ });
51
+
52
+ it("replaces \\$ inside LaTeX spans with \\text{\\textdollar}", () => {
53
+ expect(preprocessMathDelimiters("where $P = \\$2,847,500$ end"))
54
+ .toBe("where $P = \\text{\\textdollar}2,847,500$ end");
55
+ });
56
+
57
+ it("does not replace \\$ outside LaTeX spans", () => {
58
+ expect(preprocessMathDelimiters("costs \\$100")).toBe("costs \\$100");
59
+ });
60
+
61
+ it("escapes currency $ adjacent to LaTeX pairs", () => {
62
+ expect(preprocessMathDelimiters("costs $500M. Also $x = \\frac{1}{2}$ works"))
63
+ .toBe("costs \\$500M. Also $x = \\frac{1}{2}$ works");
64
+ });
65
+
66
+ it("handles currency and LaTeX on separate lines", () => {
67
+ expect(preprocessMathDelimiters("between $100M and $500M\nwhere $x = \\frac{1}{2}$"))
68
+ .toBe("between \\$100M and \\$500M\nwhere $x = \\frac{1}{2}$");
69
+ });
70
+
71
+ it("handles mixed currency, LaTeX, and \\$ in a financial report line", () => {
72
+ const input = "Where $F = \\$567,800$ (fixed costs), $P = \\$89.99$ (price), and $V = \\$0.42$ (variable). This yields $Q_{\\text{BE}} = 6,338$ units.";
73
+ const result = preprocessMathDelimiters(input);
74
+ expect(result).toContain("$F = \\text{\\textdollar}567,800$");
75
+ expect(result).toContain("$P = \\text{\\textdollar}89.99$");
76
+ expect(result).toContain("$V = \\text{\\textdollar}0.42$");
77
+ expect(result).toContain("$Q_{\\text{BE}} = 6,338$");
78
+ });
79
+
80
+ it("handles interleaved currency, inline LaTeX, and display math", () => {
81
+ const input = "The report for $500M shows that (given $sales = x*e^{y}$) we were off by $15M. This is from $$variance = x*v/2*e^(y-y`)$$";
82
+ const result = preprocessMathDelimiters(input);
83
+ expect(result).toContain("$sales = x*e^{y}$");
84
+ expect(result).toContain("$$variance = x*v/2*e^(y-y`)$$");
85
+ expect(result).toContain("\\$500M");
86
+ });
87
+ });
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Markdown math delimiter preprocessor.
3
+ *
4
+ * Disambiguates `$` characters in markdown so that remark-math correctly
5
+ * distinguishes LaTeX math (`$x = \frac{1}{2}$`) from currency (`$2,847,500`).
6
+ *
7
+ * Uses a three-pass priority algorithm over `$` positions:
8
+ * 1. Commit pairs matching LaTeX patterns (highest priority)
9
+ * 2. Pair remaining positions; escape currency patterns
10
+ * 3. Escape lone `$` adjacent to committed LaTeX pairs
11
+ *
12
+ * Also normalizes `\$` inside LaTeX spans into a KaTeX-compatible form,
13
+ * since remark-math does not treat `\$` as escaped within math delimiters.
14
+ *
15
+ * No-ops on input without `$`. Skips fenced code blocks and inline code spans.
16
+ */
17
+
18
+ const FENCED_CODE_BLOCK_REGEX = /(^|\n)(`{3,}|~{3,})[^\n]*\n[\s\S]*?\n\2(?=\n|$)/g; // ```...``` or ~~~...~~~
19
+ const INLINE_CODE_REGEX = /`[^`\n]*`/g; // `...`
20
+ const ESCAPED_DOLLAR_REGEX = /\\\$/g; // \$ inside LaTeX spans
21
+
22
+ // LaTeX positive signals
23
+ const RE_BACKSLASH_CMD = /\\[^\s]/; // \command or \symbol (\frac, \%, \$, etc.)
24
+ const RE_BRACE_GROUP = /[{}]/; // brace groups
25
+ const RE_SUB_SUPERSCRIPT = /[_^][\w{]/; // sub/superscript
26
+ const RE_SINGLE_LETTER = /^[a-zA-Z]$/; // single letter variable ($r$, $x$, $t$)
27
+ const RE_VAR_ASSIGNMENT = /^[a-zA-Z]\s*=/; // variable assignment ($r = 0.235$, $n = 4$)
28
+
29
+ // LaTeX negative signals
30
+ const RE_LEADING_SPACE = /^\s/; // space after opening $
31
+ const RE_TRAILING_SPACE = /\s$/; // space before closing $
32
+ const RE_TRAILING_OPERATOR = /[+*/-]$/; // ends with bare operator
33
+ const RE_ION_NOTATION = /\^[+-]$/; // except ^+ or ^- (ion notation)
34
+
35
+ /**
36
+ * Returns true if content between `$...$` contains LaTeX structural patterns.
37
+ */
38
+ function hasLatexPattern(content: string): boolean {
39
+ return RE_BACKSLASH_CMD.test(content)
40
+ || RE_BRACE_GROUP.test(content)
41
+ || RE_SUB_SUPERSCRIPT.test(content)
42
+ || RE_SINGLE_LETTER.test(content)
43
+ || RE_VAR_ASSIGNMENT.test(content);
44
+ }
45
+
46
+ /**
47
+ * Returns true if content between `$...$` has structural tells of currency dollar signs.
48
+ */
49
+ function hasCurrencyPattern(content: string): boolean {
50
+ if (RE_LEADING_SPACE.test(content)) return true;
51
+ if (RE_TRAILING_SPACE.test(content)) return true;
52
+ if (RE_TRAILING_OPERATOR.test(content) && !RE_ION_NOTATION.test(content)) return true;
53
+ return false;
54
+ }
55
+
56
+ /**
57
+ * Find all single-$ positions in text (skipping \$ and $$).
58
+ */
59
+ function findSingleDollarPositions(text: string): number[] {
60
+ const positions: number[] = [];
61
+ for (let i = 0; i < text.length; i++) {
62
+ if (text[i] !== "$") continue;
63
+ if (i > 0 && text[i - 1] === "\\") continue; // skip \$
64
+ if (i + 1 < text.length && text[i + 1] === "$") { i++; continue; } // skip $$ (first)
65
+ if (i > 0 && text[i - 1] === "$" && (i < 2 || text[i - 2] !== "\\")) continue; // skip $$ (second)
66
+ positions.push(i);
67
+ }
68
+ return positions;
69
+ }
70
+
71
+ /**
72
+ * Extract content between two $ positions. Returns null if the span is
73
+ * empty or crosses a line boundary (invalid for inline math).
74
+ */
75
+ function inlineMathContent(text: string, openPos: number, closePos: number): string | null {
76
+ const content = text.slice(openPos + 1, closePos);
77
+ if (content.length === 0 || content.includes("\n")) return null;
78
+ return content;
79
+ }
80
+
81
+ /**
82
+ * Classify `$` positions in a text segment, escape currency dollar signs,
83
+ * and normalize `\$` inside LaTeX spans for remark-math compatibility.
84
+ */
85
+ function processTextSegment(text: string): string {
86
+ const positions = findSingleDollarPositions(text);
87
+ if (positions.length < 2) return text;
88
+
89
+ const committed = new Set<number>(); // position indices that are paired
90
+ const toEscape = new Set<number>(); // character offsets in text to escape
91
+
92
+ // Pre-compute content for each adjacent pair (avoids redundant slicing in Pass 1 & 2)
93
+ const adjacentContent: (string | null)[] = new Array(positions.length - 1);
94
+ for (let i = 0; i < positions.length - 1; i++) {
95
+ adjacentContent[i] = inlineMathContent(text, positions[i], positions[i + 1]);
96
+ }
97
+
98
+ // Pass 1: commit definitely-LaTeX adjacent pairs and record their char boundaries
99
+ const latexSpans: [number, number][] = [];
100
+ for (let i = 0; i < positions.length - 1; i++) {
101
+ if (committed.has(i)) continue;
102
+ const content = adjacentContent[i];
103
+ if (content !== null && hasLatexPattern(content)) {
104
+ committed.add(i);
105
+ committed.add(i + 1);
106
+ latexSpans.push([positions[i], positions[i + 1]]);
107
+ }
108
+ }
109
+
110
+ // Pass 2: pair remaining positions left-to-right
111
+ let idx = 0;
112
+ while (idx < positions.length) {
113
+ if (committed.has(idx)) { idx++; continue; }
114
+
115
+ // Find next uncommitted position
116
+ let next = idx + 1;
117
+ while (next < positions.length && committed.has(next)) next++;
118
+ if (next >= positions.length) break;
119
+
120
+ // If committed positions sit between idx and next, remark-math would
121
+ // pair positions[idx] with the first committed $, breaking LaTeX.
122
+ if (next > idx + 1) {
123
+ toEscape.add(positions[idx]);
124
+ idx++;
125
+ continue;
126
+ }
127
+
128
+ // next === idx + 1, so we can reuse the pre-computed content
129
+ const content = adjacentContent[idx];
130
+ if (content === null) {
131
+ // Cross-line pair: remark-math processes per-paragraph, so these
132
+ // can't actually pair. Don't consume either position.
133
+ idx++;
134
+ continue;
135
+ }
136
+ if (hasCurrencyPattern(content)) {
137
+ toEscape.add(positions[idx]);
138
+ toEscape.add(positions[next]);
139
+ }
140
+ committed.add(idx);
141
+ committed.add(next);
142
+ idx = next + 1;
143
+ }
144
+
145
+ // Pass 3: escape lone $ adjacent to committed LaTeX pairs
146
+ for (let i = 0; i < positions.length; i++) {
147
+ if (committed.has(i)) continue;
148
+ if (i + 1 < positions.length && committed.has(i + 1)) {
149
+ toEscape.add(positions[i]);
150
+ }
151
+ }
152
+
153
+ if (toEscape.size === 0 && latexSpans.length === 0) return text;
154
+
155
+ // Build result: escape false-positive $ and replace \$ inside LaTeX spans
156
+ const parts: string[] = [];
157
+ let segStart = 0;
158
+ const escapePositions = Array.from(toEscape).sort((a, b) => a - b);
159
+ let escIdx = 0;
160
+ let spanIdx = 0;
161
+
162
+ while (escIdx < escapePositions.length || spanIdx < latexSpans.length) {
163
+ const escPos = escIdx < escapePositions.length ? escapePositions[escIdx] : Infinity;
164
+ const spanOpen = spanIdx < latexSpans.length ? latexSpans[spanIdx][0] : Infinity;
165
+
166
+ if (escPos < spanOpen) {
167
+ if (escPos > segStart) parts.push(text.slice(segStart, escPos));
168
+ parts.push("\\$");
169
+ segStart = escPos + 1;
170
+ escIdx++;
171
+ } else {
172
+ const [open, close] = latexSpans[spanIdx];
173
+ if (open > segStart) parts.push(text.slice(segStart, open));
174
+ const spanContent = text.slice(open, close + 1);
175
+ parts.push(spanContent.replace(ESCAPED_DOLLAR_REGEX, "\\text{\\textdollar}"));
176
+ segStart = close + 1;
177
+ while (escIdx < escapePositions.length && escapePositions[escIdx] <= close) escIdx++;
178
+ spanIdx++;
179
+ }
180
+ }
181
+
182
+ if (segStart < text.length) parts.push(text.slice(segStart));
183
+ return parts.join("");
184
+ }
185
+
186
+ /**
187
+ * Process text segments outside inline code spans.
188
+ */
189
+ function processSkippingInlineCode(text: string): string {
190
+ const parts: string[] = [];
191
+ let lastIndex = 0;
192
+ let match: RegExpExecArray | null;
193
+
194
+ INLINE_CODE_REGEX.lastIndex = 0;
195
+ while ((match = INLINE_CODE_REGEX.exec(text)) !== null) {
196
+ parts.push(processTextSegment(text.slice(lastIndex, match.index)));
197
+ parts.push(match[0]);
198
+ lastIndex = match.index + match[0].length;
199
+ }
200
+
201
+ parts.push(processTextSegment(text.slice(lastIndex)));
202
+ return parts.join("");
203
+ }
204
+
205
+ /**
206
+ * Preprocess markdown to disambiguate `$` math delimiters from currency signs.
207
+ *
208
+ * Classification priority: LaTeX (preserve) > currency (escape) > uncertain (preserve).
209
+ * Skips fenced code blocks and inline code spans.
210
+ */
211
+ export function preprocessMathDelimiters(markdown: string): string {
212
+ if (!markdown || !markdown.includes("$")) {
213
+ return markdown;
214
+ }
215
+
216
+ const parts: string[] = [];
217
+ let lastIndex = 0;
218
+ let match: RegExpExecArray | null;
219
+
220
+ FENCED_CODE_BLOCK_REGEX.lastIndex = 0;
221
+ while ((match = FENCED_CODE_BLOCK_REGEX.exec(markdown)) !== null) {
222
+ parts.push(processSkippingInlineCode(markdown.slice(lastIndex, match.index)));
223
+ parts.push(match[0]);
224
+ lastIndex = match.index + match[0].length;
225
+ }
226
+
227
+ parts.push(processSkippingInlineCode(markdown.slice(lastIndex)));
228
+ return parts.join("");
229
+ }
@@ -4,6 +4,7 @@ import debounce from 'debounce';
4
4
  import clsx from 'clsx';
5
5
  import { RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react';
6
6
  import type * as monaco from 'monaco-editor';
7
+ import { registerCustomFoldingProviders } from './foldingProviders.js';
7
8
 
8
9
  export type Monaco = typeof monaco;
9
10
 
@@ -47,6 +48,7 @@ interface MonacoEditorProps {
47
48
  beforeMount?: (monaco: typeof import('monaco-editor')) => void;
48
49
  onMount?: (editor: monaco.editor.IStandaloneCodeEditor, monaco: typeof import('monaco-editor')) => void;
49
50
  defaultValue?: string;
51
+ useCustomFolding?: boolean;
50
52
  }
51
53
 
52
54
  export function MonacoEditor({
@@ -60,10 +62,15 @@ export function MonacoEditor({
60
62
  beforeMount,
61
63
  onMount,
62
64
  defaultValue,
65
+ useCustomFolding = false,
63
66
  }: MonacoEditorProps) {
64
67
  const [editorValue, setEditorValue] = useState(value || defaultValue || '');
65
68
  const editorInstanceRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
69
+ const monacoInstanceRef = useRef<typeof import('monaco-editor') | null>(null);
66
70
  const { theme } = useTheme();
71
+ const resolvedTheme = theme === 'system'
72
+ ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
73
+ : theme;
67
74
 
68
75
  const getValueRef = useRef(() => editorValue);
69
76
  const setValueRef = useRef((newValue: string) => {
@@ -101,6 +108,22 @@ export function MonacoEditor({
101
108
  }
102
109
  }, [onChange, debounceTimeout]);
103
110
 
111
+ const foldAllCodeBlocks = useCallback(async (
112
+ editor: monaco.editor.IStandaloneCodeEditor,
113
+ monacoInstance: typeof import('monaco-editor'),
114
+ ) => {
115
+ const model = editor.getModel();
116
+ if (!model) return;
117
+ const codeBlockRegExp = /```[\s\S]*?```/g;
118
+ let match;
119
+ while ((match = codeBlockRegExp.exec(model.getValue())) !== null) {
120
+ const startLine = model.getPositionAt(match.index).lineNumber;
121
+ const endLine = model.getPositionAt(match.index + match[0].length).lineNumber;
122
+ editor.setSelection(new monacoInstance.Selection(startLine, 1, endLine, 1));
123
+ await editor.getAction('editor.createFoldingRangeFromSelection')?.run();
124
+ }
125
+ }, []);
126
+
104
127
  const handleEditorChange = useCallback((newValue: string | undefined) => {
105
128
  const actualValue = newValue || '';
106
129
  setEditorValue(actualValue);
@@ -123,6 +146,12 @@ export function MonacoEditor({
123
146
 
124
147
  const handleEditorDidMount = useCallback((editor: monaco.editor.IStandaloneCodeEditor, monacoInstance: typeof import('monaco-editor')) => {
125
148
  editorInstanceRef.current = editor;
149
+ monacoInstanceRef.current = monacoInstance;
150
+
151
+ if (useCustomFolding) {
152
+ registerCustomFoldingProviders(monacoInstance);
153
+ }
154
+
126
155
  // Update the setValue ref to use the actual editor instance
127
156
  setValueRef.current = (newValue: string) => {
128
157
  setEditorValue(newValue);
@@ -131,7 +160,7 @@ export function MonacoEditor({
131
160
 
132
161
  // Set up custom theme for better error line highlighting
133
162
  monacoInstance.editor.defineTheme('errorLineTheme', {
134
- base: theme === 'dark' ? 'vs-dark' : 'vs',
163
+ base: resolvedTheme === 'dark' ? 'vs-dark' : 'vs',
135
164
  inherit: true,
136
165
  rules: [],
137
166
  colors: {
@@ -142,9 +171,13 @@ export function MonacoEditor({
142
171
 
143
172
  monacoInstance.editor.setTheme('errorLineTheme');
144
173
 
174
+ if (useCustomFolding) {
175
+ setTimeout(() => foldAllCodeBlocks(editor, monacoInstance), 300);
176
+ }
177
+
145
178
  // Call custom onMount if provided
146
179
  onMount?.(editor, monacoInstance);
147
- }, [onMount, theme]);
180
+ }, [onMount, resolvedTheme, useCustomFolding, foldAllCodeBlocks]);
148
181
 
149
182
  // Update editor value when prop changes from outside
150
183
  useEffect(() => {
@@ -157,6 +190,15 @@ export function MonacoEditor({
157
190
  }
158
191
  }, [value]); // Only depend on value prop, not editorValue
159
192
 
193
+ // Re-fold code blocks when value prop changes externally
194
+ useEffect(() => {
195
+ if (!useCustomFolding || !editorInstanceRef.current || !monacoInstanceRef.current) return;
196
+ const editor = editorInstanceRef.current;
197
+ const monacoInstance = monacoInstanceRef.current;
198
+ const timer = setTimeout(() => foldAllCodeBlocks(editor, monacoInstance), 300);
199
+ return () => clearTimeout(timer);
200
+ }, [value, useCustomFolding, foldAllCodeBlocks]);
201
+
160
202
  const defaultOptions: monaco.editor.IStandaloneEditorConstructionOptions = {
161
203
  fontSize: 14,
162
204
  fontFamily: 'monospace',
@@ -164,7 +206,7 @@ export function MonacoEditor({
164
206
  scrollBeyondLastLine: false,
165
207
  wordWrap: 'on' as const,
166
208
  lineNumbers: 'on' as const,
167
- folding: false,
209
+ folding: true,
168
210
  lineDecorationsWidth: 10,
169
211
  lineNumbersMinChars: 3,
170
212
  automaticLayout: true,
@@ -172,6 +214,7 @@ export function MonacoEditor({
172
214
  formatOnType: true,
173
215
  tabSize: 2,
174
216
  insertSpaces: true,
217
+ fixedOverflowWidgets: true, // Hover/diagnostic popovers float outside the editor bounds
175
218
  glyphMargin: true, // Enable better error reporting
176
219
  renderValidationDecorations: 'on', // Show error squiggles
177
220
  renderLineHighlight: 'line', // Highlight entire line for errors
@@ -192,7 +235,7 @@ export function MonacoEditor({
192
235
  <Editor
193
236
  className="h-full w-full"
194
237
  height="100%"
195
- theme={theme === 'dark' ? 'vs-dark' : 'light'}
238
+ theme={resolvedTheme === 'dark' ? 'vs-dark' : 'light'}
196
239
  language={language}
197
240
  value={editorValue}
198
241
  onChange={handleEditorChange}
@@ -0,0 +1,122 @@
1
+ import type * as monaco from 'monaco-editor';
2
+
3
+ const foldingProvidersRegistered = new Set<string>();
4
+
5
+ export function registerCustomFoldingProviders(monacoInstance: typeof import('monaco-editor')): void {
6
+ // Markdown: fold by heading hierarchy (## sections)
7
+ if (!foldingProvidersRegistered.has('markdown')) {
8
+ foldingProvidersRegistered.add('markdown');
9
+ monacoInstance.languages.registerFoldingRangeProvider('markdown', {
10
+ provideFoldingRanges(model) {
11
+ const ranges: monaco.languages.FoldingRange[] = [];
12
+ const lines = model.getLinesContent();
13
+ const headingPattern = /^(#{1,6})\s/;
14
+ const stack: Array<{ level: number; line: number }> = [];
15
+
16
+ for (let i = 0; i < lines.length; i++) {
17
+ const lineNumber = i + 1;
18
+ const match = headingPattern.exec(lines[i]);
19
+ if (match) {
20
+ const level = match[1].length;
21
+ while (stack.length > 0 && stack[stack.length - 1].level >= level) {
22
+ const top = stack.pop()!;
23
+ if (lineNumber - 1 > top.line) {
24
+ ranges.push({ start: top.line, end: lineNumber - 1 });
25
+ }
26
+ }
27
+ stack.push({ level, line: lineNumber });
28
+ }
29
+ }
30
+ const lastLine = lines.length;
31
+ while (stack.length > 0) {
32
+ const top = stack.pop()!;
33
+ if (lastLine > top.line) {
34
+ ranges.push({ start: top.line, end: lastLine });
35
+ }
36
+ }
37
+ return ranges;
38
+ },
39
+ });
40
+ }
41
+
42
+ // JS/TS: brace folding (if/else, functions) takes priority, followed by
43
+ // markdown heading folding inside template literals. Brace ranges are
44
+ // returned first so Monaco resolves conflicts in their favour.
45
+ // Using registerFoldingRangeProvider (not createFoldingRangeFromSelection)
46
+ // so both live in the same range set and Monaco's overlap resolution is
47
+ // consistent — headings are always bounded by their template literal close.
48
+ for (const lang of ['javascript', 'typescript'] as const) {
49
+ if (!foldingProvidersRegistered.has(lang)) {
50
+ foldingProvidersRegistered.add(lang);
51
+ monacoInstance.languages.registerFoldingRangeProvider(lang, {
52
+ provideFoldingRanges(model) {
53
+ const lines = model.getLinesContent();
54
+ const headingPattern = /^(#{1,6})\s/;
55
+
56
+ const braceRanges: monaco.languages.FoldingRange[] = [];
57
+ const headingRanges: monaco.languages.FoldingRange[] = [];
58
+
59
+ const braceStack: number[] = [];
60
+ const headingStack: Array<{ level: number; line: number }> = [];
61
+
62
+ let inTemplate = false;
63
+ let inString = false;
64
+ let stringChar = '';
65
+
66
+ for (let i = 0; i < lines.length; i++) {
67
+ const lineNumber = i + 1;
68
+ const line = lines[i];
69
+ const lineStartedInTemplate = inTemplate;
70
+
71
+ for (let j = 0; j < line.length; j++) {
72
+ const ch = line[j];
73
+ if (ch === '\\') { j++; continue; }
74
+ if (inString) { if (ch === stringChar) inString = false; continue; }
75
+ if (inTemplate) { if (ch === '`') inTemplate = false; continue; }
76
+ if (ch === '`') { inTemplate = true; continue; }
77
+ if (ch === '"' || ch === "'") { inString = true; stringChar = ch; continue; }
78
+ // Brace folding — only outside strings/templates
79
+ if (ch === '{') { braceStack.push(lineNumber); }
80
+ if (ch === '}' && braceStack.length > 0) {
81
+ const start = braceStack.pop()!;
82
+ // If there is non-whitespace content after `}` on this line
83
+ // (e.g. "} else {"), end the fold at the previous line so
84
+ // the continuation is not hidden inside the fold.
85
+ const restOfLine = line.slice(j + 1).trimStart();
86
+ const endLine = restOfLine.length > 0 ? lineNumber - 1 : lineNumber;
87
+ if (endLine > start) braceRanges.push({ start, end: endLine });
88
+ }
89
+ }
90
+
91
+ // Markdown heading folding — only inside template literals
92
+ if (lineStartedInTemplate) {
93
+ const match = headingPattern.exec(line);
94
+ if (match) {
95
+ const level = match[1].length;
96
+ while (headingStack.length > 0 && headingStack[headingStack.length - 1].level >= level) {
97
+ const top = headingStack.pop()!;
98
+ if (lineNumber - 1 > top.line) {
99
+ headingRanges.push({ start: top.line, end: lineNumber - 1 });
100
+ }
101
+ }
102
+ headingStack.push({ level, line: lineNumber });
103
+ }
104
+ // Template just closed — seal all open heading sections here
105
+ if (!inTemplate) {
106
+ while (headingStack.length > 0) {
107
+ const top = headingStack.pop()!;
108
+ if (lineNumber > top.line) {
109
+ headingRanges.push({ start: top.line, end: lineNumber });
110
+ }
111
+ }
112
+ }
113
+ }
114
+ }
115
+
116
+ // Brace ranges first → Monaco resolves conflicts in their favour
117
+ return [...braceRanges, ...headingRanges];
118
+ },
119
+ });
120
+ }
121
+ }
122
+ }