@mui/internal-docs-infra 0.11.1-canary.8 → 0.11.1

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 (319) hide show
  1. package/ChunkProvider/ChunkContext.d.mts +10 -0
  2. package/ChunkProvider/ChunkContext.mjs +15 -0
  3. package/ChunkProvider/ChunkProvider.d.mts +14 -0
  4. package/ChunkProvider/ChunkProvider.mjs +38 -0
  5. package/ChunkProvider/PreloadContext.d.mts +14 -0
  6. package/ChunkProvider/PreloadContext.mjs +18 -0
  7. package/ChunkProvider/PreloadProvider.d.mts +13 -0
  8. package/ChunkProvider/PreloadProvider.mjs +33 -0
  9. package/ChunkProvider/index.d.mts +7 -0
  10. package/ChunkProvider/index.mjs +7 -0
  11. package/ChunkProvider/types.d.mts +23 -0
  12. package/ChunkProvider/types.mjs +1 -0
  13. package/ChunkProvider/usePreload.d.mts +8 -0
  14. package/ChunkProvider/usePreload.mjs +21 -0
  15. package/CodeControllerContext/CodeControllerContext.d.mts +11 -0
  16. package/CodeControllerContext/CodeControllerContext.mjs +2 -1
  17. package/CodeHighlighter/CodeHighlighter.d.mts +15 -1
  18. package/CodeHighlighter/CodeHighlighter.mjs +97 -319
  19. package/CodeHighlighter/CodeHighlighterChunk.d.mts +42 -0
  20. package/CodeHighlighter/CodeHighlighterChunk.mjs +77 -0
  21. package/CodeHighlighter/CodeHighlighterClient.mjs +597 -128
  22. package/CodeHighlighter/CodeHighlighterContext.d.mts +57 -1
  23. package/CodeHighlighter/CodeHighlighterFallbackContext.d.mts +14 -2
  24. package/CodeHighlighter/CodeHighlighterFallbackContext.mjs +1 -3
  25. package/CodeHighlighter/CodeInitialSourceLoader.d.mts +10 -0
  26. package/CodeHighlighter/CodeInitialSourceLoader.mjs +108 -0
  27. package/CodeHighlighter/CodeSourceLoader.d.mts +11 -0
  28. package/CodeHighlighter/CodeSourceLoader.mjs +128 -0
  29. package/CodeHighlighter/buildCodeHighlighterChunkProps.d.mts +47 -0
  30. package/CodeHighlighter/buildCodeHighlighterChunkProps.mjs +61 -0
  31. package/CodeHighlighter/buildStringFallback.d.mts +29 -0
  32. package/CodeHighlighter/buildStringFallback.mjs +42 -0
  33. package/CodeHighlighter/codeToFallbackProps.d.mts +31 -2
  34. package/CodeHighlighter/codeToFallbackProps.mjs +347 -42
  35. package/CodeHighlighter/createClientProps.d.mts +17 -0
  36. package/CodeHighlighter/createClientProps.mjs +78 -0
  37. package/CodeHighlighter/errors.d.mts +6 -0
  38. package/CodeHighlighter/errors.mjs +10 -0
  39. package/CodeHighlighter/fallbackCompression.d.mts +96 -0
  40. package/CodeHighlighter/fallbackCompression.mjs +253 -0
  41. package/CodeHighlighter/fallbackFormat.d.mts +137 -0
  42. package/CodeHighlighter/fallbackFormat.mjs +422 -0
  43. package/CodeHighlighter/index.d.mts +4 -1
  44. package/CodeHighlighter/index.mjs +3 -1
  45. package/CodeHighlighter/mergeComments.d.mts +38 -0
  46. package/CodeHighlighter/mergeComments.mjs +80 -0
  47. package/CodeHighlighter/prepareInitialSource.d.mts +42 -0
  48. package/CodeHighlighter/prepareInitialSource.mjs +292 -0
  49. package/CodeHighlighter/resolveFallbackCritical.d.mts +23 -0
  50. package/CodeHighlighter/resolveFallbackCritical.mjs +44 -0
  51. package/CodeHighlighter/types.d.mts +272 -8
  52. package/CodeHighlighter/useCodeFallback.d.mts +94 -0
  53. package/CodeHighlighter/useCodeFallback.mjs +204 -0
  54. package/CodeHighlighter/useGrammarsReady.d.mts +18 -0
  55. package/CodeHighlighter/useGrammarsReady.mjs +45 -0
  56. package/CodeHighlighter/useSpeculativeCodePreload.d.mts +26 -0
  57. package/CodeHighlighter/useSpeculativeCodePreload.mjs +40 -0
  58. package/CodeHighlighter/useSpeculativeEditingPreload.d.mts +33 -0
  59. package/CodeHighlighter/useSpeculativeEditingPreload.mjs +58 -0
  60. package/CodeHighlighter/useSpeculativeGrammarPreload.d.mts +23 -0
  61. package/CodeHighlighter/useSpeculativeGrammarPreload.mjs +31 -0
  62. package/CodeHighlighter/useSpeculativeUseCodePreload.d.mts +22 -0
  63. package/CodeHighlighter/useSpeculativeUseCodePreload.mjs +41 -0
  64. package/CodeProvider/CodeContext.d.mts +47 -12
  65. package/CodeProvider/CodeContext.mjs +7 -0
  66. package/CodeProvider/CodeProvider.d.mts +4 -2
  67. package/CodeProvider/CodeProvider.mjs +40 -102
  68. package/CodeProvider/CodeProviderLazy.d.mts +40 -0
  69. package/CodeProvider/CodeProviderLazy.mjs +96 -0
  70. package/CodeProvider/constants.d.mts +26 -0
  71. package/CodeProvider/constants.mjs +24 -0
  72. package/CodeProvider/createParseSourceWorkerClient.d.mts +6 -0
  73. package/CodeProvider/createParseSourceWorkerClient.mjs +22 -2
  74. package/CodeProvider/index.d.mts +2 -1
  75. package/CodeProvider/index.mjs +9 -1
  76. package/CodeProvider/parseSourceWorker.mjs +33 -0
  77. package/CodeProvider/useCodeProviderValue.d.mts +54 -0
  78. package/CodeProvider/useCodeProviderValue.mjs +188 -0
  79. package/CoordinatedLazy/ChunkServerLoader.d.mts +25 -0
  80. package/CoordinatedLazy/ChunkServerLoader.mjs +97 -0
  81. package/CoordinatedLazy/CoordinatedContentContext.d.mts +15 -0
  82. package/CoordinatedLazy/CoordinatedContentContext.mjs +22 -0
  83. package/CoordinatedLazy/CoordinatedFallbackContext.d.mts +11 -0
  84. package/CoordinatedLazy/CoordinatedFallbackContext.mjs +13 -0
  85. package/CoordinatedLazy/CoordinatedGateContext.d.mts +14 -0
  86. package/CoordinatedLazy/CoordinatedGateContext.mjs +19 -0
  87. package/CoordinatedLazy/CoordinatedLazy.d.mts +14 -0
  88. package/CoordinatedLazy/CoordinatedLazy.mjs +86 -0
  89. package/CoordinatedLazy/CoordinatedLazyClient.d.mts +24 -0
  90. package/CoordinatedLazy/CoordinatedLazyClient.mjs +65 -0
  91. package/CoordinatedLazy/LazyContent.d.mts +26 -0
  92. package/CoordinatedLazy/LazyContent.mjs +80 -0
  93. package/CoordinatedLazy/LazyContentServer.d.mts +18 -0
  94. package/CoordinatedLazy/LazyContentServer.mjs +25 -0
  95. package/CoordinatedLazy/buildChunkRenderInputs.d.mts +8 -0
  96. package/CoordinatedLazy/buildChunkRenderInputs.mjs +35 -0
  97. package/CoordinatedLazy/createCoordinatedLazy.d.mts +32 -0
  98. package/CoordinatedLazy/createCoordinatedLazy.mjs +127 -0
  99. package/CoordinatedLazy/index.d.mts +14 -0
  100. package/CoordinatedLazy/index.mjs +18 -0
  101. package/CoordinatedLazy/resolveChunkRender.d.mts +26 -0
  102. package/CoordinatedLazy/resolveChunkRender.mjs +73 -0
  103. package/CoordinatedLazy/types.d.mts +408 -0
  104. package/CoordinatedLazy/types.mjs +1 -0
  105. package/CoordinatedLazy/useChunk.d.mts +30 -0
  106. package/CoordinatedLazy/useChunk.mjs +135 -0
  107. package/CoordinatedLazy/useCoordinatedFallback.d.mts +12 -0
  108. package/CoordinatedLazy/useCoordinatedFallback.mjs +40 -0
  109. package/CoordinatedLazy/useCoordinatedSwap.d.mts +16 -0
  110. package/CoordinatedLazy/useCoordinatedSwap.mjs +124 -0
  111. package/LICENSE +1 -1
  112. package/abstractCreateDemo/abstractCreateDemo.d.mts +54 -3
  113. package/abstractCreateDemo/abstractCreateDemo.mjs +47 -7
  114. package/abstractCreateDemo/resolveDemoFlag.d.mts +20 -0
  115. package/abstractCreateDemo/resolveDemoFlag.mjs +25 -0
  116. package/abstractCreateStream/abstractCreateStream.d.mts +18 -0
  117. package/abstractCreateStream/abstractCreateStream.mjs +45 -0
  118. package/abstractCreateStream/index.d.mts +2 -0
  119. package/abstractCreateStream/index.mjs +1 -0
  120. package/abstractCreateStream/types.d.mts +34 -0
  121. package/abstractCreateStream/types.mjs +1 -0
  122. package/abstractCreateTypes/TypeCode.mjs +12 -11
  123. package/abstractCreateTypes/typesToJsx.mjs +30 -9
  124. package/cli/ensureDemoClients.mjs +4 -148
  125. package/cli/ensureDemoPages.d.mts +45 -0
  126. package/cli/ensureDemoPages.mjs +99 -0
  127. package/cli/fileUtils/index.d.mts +11 -0
  128. package/cli/fileUtils/index.mjs +48 -0
  129. package/cli/findDemoIndexFiles.d.mts +15 -0
  130. package/cli/findDemoIndexFiles.mjs +121 -0
  131. package/cli/index.mjs +1 -1
  132. package/cli/loadNextConfig.d.mts +25 -0
  133. package/cli/loadNextConfig.mjs +60 -1
  134. package/cli/runBrowser.mjs +1 -1
  135. package/cli/runValidate.mjs +44 -1
  136. package/package.json +85 -5
  137. package/pipeline/enhanceCodeEmphasis/enhanceCodeEmphasis.mjs +30 -0
  138. package/pipeline/enhanceCodeEmphasis/enhanceCodeEmphasisLazy.d.mts +17 -0
  139. package/pipeline/enhanceCodeEmphasis/enhanceCodeEmphasisLazy.mjs +52 -0
  140. package/pipeline/hastUtils/frameFallbackFromSpans.d.mts +18 -0
  141. package/pipeline/hastUtils/frameFallbackFromSpans.mjs +24 -0
  142. package/pipeline/hastUtils/hast.d.mts +27 -0
  143. package/pipeline/hastUtils/hastCompression.d.mts +3 -1
  144. package/pipeline/hastUtils/hastCompression.mjs +9 -1
  145. package/pipeline/hastUtils/hastDecompress.mjs +10 -4
  146. package/pipeline/hastUtils/hastDictionary.mjs +9 -0
  147. package/pipeline/hastUtils/hastUtils.d.mts +4 -3
  148. package/pipeline/hastUtils/hastUtils.mjs +24 -12
  149. package/pipeline/hastUtils/index.d.mts +2 -1
  150. package/pipeline/hastUtils/index.mjs +2 -1
  151. package/pipeline/hastUtils/stripHighlightingSpans.d.mts +6 -2
  152. package/pipeline/hastUtils/stripHighlightingSpans.mjs +22 -10
  153. package/pipeline/lintJavascriptDemoFocus/lintJavascriptDemoFocus.mjs +10 -7
  154. package/pipeline/loadIsomorphicCodeVariant/applyCodeTransform.d.mts +31 -13
  155. package/pipeline/loadIsomorphicCodeVariant/applyCodeTransform.mjs +50 -55
  156. package/pipeline/loadIsomorphicCodeVariant/applyCodeTransformWithComments.d.mts +78 -0
  157. package/pipeline/loadIsomorphicCodeVariant/applyCodeTransformWithComments.mjs +405 -0
  158. package/pipeline/loadIsomorphicCodeVariant/computeHastDeltas.d.mts +5 -5
  159. package/pipeline/loadIsomorphicCodeVariant/computeHastDeltas.mjs +36 -66
  160. package/pipeline/loadIsomorphicCodeVariant/decodeHastSource.d.mts +23 -0
  161. package/pipeline/loadIsomorphicCodeVariant/decodeHastSource.mjs +92 -0
  162. package/pipeline/loadIsomorphicCodeVariant/decodeSource.d.mts +19 -0
  163. package/pipeline/loadIsomorphicCodeVariant/decodeSource.mjs +25 -0
  164. package/pipeline/loadIsomorphicCodeVariant/decodeSourceToText.d.mts +17 -0
  165. package/pipeline/loadIsomorphicCodeVariant/decodeSourceToText.mjs +26 -0
  166. package/pipeline/loadIsomorphicCodeVariant/diffHast.d.mts +26 -2
  167. package/pipeline/loadIsomorphicCodeVariant/diffHast.mjs +563 -19
  168. package/pipeline/loadIsomorphicCodeVariant/embedTransforms.d.mts +49 -0
  169. package/pipeline/loadIsomorphicCodeVariant/embedTransforms.mjs +152 -0
  170. package/pipeline/loadIsomorphicCodeVariant/findExpandingRanges.d.mts +51 -0
  171. package/pipeline/loadIsomorphicCodeVariant/findExpandingRanges.mjs +161 -0
  172. package/pipeline/loadIsomorphicCodeVariant/flattenCodeVariant.mjs +6 -3
  173. package/pipeline/loadIsomorphicCodeVariant/getAvailableTransforms.d.mts +12 -0
  174. package/pipeline/loadIsomorphicCodeVariant/getAvailableTransforms.mjs +44 -0
  175. package/pipeline/loadIsomorphicCodeVariant/getInitialVisibleSourceLines.d.mts +16 -0
  176. package/pipeline/loadIsomorphicCodeVariant/getInitialVisibleSourceLines.mjs +74 -0
  177. package/pipeline/loadIsomorphicCodeVariant/loadCodeFallback.mjs +17 -5
  178. package/pipeline/loadIsomorphicCodeVariant/loadIsomorphicCodeVariant.mjs +229 -15
  179. package/pipeline/loadIsomorphicCodeVariant/transformSource.d.mts +2 -2
  180. package/pipeline/loadIsomorphicCodeVariant/transformSource.mjs +56 -22
  181. package/pipeline/loadPrecomputedCodeHighlighter/loadPrecomputedCodeHighlighter.d.mts +18 -0
  182. package/pipeline/loadPrecomputedCodeHighlighter/loadPrecomputedCodeHighlighter.mjs +11 -7
  183. package/pipeline/loadServerTypes/hastTypeUtils.d.mts +2 -2
  184. package/pipeline/loadServerTypes/hastTypeUtils.mjs +4 -4
  185. package/pipeline/loadServerTypes/loadServerTypes.mjs +1 -1
  186. package/pipeline/loadServerTypesMeta/extractJSDocText.d.mts +14 -0
  187. package/pipeline/loadServerTypesMeta/extractJSDocText.mjs +60 -0
  188. package/pipeline/loadServerTypesMeta/processTypes.mjs +43 -46
  189. package/pipeline/loadServerTypesText/order.mjs +1 -1
  190. package/pipeline/loadServerTypesText/parseTypesMarkdown.mjs +3 -1
  191. package/pipeline/loaderUtils/index.d.mts +0 -1
  192. package/pipeline/loaderUtils/index.mjs +0 -1
  193. package/pipeline/loaderUtils/parseImportsAndComments.d.mts +5 -1
  194. package/pipeline/loaderUtils/parseImportsAndComments.mjs +19 -9
  195. package/pipeline/loaderUtils/resolveModulePath.mjs +23 -1
  196. package/pipeline/parseCreateFactoryCall/parseCreateFactoryCall.d.mts +12 -0
  197. package/pipeline/parseCreateFactoryCall/parseCreateFactoryCall.mjs +17 -13
  198. package/pipeline/parseSource/addLineGutters.mjs +45 -11
  199. package/pipeline/parseSource/calculateFrameRanges.d.mts +22 -0
  200. package/pipeline/parseSource/calculateFrameRanges.mjs +69 -25
  201. package/pipeline/parseSource/detectGrammarScopes.d.mts +13 -0
  202. package/pipeline/parseSource/detectGrammarScopes.mjs +35 -0
  203. package/pipeline/parseSource/extendSyntaxTokens.mjs +501 -43
  204. package/pipeline/parseSource/frameVisibility.d.mts +47 -0
  205. package/pipeline/parseSource/frameVisibility.mjs +114 -0
  206. package/pipeline/parseSource/grammarCache.d.mts +33 -0
  207. package/pipeline/parseSource/grammarCache.mjs +73 -0
  208. package/pipeline/parseSource/grammarLoaders.d.mts +14 -0
  209. package/pipeline/parseSource/grammarLoaders.mjs +24 -0
  210. package/pipeline/parseSource/grammarMaps.d.mts +21 -1
  211. package/pipeline/parseSource/grammarMaps.mjs +36 -0
  212. package/pipeline/parseSource/isFrameSpan.d.mts +19 -0
  213. package/pipeline/parseSource/isFrameSpan.mjs +24 -0
  214. package/pipeline/parseSource/parseSource.d.mts +41 -6
  215. package/pipeline/parseSource/parseSource.mjs +184 -36
  216. package/pipeline/parseSource/redistributeFrameFallbacks.d.mts +40 -0
  217. package/pipeline/parseSource/redistributeFrameFallbacks.mjs +138 -0
  218. package/pipeline/parseSource/restructureFrames.d.mts +5 -0
  219. package/pipeline/parseSource/restructureFrames.mjs +179 -16
  220. package/pipeline/syncPageIndex/metadataToMarkdown.mjs +6 -2
  221. package/pipeline/transformHtmlCodeBlock/transformHtmlCodeBlock.d.mts +26 -0
  222. package/pipeline/transformHtmlCodeBlock/transformHtmlCodeBlock.mjs +181 -114
  223. package/pipeline/transformHtmlCodeInline/removeSuffixFromHighlightedNodes.d.mts +12 -0
  224. package/pipeline/transformHtmlCodeInline/removeSuffixFromHighlightedNodes.mjs +52 -0
  225. package/pipeline/transformHtmlCodeInline/transformHtmlCodeInline.mjs +22 -1
  226. package/pipeline/transformTypescriptToJavascript/removeTypes.d.mts +5 -8
  227. package/pipeline/transformTypescriptToJavascript/removeTypes.mjs +27 -93
  228. package/useCode/EditableEngine.d.mts +233 -0
  229. package/useCode/EditableEngine.mjs +1712 -0
  230. package/useCode/EditingEngine.d.mts +13 -0
  231. package/useCode/EditingEngine.mjs +14 -0
  232. package/useCode/Pre.browser.mjs +5 -1
  233. package/useCode/Pre.d.mts +127 -1
  234. package/useCode/Pre.mjs +417 -165
  235. package/useCode/SourceEditingEngine.d.mts +50 -0
  236. package/useCode/SourceEditingEngine.mjs +461 -0
  237. package/useCode/TransformEngine.d.mts +39 -0
  238. package/useCode/TransformEngine.mjs +208 -0
  239. package/useCode/editingEngineCache.d.mts +29 -0
  240. package/useCode/editingEngineCache.mjs +68 -0
  241. package/useCode/sourceLineCounts.d.mts +80 -0
  242. package/useCode/sourceLineCounts.mjs +284 -0
  243. package/useCode/subscribeToggleNudge.d.mts +3 -0
  244. package/useCode/subscribeToggleNudge.mjs +95 -0
  245. package/useCode/transformEngineCache.d.mts +21 -0
  246. package/useCode/transformEngineCache.mjs +60 -0
  247. package/useCode/useCode.d.mts +140 -1
  248. package/useCode/useCode.mjs +250 -19
  249. package/useCode/useCodeUtils.d.mts +131 -20
  250. package/useCode/useCodeUtils.mjs +267 -194
  251. package/useCode/useCopyFunctionality.d.mts +13 -1
  252. package/useCode/useCopyFunctionality.mjs +39 -9
  253. package/useCode/useEditable.browser.mjs +10 -2
  254. package/useCode/useEditable.d.mts +27 -106
  255. package/useCode/useEditable.integration.browser.d.mts +1 -0
  256. package/useCode/useEditable.integration.browser.mjs +870 -0
  257. package/useCode/useEditable.mjs +198 -1247
  258. package/useCode/useEditableUtils.d.mts +50 -1
  259. package/useCode/useEditableUtils.mjs +29 -0
  260. package/useCode/useFileNavigation.d.mts +91 -3
  261. package/useCode/useFileNavigation.mjs +201 -41
  262. package/useCode/useHighlightGate.d.mts +17 -0
  263. package/useCode/useHighlightGate.mjs +147 -0
  264. package/useCode/useSourceEditing.d.mts +8 -0
  265. package/useCode/useSourceEditing.mjs +158 -314
  266. package/useCode/useSourceEnhancing.d.mts +5 -1
  267. package/useCode/useSourceEnhancing.mjs +22 -36
  268. package/useCode/useTransformManagement.d.mts +93 -5
  269. package/useCode/useTransformManagement.mjs +496 -28
  270. package/useCode/useTransitionPhase.d.mts +24 -0
  271. package/useCode/useTransitionPhase.mjs +49 -0
  272. package/useCode/useUIState.d.mts +2 -2
  273. package/useCode/useUIState.mjs +8 -8
  274. package/useCode/useVariantSelection.d.mts +130 -6
  275. package/useCode/useVariantSelection.mjs +529 -93
  276. package/useCodeWindow/useCodeWindow.d.mts +19 -2
  277. package/useCodeWindow/useCodeWindow.mjs +98 -71
  278. package/useCoordinated/coordinatePreference.d.mts +439 -0
  279. package/useCoordinated/coordinatePreference.mjs +951 -0
  280. package/useCoordinated/coordinatePreference.testUtils.d.mts +21 -0
  281. package/useCoordinated/coordinatePreference.testUtils.mjs +69 -0
  282. package/useCoordinated/createSettleGate.d.mts +96 -0
  283. package/useCoordinated/createSettleGate.mjs +171 -0
  284. package/useCoordinated/index.d.mts +8 -0
  285. package/useCoordinated/index.mjs +8 -0
  286. package/useCoordinated/layoutShiftGate.d.mts +24 -0
  287. package/useCoordinated/layoutShiftGate.mjs +79 -0
  288. package/useCoordinated/pageSettleGate.d.mts +11 -0
  289. package/useCoordinated/pageSettleGate.mjs +13 -0
  290. package/useCoordinated/scheduleTasks.d.mts +23 -0
  291. package/useCoordinated/scheduleTasks.mjs +45 -0
  292. package/useCoordinated/useCoordinated.d.mts +193 -0
  293. package/useCoordinated/useCoordinated.mjs +469 -0
  294. package/useCoordinated/useCoordinatedLazy.d.mts +17 -0
  295. package/useCoordinated/useCoordinatedLazy.mjs +38 -0
  296. package/useCoordinated/useCoordinatedLocalStorage.d.mts +16 -0
  297. package/useCoordinated/useCoordinatedLocalStorage.mjs +22 -0
  298. package/useCoordinated/useCoordinatedPreference.d.mts +20 -0
  299. package/useCoordinated/useCoordinatedPreference.mjs +26 -0
  300. package/useCoordinated/useSettleGate.d.mts +11 -0
  301. package/useCoordinated/useSettleGate.mjs +34 -0
  302. package/useDemo/exportVariant.d.mts +12 -5
  303. package/useDemo/exportVariant.mjs +59 -5
  304. package/useDemo/useDemo.d.mts +5 -2
  305. package/useScrollAnchor/useScrollAnchor.mjs +28 -5
  306. package/useStream/index.d.mts +6 -0
  307. package/useStream/index.mjs +6 -0
  308. package/useStream/streamChunks.d.mts +23 -0
  309. package/useStream/streamChunks.mjs +85 -0
  310. package/useStream/types.d.mts +45 -0
  311. package/useStream/types.mjs +1 -0
  312. package/useStream/useStream.d.mts +57 -0
  313. package/useStream/useStream.mjs +119 -0
  314. package/useStream/useStreamController.d.mts +15 -0
  315. package/useStream/useStreamController.mjs +90 -0
  316. package/withDocsInfra/withDocsInfra.d.mts +19 -0
  317. package/withDocsInfra/withDocsInfra.mjs +13 -5
  318. package/pipeline/loaderUtils/convertCommentsToOneIndexed.d.mts +0 -8
  319. package/pipeline/loaderUtils/convertCommentsToOneIndexed.mjs +0 -16
@@ -0,0 +1,870 @@
1
+ var _style;
2
+ /**
3
+ * Browser integration tests for `useEditable`: the full type → flush → re-highlight →
4
+ * restore cycle the docs live code editor runs on every keystroke. They cover the
5
+ * behaviors that only emerge when the highlighted DOM is replaced underneath the caret —
6
+ * caret stability while typing and deleting indents, scroll-anchor `onBoundary` firing at
7
+ * the visible top/bottom, focus retention on the first keystroke, and a selection's
8
+ * direction surviving a re-render.
9
+ *
10
+ * Unlike `useEditable.browser.ts` (which drives a static highlighted DOM with a `vi.fn()`
11
+ * onChange and only asserts the *text* handed to onChange), these tests wire `useEditable`
12
+ * to a real React component whose `onChange` updates source state, re-highlights it into
13
+ * the production `.line`/`pl-*` span structure, and lets the engine's `observeAndRestore`
14
+ * restore the caret.
15
+ */
16
+ import * as React from 'react';
17
+ import { describe, it, expect, vi, beforeAll, afterEach } from 'vitest';
18
+ import { render, cleanup, act } from '@testing-library/react';
19
+ import { userEvent } from 'vitest/browser';
20
+ import { useEditable, preloadEditableEngine } from "./useEditable.mjs";
21
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
22
+ beforeAll(async () => {
23
+ await preloadEditableEngine();
24
+ });
25
+ afterEach(() => {
26
+ cleanup();
27
+ document.body.innerHTML = '';
28
+ window.getSelection()?.removeAllRanges();
29
+ });
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Minimal, deterministic re-highlighter
33
+ // ---------------------------------------------------------------------------
34
+ // Splits each line into tokens and wraps identifiers (`pl-smi`) and single-char
35
+ // operators (`pl-k`) in their own spans, leaving whitespace/punctuation as bare
36
+ // text. This reproduces the multi-span-per-line DOM shape that production
37
+ // highlighting produces — enough to surface caret-at-element-boundary bugs —
38
+ // without pulling in the async grammar/worker pipeline. Each line span keeps its
39
+ // trailing `\n` inside it, matching the production serializer.
40
+ const WORD = /[A-Za-z0-9_$]/;
41
+ const OPERATOR = new Set(['=', '+', '-', '*', '/', '<', '>', '!', '&', '|', '%', '?', ':']);
42
+ function highlightLine(lineText) {
43
+ const nodes = [];
44
+ let i = 0;
45
+ let key = 0;
46
+ while (i < lineText.length) {
47
+ const char = lineText[i];
48
+ if (WORD.test(char)) {
49
+ let j = i + 1;
50
+ while (j < lineText.length && WORD.test(lineText[j])) {
51
+ j += 1;
52
+ }
53
+ nodes.push(/*#__PURE__*/_jsx("span", {
54
+ className: "pl-smi",
55
+ children: lineText.slice(i, j)
56
+ }, key));
57
+ key += 1;
58
+ i = j;
59
+ } else if (OPERATOR.has(char)) {
60
+ nodes.push(/*#__PURE__*/_jsx("span", {
61
+ className: "pl-k",
62
+ children: char
63
+ }, key));
64
+ key += 1;
65
+ i += 1;
66
+ } else {
67
+ // Coalesce a run of bare characters (whitespace/punctuation) into one
68
+ // text node, as the highlighter would.
69
+ let j = i + 1;
70
+ while (j < lineText.length && !WORD.test(lineText[j]) && !OPERATOR.has(lineText[j])) {
71
+ j += 1;
72
+ }
73
+ nodes.push(lineText.slice(i, j));
74
+ i = j;
75
+ }
76
+ }
77
+ return nodes;
78
+ }
79
+ function Highlighted({
80
+ source
81
+ }) {
82
+ // Mirror the production live-editing structure: `.line` spans live inside a
83
+ // `.frame`, and the newline after each line is a SEPARATE sibling text node
84
+ // ("trailing newline" / gap node), NOT kept inside the line span. These gap
85
+ // nodes are exactly what `caretSelector` + `snapCaretOutOfGapNode` exist for.
86
+ // `source` always carries a trailing newline.
87
+ const withoutTrailing = source.endsWith('\n') ? source.slice(0, -1) : source;
88
+ const lines = withoutTrailing.split('\n');
89
+ return /*#__PURE__*/_jsx("code", {
90
+ children: /*#__PURE__*/_jsx("span", {
91
+ className: "frame",
92
+ "data-frame": "0",
93
+ "data-lined": "",
94
+ children: lines.map((line, index) => /*#__PURE__*/_jsxs(React.Fragment, {
95
+ children: [/*#__PURE__*/_jsx("span", {
96
+ className: "line",
97
+ "data-ln": index + 1,
98
+ children: highlightLine(line)
99
+ }), '\n']
100
+ }, index))
101
+ })
102
+ });
103
+ }
104
+ function Editor({
105
+ initialSource,
106
+ options,
107
+ handleRef,
108
+ scroll
109
+ }) {
110
+ const [source, setSource] = React.useState(initialSource);
111
+ const [, forceRerender] = React.useReducer(count => count + 1, 0);
112
+ const ref = React.useRef(null);
113
+ const scrollRef = React.useRef(null);
114
+ const onChange = React.useMemo(() => vi.fn((text, _position) => {
115
+ setSource(text);
116
+ }), []);
117
+ useEditable(ref, onChange, options);
118
+ React.useEffect(() => {
119
+ handleRef.current = {
120
+ ref,
121
+ onChange,
122
+ getSource: () => onChange.mock.calls.length ? onChange.mock.calls[onChange.mock.calls.length - 1][0] : initialSource,
123
+ scrollEl: scrollRef.current,
124
+ rerender: forceRerender
125
+ };
126
+ });
127
+ const editor = /*#__PURE__*/_jsxs(React.Fragment, {
128
+ children: [_style || (_style = /*#__PURE__*/_jsx("style", {
129
+ children: `
130
+ [data-testid="editor"] { line-height: 1.5; font-family: monospace; }
131
+ [data-testid="editor"] .frame[data-lined] { display: block; white-space: normal; line-height: 0; }
132
+ [data-testid="editor"] .frame[data-lined] .line { display: block; white-space: pre; line-height: initial; }
133
+ `
134
+ })), /*#__PURE__*/_jsx("pre", {
135
+ ref: ref,
136
+ style: {
137
+ tabSize: 2,
138
+ margin: 0
139
+ },
140
+ "data-testid": "editor",
141
+ children: /*#__PURE__*/_jsx(Highlighted, {
142
+ source: source
143
+ })
144
+ })]
145
+ });
146
+ if (!scroll) {
147
+ return editor;
148
+ }
149
+ // A short, scrollable viewport so we can detect "the view jumps" by reading
150
+ // scrollTop before/after an edit.
151
+ return /*#__PURE__*/_jsx("div", {
152
+ ref: scrollRef,
153
+ style: {
154
+ height: '60px',
155
+ overflow: 'auto',
156
+ fontFamily: 'monospace',
157
+ lineHeight: '20px'
158
+ },
159
+ "data-testid": "scroll",
160
+ children: editor
161
+ });
162
+ }
163
+
164
+ /** Renders the editor and returns a handle once the engine has attached. */
165
+ async function setupEditor(initialSource, options = {}, opts = {}) {
166
+ const handleRef = {
167
+ current: null
168
+ };
169
+ render(/*#__PURE__*/_jsx(Editor, {
170
+ initialSource: initialSource,
171
+ options: options,
172
+ handleRef: handleRef,
173
+ scroll: opts.scroll
174
+ }));
175
+
176
+ // Wait a frame for the layout effects (engine attach + contentEditable).
177
+ await act(async () => {
178
+ await new Promise(resolve => {
179
+ requestAnimationFrame(() => resolve());
180
+ });
181
+ });
182
+ const handle = handleRef.current;
183
+ const element = handle.ref.current;
184
+ return {
185
+ handle,
186
+ element
187
+ };
188
+ }
189
+
190
+ /**
191
+ * Places the caret at an absolute character offset (counting newlines that live
192
+ * inside `.line` spans) and waits a frame so the engine captures the position.
193
+ */
194
+ async function placeCaret(element, offset) {
195
+ element.focus();
196
+ const sel = window.getSelection();
197
+ const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
198
+ let current = 0;
199
+ let node = walker.nextNode();
200
+ while (node) {
201
+ const len = node.textContent.length;
202
+ if (current + len >= offset) {
203
+ const range = document.createRange();
204
+ range.setStart(node, offset - current);
205
+ range.collapse(true);
206
+ sel.removeAllRanges();
207
+ sel.addRange(range);
208
+ break;
209
+ }
210
+ current += len;
211
+ node = walker.nextNode();
212
+ }
213
+ await act(async () => {
214
+ await new Promise(resolve => {
215
+ requestAnimationFrame(() => resolve());
216
+ });
217
+ });
218
+ }
219
+
220
+ /** Computes the caret's (line, column) from the live Selection. */
221
+ function caretLineColumn(element) {
222
+ const sel = window.getSelection();
223
+ const range = sel.getRangeAt(0);
224
+ const until = document.createRange();
225
+ until.setStart(element, 0);
226
+ until.setEnd(range.startContainer, range.startOffset);
227
+ const text = until.toString();
228
+ const lines = text.split('\n');
229
+ return {
230
+ line: lines.length - 1,
231
+ column: lines[lines.length - 1].length,
232
+ position: text.length
233
+ };
234
+ }
235
+
236
+ /** Lets queued microtasks / rAF callbacks / React effects settle. */
237
+ async function settle() {
238
+ await act(async () => {
239
+ await new Promise(resolve => {
240
+ requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
241
+ });
242
+ });
243
+ }
244
+
245
+ /** Builds an async preParse that re-highlights on a macrotask (worker-like). */
246
+ function asyncPreParse() {
247
+ return vi.fn((_text, _position) => new Promise(resolve => {
248
+ setTimeout(() => resolve(undefined), 0);
249
+ }));
250
+ }
251
+
252
+ /**
253
+ * A preParse whose resolution is controlled by the test — models the worker
254
+ * round-trip so we can inspect the DOM *during* the async gap (before the
255
+ * re-highlight commits) the way the user sees "the wrong thing for a second".
256
+ */
257
+ function deferredPreParse() {
258
+ let resolveFn = null;
259
+ const preParse = vi.fn((_text, _position) => new Promise(resolve => {
260
+ resolveFn = () => resolve(undefined);
261
+ }));
262
+ return {
263
+ preParse,
264
+ resolvePending: async () => {
265
+ resolveFn?.();
266
+ resolveFn = null;
267
+ await settle();
268
+ }
269
+ };
270
+ }
271
+ describe('useEditable — caret & selection across re-highlights', () => {
272
+ // -------------------------------------------------------------------------
273
+ // Caret stability when typing x, =, Backspace (must not jump to column 0)
274
+ // -------------------------------------------------------------------------
275
+ it('keeps the caret after the typed char when typing x, =, Backspace on a fresh line', async () => {
276
+ const {
277
+ handle,
278
+ element
279
+ } = await setupEditor('\n', {
280
+ indentation: 2,
281
+ caretSelector: '.line'
282
+ });
283
+ await placeCaret(element, 0);
284
+ await userEvent.keyboard('x');
285
+ await settle();
286
+ await userEvent.keyboard('=');
287
+ await settle();
288
+ await userEvent.keyboard('{Backspace}');
289
+ await settle();
290
+ expect(handle.getSource()).toBe('x\n');
291
+ expect(caretLineColumn(element)).toMatchObject({
292
+ line: 0,
293
+ column: 1
294
+ });
295
+ });
296
+ it('keeps the caret after the typed char through an async re-highlight (preParse)', async () => {
297
+ const {
298
+ handle,
299
+ element
300
+ } = await setupEditor('\n', {
301
+ indentation: 2,
302
+ caretSelector: '.line',
303
+ preParse: asyncPreParse()
304
+ });
305
+ await placeCaret(element, 0);
306
+ await userEvent.keyboard('x');
307
+ await settle();
308
+ await settle();
309
+ await userEvent.keyboard('=');
310
+ await settle();
311
+ await settle();
312
+ await userEvent.keyboard('{Backspace}');
313
+ await settle();
314
+ await settle();
315
+ expect(handle.getSource()).toBe('x\n');
316
+ expect(caretLineColumn(element)).toMatchObject({
317
+ line: 0,
318
+ column: 1
319
+ });
320
+ });
321
+ it('keeps the caret when typing = then Backspace at the end of an indented line', async () => {
322
+ // ` <p style=` then backspace the `=`. This mirrors typing an attribute.
323
+ const initial = 'function App() {\n return <p style\n}\n';
324
+ const {
325
+ handle,
326
+ element
327
+ } = await setupEditor(initial, {
328
+ indentation: 2,
329
+ caretSelector: '.line'
330
+ });
331
+ // caret at end of ` return <p style` (line 1)
332
+ const offset = 'function App() {\n return <p style'.length;
333
+ await placeCaret(element, offset);
334
+ await userEvent.keyboard('=');
335
+ await settle();
336
+ await userEvent.keyboard('{Backspace}');
337
+ await settle();
338
+ expect(handle.getSource()).toBe(initial);
339
+ // caret should be back at end of `style` (column 17), not column 0
340
+ expect(caretLineColumn(element)).toMatchObject({
341
+ line: 1,
342
+ column: ' return <p style'.length
343
+ });
344
+ });
345
+ it('keeps the caret at the end of an indented line in a collapsed (minColumn) gutter', async () => {
346
+ // The collapsed editor (clipped indent gutter) is where the real bug shows.
347
+ const initial = 'function foo() {\n doStuff()\n}\n';
348
+ const {
349
+ handle,
350
+ element
351
+ } = await setupEditor(initial, {
352
+ indentation: 2,
353
+ caretSelector: '.line',
354
+ minColumn: 2,
355
+ minRow: 1,
356
+ maxRow: 1,
357
+ onBoundary: vi.fn()
358
+ });
359
+ const offset = 'function foo() {\n doStuff()'.length; // end of line 1
360
+ await placeCaret(element, offset);
361
+ await userEvent.keyboard('x');
362
+ await settle();
363
+ await userEvent.keyboard('=');
364
+ await settle();
365
+ await userEvent.keyboard('{Backspace}');
366
+ await settle();
367
+ expect(handle.getSource()).toBe('function foo() {\n doStuff()x\n}\n');
368
+ expect(caretLineColumn(element)).toMatchObject({
369
+ line: 1,
370
+ column: ' doStuff()x'.length
371
+ });
372
+ });
373
+ it('keeps the caret in a collapsed gutter through an async re-highlight', async () => {
374
+ const initial = 'function foo() {\n doStuff()\n}\n';
375
+ const {
376
+ handle,
377
+ element
378
+ } = await setupEditor(initial, {
379
+ indentation: 2,
380
+ caretSelector: '.line',
381
+ minColumn: 2,
382
+ minRow: 1,
383
+ maxRow: 1,
384
+ onBoundary: vi.fn(),
385
+ preParse: asyncPreParse()
386
+ });
387
+ const offset = 'function foo() {\n doStuff()'.length;
388
+ await placeCaret(element, offset);
389
+ await userEvent.keyboard('x');
390
+ await settle();
391
+ await settle();
392
+ await userEvent.keyboard('=');
393
+ await settle();
394
+ await settle();
395
+ await userEvent.keyboard('{Backspace}');
396
+ await settle();
397
+ await settle();
398
+ expect(handle.getSource()).toBe('function foo() {\n doStuff()x\n}\n');
399
+ expect(caretLineColumn(element)).toMatchObject({
400
+ line: 1,
401
+ column: ' doStuff()x'.length
402
+ });
403
+ });
404
+ it('lands the caret at the line/gap boundary when editing at the end of a line', async () => {
405
+ // The real bug uses the `End` key to land the caret at the line end — which
406
+ // in the framed `.line` structure is the boundary with the inter-line gap
407
+ // node. Native typing there can flatten the spans / split across lines.
408
+ const initial = 'function foo() {\n doStuff()\n}\n';
409
+ const {
410
+ handle,
411
+ element
412
+ } = await setupEditor(initial, {
413
+ indentation: 2,
414
+ caretSelector: '.line'
415
+ });
416
+ // Put the caret somewhere on line 1, then End to the line end.
417
+ await placeCaret(element, 'function foo() {\n do'.length);
418
+ await userEvent.keyboard('{End}');
419
+ await settle();
420
+ await userEvent.keyboard('x');
421
+ await settle();
422
+ await userEvent.keyboard('=');
423
+ await settle();
424
+ await userEvent.keyboard('{Backspace}');
425
+ await settle();
426
+ expect(handle.getSource()).toBe('function foo() {\n doStuff()x\n}\n');
427
+ expect(caretLineColumn(element)).toMatchObject({
428
+ line: 1,
429
+ column: ' doStuff()x'.length
430
+ });
431
+ });
432
+
433
+ // -------------------------------------------------------------------------
434
+ // Caret restoration when erasing the last indent on a clipped (collapsed-window) line
435
+ // -------------------------------------------------------------------------
436
+ it('restores the caret when backspacing the last indent of a blank clipped line (minColumn)', async () => {
437
+ // Simulate a collapsed window: indentation clipped to minColumn=2, the
438
+ // visible region is rows 1..3. Line 2 is a blank line with exactly 2 spaces.
439
+ const initial = 'function foo() {\n const a = 1;\n \n return a;\n}\n';
440
+ const {
441
+ element
442
+ } = await setupEditor(initial, {
443
+ indentation: 2,
444
+ caretSelector: '.line',
445
+ minColumn: 2,
446
+ minRow: 1,
447
+ maxRow: 3,
448
+ onBoundary: vi.fn()
449
+ }, {
450
+ scroll: true
451
+ });
452
+ // caret at end of the blank line 2 (column 2 == minColumn)
453
+ const offset = 'function foo() {\n const a = 1;\n '.length;
454
+ await placeCaret(element, offset);
455
+ const before = caretLineColumn(element);
456
+ await userEvent.keyboard('{Backspace}');
457
+ await settle();
458
+ const after = caretLineColumn(element);
459
+ // Assert the (arguably correct) behavior: stay on the same line, now empty.
460
+ expect(after.line).toBe(before.line);
461
+ });
462
+ it('restores the caret when backspacing an indent on a content line in a clipped gutter (minColumn)', async () => {
463
+ const initial = 'function foo() {\n const a = 1;\n}\n';
464
+ const {
465
+ element
466
+ } = await setupEditor(initial, {
467
+ indentation: 2,
468
+ caretSelector: '.line',
469
+ minColumn: 2,
470
+ minRow: 1,
471
+ maxRow: 1,
472
+ onBoundary: vi.fn()
473
+ }, {
474
+ scroll: true
475
+ });
476
+ // caret right after the 2-space indent on line 1 (` const a = 1;`)
477
+ const offset = 'function foo() {\n '.length;
478
+ await placeCaret(element, offset);
479
+ await userEvent.keyboard('{Backspace}');
480
+ await settle();
481
+ const after = caretLineColumn(element);
482
+ expect(after).toBeTruthy();
483
+ });
484
+
485
+ // -------------------------------------------------------------------------
486
+ // ArrowUp at the visible top fires onBoundary (scroll anchor)
487
+ // -------------------------------------------------------------------------
488
+ it('fires onBoundary on ArrowUp at the first row and ArrowDown at the last row', async () => {
489
+ const onBoundary = vi.fn();
490
+ const initial = 'line0\nline1\nline2\nline3\nline4\n';
491
+ const {
492
+ element
493
+ } = await setupEditor(initial, {
494
+ indentation: 2,
495
+ caretSelector: '.line',
496
+ minRow: 2,
497
+ maxRow: 3,
498
+ onBoundary
499
+ }, {
500
+ scroll: true
501
+ });
502
+ // Caret on line 2 (minRow), then ArrowUp.
503
+ const upOffset = 'line0\nline1\n'.length + 2;
504
+ await placeCaret(element, upOffset);
505
+ await userEvent.keyboard('{ArrowUp}');
506
+ await settle();
507
+ const upCalls = onBoundary.mock.calls.length;
508
+
509
+ // Caret on line 3 (maxRow), then ArrowDown.
510
+ const downOffset = 'line0\nline1\nline2\n'.length + 2;
511
+ await placeCaret(element, downOffset);
512
+ await userEvent.keyboard('{ArrowDown}');
513
+ await settle();
514
+ const downCalls = onBoundary.mock.calls.length - upCalls;
515
+ expect(upCalls).toBeGreaterThan(0); // ArrowUp must fire the boundary
516
+ expect(downCalls).toBeGreaterThan(0); // ArrowDown must fire the boundary
517
+ });
518
+
519
+ // -------------------------------------------------------------------------
520
+ // Transient DOM vs committed source during an async re-highlight: backspacing the
521
+ // last indent collapses the line during the worker round-trip, before the committed
522
+ // source catches up.
523
+ // -------------------------------------------------------------------------
524
+ it('keeps the transient DOM consistent with the committed source when async-backspacing the last indent', async () => {
525
+ const {
526
+ preParse,
527
+ resolvePending
528
+ } = deferredPreParse();
529
+ const initial = 'function foo() {\n const a = 1;\n \n return a;\n}\n';
530
+ const {
531
+ element
532
+ } = await setupEditor(initial, {
533
+ indentation: 2,
534
+ caretSelector: '.line',
535
+ minColumn: 2,
536
+ minRow: 1,
537
+ maxRow: 3,
538
+ onBoundary: vi.fn(),
539
+ preParse
540
+ }, {
541
+ scroll: true
542
+ });
543
+ const offset = 'function foo() {\n const a = 1;\n '.length;
544
+ await placeCaret(element, offset);
545
+
546
+ // Compact structural signature: the frame's direct children, each as
547
+ // either `LINE(<text>)` or `TEXT(<text>)`, so we can see the mangled
548
+ // transient structure without vitest truncating a long innerHTML string.
549
+ const signature = () => {
550
+ const frame = element.querySelector('.frame');
551
+ return Array.from(frame.childNodes).map(node => {
552
+ if (node.nodeType === Node.TEXT_NODE) {
553
+ return `TEXT(${JSON.stringify(node.textContent)})`;
554
+ }
555
+ const childEl = node;
556
+ if (childEl.classList?.contains('line')) {
557
+ return `LINE(${JSON.stringify(childEl.textContent)})`;
558
+ }
559
+ return `OTHER(${childEl.tagName})`;
560
+ });
561
+ };
562
+ await userEvent.keyboard('{Backspace}');
563
+ await settle();
564
+
565
+ // DURING the async gap (preParse not yet resolved): what does the DOM show?
566
+ const transientSig = signature();
567
+ await resolvePending();
568
+ await settle();
569
+ const finalSig = signature();
570
+ // The transient DOM the user sees during the async worker round-trip must already
571
+ // be structurally consistent with the final committed result — no wrong intermediate
572
+ // state (no dangling empty `.line` span, no dropped gap newline), so the signatures match.
573
+ expect(transientSig).toEqual(finalSig);
574
+ });
575
+
576
+ // -------------------------------------------------------------------------
577
+ // ArrowUp navigation across consecutive empty lines
578
+ // -------------------------------------------------------------------------
579
+ it('moves ArrowUp onto an empty line first, then the line above', async () => {
580
+ const initial = 'aaa\n\nbbb\nccc\n';
581
+ const {
582
+ element
583
+ } = await setupEditor(initial, {
584
+ indentation: 2,
585
+ caretSelector: '.line'
586
+ });
587
+
588
+ // Start on "ccc" (line 3), column 1.
589
+ const start = 'aaa\n\nbbb\n'.length + 1;
590
+ await placeCaret(element, start);
591
+ await userEvent.keyboard('{ArrowUp}');
592
+ await settle();
593
+ const step1 = caretLineColumn(element); // expect line 2 ("bbb")
594
+
595
+ await userEvent.keyboard('{ArrowUp}');
596
+ await settle();
597
+ const step2 = caretLineColumn(element); // expect line 1 (empty)
598
+
599
+ await userEvent.keyboard('{ArrowUp}');
600
+ await settle();
601
+ const step3 = caretLineColumn(element); // expect line 0 ("aaa")
602
+
603
+ // At the engine level the empty line IS reachable and each step lands on
604
+ // the expected row (this passes in all 3 browsers). If the "weird" ArrowUp
605
+ // behavior shows up in the real docs, it is not in this engine path —
606
+ // suspect the real frame/visibleFrames render or layout, not navigation.
607
+ expect(step1.line).toBe(2);
608
+ expect(step2.line).toBe(1); // the empty line — reachable, not skipped
609
+ expect(step3.line).toBe(0);
610
+ });
611
+
612
+ // -------------------------------------------------------------------------
613
+ // New bug: TWO consecutive empty lines — ArrowUp skips both at once.
614
+ // -------------------------------------------------------------------------
615
+ it('stops ArrowUp on each of two consecutive empty lines (does not skip both)', async () => {
616
+ // Lines: 0 "aaa", 1 "" , 2 "", 3 "bbb", 4 "ccc".
617
+ const initial = 'aaa\n\n\nbbb\nccc\n';
618
+ const {
619
+ element
620
+ } = await setupEditor(initial, {
621
+ indentation: 2,
622
+ caretSelector: '.line'
623
+ });
624
+
625
+ // Start on "bbb" (line 3), column 1.
626
+ const start = 'aaa\n\n\n'.length + 1;
627
+ await placeCaret(element, start);
628
+ await userEvent.keyboard('{ArrowUp}');
629
+ await settle();
630
+ const step1 = caretLineColumn(element); // expect line 2 (second empty line)
631
+
632
+ await userEvent.keyboard('{ArrowUp}');
633
+ await settle();
634
+ const step2 = caretLineColumn(element); // expect line 1 (first empty line)
635
+
636
+ await userEvent.keyboard('{ArrowUp}');
637
+ await settle();
638
+ const step3 = caretLineColumn(element); // expect line 0 ("aaa")
639
+
640
+ // DESIRED: ArrowUp stops on EACH line, including the zero-height empty ones.
641
+ // The bug skips both empty lines, so a single ArrowUp from "bbb" jumps
642
+ // straight to "aaa" (step1.line === 0).
643
+ const ctx = JSON.stringify({
644
+ step1,
645
+ step2,
646
+ step3
647
+ });
648
+ expect(step1.line, ctx).toBe(2); // second empty line
649
+ expect(step2.line, ctx).toBe(1); // first empty line
650
+ expect(step3.line, ctx).toBe(0); // "aaa"
651
+ });
652
+
653
+ // -------------------------------------------------------------------------
654
+ // A BACKWARD Shift+Arrow selection keeps its focus at the top across a
655
+ // host re-render (the restore must not flip the focus to the bottom end).
656
+ // -------------------------------------------------------------------------
657
+ it('preserves a backward Shift+ArrowUp selection across a re-render, focus still at the top', async () => {
658
+ const initial = 'aaa\nbbb\nccc\nddd\n';
659
+ const {
660
+ handle,
661
+ element
662
+ } = await setupEditor(initial, {
663
+ indentation: 2,
664
+ caretSelector: '.line'
665
+ });
666
+
667
+ // Reads the moving end (focus) and the fixed end (anchor) of the live
668
+ // selection as (line, column), so we can assert the selection DIRECTION —
669
+ // `caretLineColumn` only reports the forward-normalized range start.
670
+ const endPoint = (node, offset) => {
671
+ const until = document.createRange();
672
+ until.setStart(element, 0);
673
+ until.setEnd(node, offset);
674
+ const lines = until.toString().split('\n');
675
+ return {
676
+ line: lines.length - 1,
677
+ column: lines[lines.length - 1].length
678
+ };
679
+ };
680
+ const focusPoint = () => {
681
+ const sel = window.getSelection();
682
+ return endPoint(sel.focusNode, sel.focusOffset);
683
+ };
684
+ const anchorPoint = () => {
685
+ const sel = window.getSelection();
686
+ return endPoint(sel.anchorNode, sel.anchorOffset);
687
+ };
688
+
689
+ // Start collapsed on "ddd" (line 3), column 0, then grow the selection
690
+ // UPWARD twice so the focus is above the anchor (a backward range).
691
+ await placeCaret(element, 'aaa\nbbb\nccc\n'.length);
692
+ await userEvent.keyboard('{Shift>}{ArrowUp}{ArrowUp}{/Shift}');
693
+ await settle();
694
+
695
+ // Anchor stays on line 3; the focus has climbed two lines up to line 1.
696
+ expect(anchorPoint().line, 'anchor before re-render').toBe(3);
697
+ expect(focusPoint().line, 'focus before re-render').toBe(1);
698
+
699
+ // An idle host re-render (e.g. an async re-highlight committing) re-runs the
700
+ // engine's caret/selection restore. The backward direction must survive it.
701
+ act(() => {
702
+ handle.rerender();
703
+ });
704
+ await settle();
705
+ expect(anchorPoint().line, 'anchor after re-render').toBe(3);
706
+ // The bug: the restore rebuilt a forward range, flipping the focus to the
707
+ // bottom (line 3). With the fix the focus stays at the top (line 1).
708
+ expect(focusPoint().line, 'focus after re-render').toBe(1);
709
+
710
+ // And the next Shift+ArrowUp must keep extending from the TOP — landing the
711
+ // focus on line 0 — rather than collapsing the selection from the bottom.
712
+ await userEvent.keyboard('{Shift>}{ArrowUp}{/Shift}');
713
+ await settle();
714
+ expect(focusPoint().line, 'focus after a third Shift+ArrowUp').toBe(0);
715
+ expect(anchorPoint().line, 'anchor unchanged after third Shift+ArrowUp').toBe(3);
716
+ });
717
+
718
+ // -------------------------------------------------------------------------
719
+ // Focus retention on the first keystroke (async preParse path)
720
+ // -------------------------------------------------------------------------
721
+ it('keeps focus after the first keystroke through an async re-highlight', async () => {
722
+ const {
723
+ element
724
+ } = await setupEditor('hello\n', {
725
+ indentation: 2,
726
+ caretSelector: '.line',
727
+ preParse: asyncPreParse()
728
+ });
729
+ await placeCaret(element, 5);
730
+ expect(document.activeElement).toBe(element);
731
+ await userEvent.keyboard('!');
732
+ await settle();
733
+ await settle();
734
+ expect(document.activeElement).toBe(element);
735
+ });
736
+
737
+ // -------------------------------------------------------------------------
738
+ // Single-bound paging: PageDown engages only when `maxRow` is set and PageUp
739
+ // only when `minRow` is set — mirroring ArrowDown (needs `maxRow`) / ArrowUp
740
+ // (needs `minRow`). With only the OPPOSITE bound present the page key has no
741
+ // fold in its direction, so it falls through to native and must NOT fire
742
+ // `onBoundary`. Latent through `Pre` (which always supplies both bounds), but
743
+ // `Options.minRow`/`maxRow` are independently optional.
744
+ // -------------------------------------------------------------------------
745
+ it('pages to the fold only for the bound in its own direction', async () => {
746
+ const initial = 'line0\nline1\nline2\nline3\nline4\n';
747
+
748
+ // Only `maxRow` (a bottom fold, no top fold): PageDown expands, PageUp is native.
749
+ {
750
+ const onBoundary = vi.fn();
751
+ const {
752
+ element
753
+ } = await setupEditor(initial, {
754
+ indentation: 2,
755
+ caretSelector: '.line',
756
+ maxRow: 2,
757
+ onBoundary
758
+ }, {
759
+ scroll: true
760
+ });
761
+ await placeCaret(element, 'line0\n'.length); // line 1, inside the window
762
+ await userEvent.keyboard('{PageUp}');
763
+ await settle();
764
+ expect(onBoundary, 'PageUp with no minRow stays native').not.toHaveBeenCalled();
765
+ await userEvent.keyboard('{PageDown}');
766
+ await settle();
767
+ expect(onBoundary, 'PageDown expands the bottom fold').toHaveBeenCalledTimes(1);
768
+ expect(caretLineColumn(element).line, 'caret jumped to the bottom edge').toBe(2);
769
+ }
770
+
771
+ // Only `minRow` (a top fold, no bottom fold): PageUp expands, PageDown is native.
772
+ {
773
+ const onBoundary = vi.fn();
774
+ const {
775
+ element
776
+ } = await setupEditor(initial, {
777
+ indentation: 2,
778
+ caretSelector: '.line',
779
+ minRow: 2,
780
+ onBoundary
781
+ }, {
782
+ scroll: true
783
+ });
784
+ await placeCaret(element, 'line0\nline1\nline2\nline3\n'.length); // line 4, inside the window
785
+ await userEvent.keyboard('{PageDown}');
786
+ await settle();
787
+ expect(onBoundary, 'PageDown with no maxRow stays native').not.toHaveBeenCalled();
788
+ await userEvent.keyboard('{PageUp}');
789
+ await settle();
790
+ expect(onBoundary, 'PageUp expands the top fold').toHaveBeenCalledTimes(1);
791
+ expect(caretLineColumn(element).line, 'caret jumped to the top edge').toBe(2);
792
+ }
793
+ });
794
+ });
795
+
796
+ // ---------------------------------------------------------------------------
797
+ // deletedFromLineStart: whole-line removals only, not in-place collapses
798
+ // ---------------------------------------------------------------------------
799
+ // A selection delete reports `deletedFromLineStart` so the controlled
800
+ // comment/highlight map drops its anchor one line — the post-delete caret sits on
801
+ // a line that shifted up from below the deletion. That must be limited to
802
+ // deletions that removed WHOLE lines. A selection that ends mid-line collapses the
803
+ // spanned lines INTO the first line, which survives (emptied) under the caret;
804
+ // reporting the flag there drags a marker on that surviving line one line too high
805
+ // (the live-editor "the highlight shifts up instead of being deleted" bug). The
806
+ // source stands in for an @highlight block on lines 4-6 with blank padding (3, 7).
807
+ describe('useEditable — selection delete reports deletedFromLineStart only for whole-line removals', () => {
808
+ const SRC = 'const a = 1;\nconst b = 2;\n\ndoThing();\ndoOther();\ndoLast();\n\nconst c = 3;\n';
809
+ const lastPosition = handle => handle.onChange.mock.calls.at(-1)?.[1];
810
+ it('reports the flag when a column-0 selection removes whole lines', async () => {
811
+ const {
812
+ handle,
813
+ element
814
+ } = await setupEditor(SRC, {
815
+ indentation: 2,
816
+ caretSelector: '.line'
817
+ });
818
+ // Column 0 of the blank line 3 → column 0 of the blank line 7 (a line boundary):
819
+ // whole lines 3-6 are removed and line 7 shifts up under the caret.
820
+ await placeCaret(element, 26);
821
+ await userEvent.keyboard('{Shift>}{ArrowDown}{ArrowDown}{ArrowDown}{ArrowDown}{/Shift}');
822
+ await settle();
823
+ await userEvent.keyboard('{Backspace}');
824
+ await settle();
825
+ expect(handle.getSource()).toBe('const a = 1;\nconst b = 2;\n\nconst c = 3;\n');
826
+ expect(lastPosition(handle)?.deletedFromLineStart).toBe(true);
827
+ });
828
+ it('reports the flag when a column-0 selection stops on the region’s exclusive -end line', async () => {
829
+ const {
830
+ handle,
831
+ element
832
+ } = await setupEditor(SRC, {
833
+ indentation: 2,
834
+ caretSelector: '.line'
835
+ });
836
+ // Column 0 of the blank line 3 → column 0 of line 6 (still a line boundary), one
837
+ // ArrowDown short of scenario A. A range's @highlight-end is EXCLUSIVE — it sits
838
+ // on the line just below the last highlighted line — so selecting "from the line
839
+ // above through the last newline of the region" lands here, deleting the whole
840
+ // highlighted body (lines 4-5) while doLast() (the -end line) survives. This is
841
+ // still a whole-line removal, so the flag holds; the matching comment-map case is
842
+ // `useSourceEditing`'s "removes the highlight when the selection stops on the
843
+ // region’s exclusive -end line".
844
+ await placeCaret(element, 26);
845
+ await userEvent.keyboard('{Shift>}{ArrowDown}{ArrowDown}{ArrowDown}{/Shift}');
846
+ await settle();
847
+ await userEvent.keyboard('{Backspace}');
848
+ await settle();
849
+ expect(handle.getSource()).toBe('const a = 1;\nconst b = 2;\ndoLast();\n\nconst c = 3;\n');
850
+ expect(lastPosition(handle)?.deletedFromLineStart).toBe(true);
851
+ });
852
+ it('does NOT report the flag when the selection ends mid-line and collapses in place', async () => {
853
+ const {
854
+ handle,
855
+ element
856
+ } = await setupEditor(SRC, {
857
+ indentation: 2,
858
+ caretSelector: '.line'
859
+ });
860
+ // Column 0 of line 4 → the END of line 6 (mid-line, NOT a line boundary): the three
861
+ // region lines collapse into one empty line that survives under the caret.
862
+ await placeCaret(element, 27);
863
+ await userEvent.keyboard('{Shift>}{ArrowDown}{ArrowDown}{End}{/Shift}');
864
+ await settle();
865
+ await userEvent.keyboard('{Backspace}');
866
+ await settle();
867
+ expect(handle.getSource()).toBe('const a = 1;\nconst b = 2;\n\n\n\nconst c = 3;\n');
868
+ expect(lastPosition(handle)?.deletedFromLineStart).not.toBe(true);
869
+ });
870
+ });