@xincli/opentui-core 0.4.6 → 0.4.7

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 (619) hide show
  1. package/LICENSE +21 -0
  2. package/NativeSpanFeed.d.ts +52 -0
  3. package/README.md +0 -0
  4. package/Renderable.d.ts +346 -0
  5. package/animation/Timeline.d.ts +126 -0
  6. package/ansi.d.ts +17 -0
  7. package/audio.d.ts +89 -0
  8. package/buffer.d.ts +113 -0
  9. package/console.d.ts +146 -0
  10. package/edit-buffer.d.ts +97 -0
  11. package/editor-view.d.ts +72 -0
  12. package/index-m1y9e40w.js +10045 -0
  13. package/index-m1y9e40w.js.map +32 -0
  14. package/index-sqk4ajdf.js +16121 -0
  15. package/index-sqk4ajdf.js.map +58 -0
  16. package/index.d.ts +24 -0
  17. package/index.js +11709 -0
  18. package/index.js.map +35 -0
  19. package/lib/KeyHandler.d.ts +61 -0
  20. package/lib/RGBA.d.ts +42 -0
  21. package/lib/ascii.font.d.ts +508 -0
  22. package/lib/border.d.ts +51 -0
  23. package/lib/bunfs.d.ts +7 -0
  24. package/lib/clipboard.d.ts +15 -0
  25. package/lib/clock.d.ts +15 -0
  26. package/lib/data-paths.d.ts +26 -0
  27. package/lib/debounce.d.ts +42 -0
  28. package/lib/detect-links.d.ts +6 -0
  29. package/lib/env.d.ts +42 -0
  30. package/lib/extmarks-history.d.ts +17 -0
  31. package/lib/extmarks.d.ts +90 -0
  32. package/lib/hast-styled-text.d.ts +17 -0
  33. package/lib/index.d.ts +22 -0
  34. package/lib/keybinding.internal.d.ts +33 -0
  35. package/lib/objects-in-viewport.d.ts +24 -0
  36. package/lib/output.capture.d.ts +24 -0
  37. package/lib/parse.keypress-kitty.d.ts +3 -0
  38. package/lib/parse.keypress.d.ts +27 -0
  39. package/lib/parse.mouse.d.ts +30 -0
  40. package/lib/paste.d.ts +7 -0
  41. package/lib/queue.d.ts +15 -0
  42. package/lib/render-geometry.d.ts +8 -0
  43. package/lib/renderable.validations.d.ts +12 -0
  44. package/lib/scroll-acceleration.d.ts +43 -0
  45. package/lib/selection.d.ts +64 -0
  46. package/lib/singleton.d.ts +8 -0
  47. package/lib/stdin-parser.d.ts +90 -0
  48. package/lib/styled-text.d.ts +63 -0
  49. package/lib/terminal-capability-detection.d.ts +32 -0
  50. package/lib/terminal-palette.d.ts +65 -0
  51. package/lib/tree-sitter/assets/update.d.ts +12 -0
  52. package/lib/tree-sitter/client.d.ts +65 -0
  53. package/lib/tree-sitter/default-parsers.d.ts +2 -0
  54. package/lib/tree-sitter/download-utils.d.ts +21 -0
  55. package/lib/tree-sitter/index.d.ts +7 -0
  56. package/lib/tree-sitter/parser.worker.d.ts +1 -0
  57. package/lib/tree-sitter/parsers-config.d.ts +53 -0
  58. package/lib/tree-sitter/resolve-ft.d.ts +5 -0
  59. package/lib/tree-sitter/types.d.ts +185 -0
  60. package/lib/tree-sitter/update-assets.d.ts +3 -0
  61. package/lib/tree-sitter/update-assets.js +379 -0
  62. package/lib/tree-sitter/update-assets.js.map +12 -0
  63. package/lib/tree-sitter-styled-text.d.ts +16 -0
  64. package/lib/validate-dir-name.d.ts +1 -0
  65. package/lib/yoga.options.d.ts +32 -0
  66. package/native-event-worker-repro.worker.d.ts +1 -0
  67. package/package.json +52 -74
  68. package/parser.worker.js +1399 -0
  69. package/parser.worker.js.map +20 -0
  70. package/platform/ffi.d.ts +129 -0
  71. package/platform/runtime.d.ts +14 -0
  72. package/platform/test.d.ts +12 -0
  73. package/platform/worker.d.ts +26 -0
  74. package/platform/worker.node-test.d.ts +1 -0
  75. package/plugins/core-slot.d.ts +72 -0
  76. package/plugins/registry.d.ts +42 -0
  77. package/plugins/types.d.ts +34 -0
  78. package/post/effects.d.ts +147 -0
  79. package/post/filters.d.ts +65 -0
  80. package/post/matrices.d.ts +20 -0
  81. package/renderables/ASCIIFont.d.ts +52 -0
  82. package/renderables/Box.d.ts +85 -0
  83. package/renderables/Code.d.ts +97 -0
  84. package/renderables/Diff.d.ts +147 -0
  85. package/renderables/EditBufferRenderable.d.ts +238 -0
  86. package/renderables/FrameBuffer.d.ts +16 -0
  87. package/renderables/Input.d.ts +72 -0
  88. package/renderables/LineNumberRenderable.d.ts +78 -0
  89. package/renderables/Markdown.d.ts +255 -0
  90. package/renderables/ScrollBar.d.ts +77 -0
  91. package/renderables/ScrollBox.d.ts +128 -0
  92. package/renderables/Select.d.ts +122 -0
  93. package/renderables/Slider.d.ts +47 -0
  94. package/renderables/TabSelect.d.ts +98 -0
  95. package/renderables/Text.d.ts +36 -0
  96. package/renderables/TextBufferRenderable.d.ts +107 -0
  97. package/renderables/TextNode.d.ts +92 -0
  98. package/renderables/TextTable.d.ts +154 -0
  99. package/renderables/Textarea.d.ts +65 -0
  100. package/renderables/TimeToFirstDraw.d.ts +24 -0
  101. package/renderables/__tests__/renderable-test-utils.d.ts +12 -0
  102. package/renderables/composition/VRenderable.d.ts +16 -0
  103. package/renderables/composition/constructs.d.ts +35 -0
  104. package/renderables/composition/vnode.d.ts +46 -0
  105. package/renderables/index.d.ts +23 -0
  106. package/renderables/markdown-parser.d.ts +11 -0
  107. package/renderer-theme-mode.d.ts +29 -0
  108. package/renderer.d.ts +601 -0
  109. package/runtime-plugin-support-configure.d.ts +4 -0
  110. package/runtime-plugin-support-configure.js +36 -0
  111. package/runtime-plugin-support-configure.js.map +1 -0
  112. package/runtime-plugin-support-configure.node.js +15 -0
  113. package/runtime-plugin-support.d.ts +3 -0
  114. package/runtime-plugin-support.js +5 -0
  115. package/runtime-plugin-support.js.map +1 -0
  116. package/runtime-plugin-support.node.js +15 -0
  117. package/runtime-plugin.d.ts +16 -0
  118. package/runtime-plugin.js +473 -0
  119. package/runtime-plugin.js.map +1 -0
  120. package/runtime-plugin.node.js +15 -0
  121. package/syntax-style.d.ts +61 -0
  122. package/testing/bun-test-node.d.ts +87 -0
  123. package/testing/manual-clock.d.ts +17 -0
  124. package/testing/mock-keys.d.ts +81 -0
  125. package/testing/mock-mouse.d.ts +38 -0
  126. package/testing/mock-tree-sitter-client.d.ts +27 -0
  127. package/testing/spy.d.ts +7 -0
  128. package/testing/terminal-capabilities.d.ts +7 -0
  129. package/testing/test-recorder.d.ts +61 -0
  130. package/testing/test-renderer.d.ts +54 -0
  131. package/testing/test-streams.d.ts +12 -0
  132. package/testing.d.ts +8 -0
  133. package/testing.js +1004 -0
  134. package/testing.js.map +18 -0
  135. package/tests/yoga-upstream/tools/MeasureCounter.d.ts +16 -0
  136. package/tests/yoga-upstream/tools/utils.d.ts +11 -0
  137. package/text-buffer-view.d.ts +42 -0
  138. package/text-buffer.d.ts +66 -0
  139. package/types.d.ts +175 -0
  140. package/utils.d.ts +14 -0
  141. package/yoga.d.ts +437 -0
  142. package/yoga.js +193 -0
  143. package/yoga.js.map +9 -0
  144. package/zig-structs.d.ts +256 -0
  145. package/zig.d.ts +496 -0
  146. package/dev/keypress-debug-renderer.ts +0 -148
  147. package/dev/keypress-debug.ts +0 -43
  148. package/dev/print-env-vars.ts +0 -32
  149. package/dev/test-tmux-graphics-334.sh +0 -68
  150. package/dev/test-tmux-theme-queries.sh +0 -151
  151. package/dev/thai-debug-test.ts +0 -68
  152. package/dev/theme-mode.ts +0 -290
  153. package/docs/development.md +0 -144
  154. package/prebuilt/aarch64-android/libopentui.so +0 -0
  155. package/scripts/build-android.ts +0 -226
  156. package/scripts/build-native-termux.sh +0 -525
  157. package/scripts/build.ts +0 -530
  158. package/scripts/dist-test.ts +0 -312
  159. package/scripts/package-prebuilt.ts +0 -110
  160. package/scripts/publish.ts +0 -60
  161. package/scripts/test-node-hook.mjs +0 -16
  162. package/scripts/test-node.ts +0 -241
  163. package/scripts/vendor-deps.sh +0 -73
  164. package/src/NativeSpanFeed.ts +0 -375
  165. package/src/Renderable.ts +0 -1878
  166. package/src/__snapshots__/buffer.test.ts.snap +0 -28
  167. package/src/animation/Timeline.test.ts +0 -2709
  168. package/src/animation/Timeline.ts +0 -598
  169. package/src/ansi.ts +0 -22
  170. package/src/audio.ts +0 -479
  171. package/src/benchmark/attenuation-benchmark.ts +0 -81
  172. package/src/benchmark/audio-playback-benchmark.ts +0 -344
  173. package/src/benchmark/box-draw-benchmark.ts +0 -1042
  174. package/src/benchmark/colormatrix-benchmark.ts +0 -123
  175. package/src/benchmark/gain-benchmark.ts +0 -80
  176. package/src/benchmark/latest-all-bench-run.json +0 -707
  177. package/src/benchmark/latest-async-bench-run.json +0 -336
  178. package/src/benchmark/latest-default-bench-run.json +0 -657
  179. package/src/benchmark/latest-large-bench-run.json +0 -707
  180. package/src/benchmark/latest-quick-bench-run.json +0 -207
  181. package/src/benchmark/layout-benchmark.ts +0 -2547
  182. package/src/benchmark/markdown-benchmark.ts +0 -1796
  183. package/src/benchmark/native-span-feed-async-benchmark.ts +0 -355
  184. package/src/benchmark/native-span-feed-benchmark.md +0 -56
  185. package/src/benchmark/native-span-feed-benchmark.ts +0 -596
  186. package/src/benchmark/native-span-feed-compare.ts +0 -280
  187. package/src/benchmark/render-traversal-benchmark.ts +0 -928
  188. package/src/benchmark/text-buffer-render-benchmark.ts +0 -874
  189. package/src/benchmark/text-table-benchmark.ts +0 -948
  190. package/src/buffer.test.ts +0 -292
  191. package/src/buffer.ts +0 -564
  192. package/src/console.test.ts +0 -629
  193. package/src/console.ts +0 -1253
  194. package/src/edit-buffer.test.ts +0 -1852
  195. package/src/edit-buffer.ts +0 -417
  196. package/src/editor-view.test.ts +0 -1056
  197. package/src/editor-view.ts +0 -290
  198. package/src/index.ts +0 -25
  199. package/src/lib/KeyHandler.integration.test.ts +0 -292
  200. package/src/lib/KeyHandler.stopPropagation.test.ts +0 -289
  201. package/src/lib/KeyHandler.test.ts +0 -662
  202. package/src/lib/KeyHandler.ts +0 -222
  203. package/src/lib/RGBA.test.ts +0 -60
  204. package/src/lib/RGBA.ts +0 -343
  205. package/src/lib/ascii.font.ts +0 -330
  206. package/src/lib/border.test.ts +0 -83
  207. package/src/lib/border.ts +0 -170
  208. package/src/lib/bunfs.test.ts +0 -27
  209. package/src/lib/bunfs.ts +0 -18
  210. package/src/lib/clipboard.test.ts +0 -63
  211. package/src/lib/clipboard.ts +0 -41
  212. package/src/lib/clock.ts +0 -35
  213. package/src/lib/data-paths.test.ts +0 -133
  214. package/src/lib/data-paths.ts +0 -109
  215. package/src/lib/debounce.ts +0 -106
  216. package/src/lib/detect-links.test.ts +0 -98
  217. package/src/lib/detect-links.ts +0 -56
  218. package/src/lib/env.test.ts +0 -228
  219. package/src/lib/env.ts +0 -209
  220. package/src/lib/extmarks-history.ts +0 -51
  221. package/src/lib/extmarks-multiwidth.test.ts +0 -324
  222. package/src/lib/extmarks.test.ts +0 -3547
  223. package/src/lib/extmarks.ts +0 -861
  224. package/src/lib/fonts/block.json +0 -405
  225. package/src/lib/fonts/grid.json +0 -265
  226. package/src/lib/fonts/huge.json +0 -741
  227. package/src/lib/fonts/pallet.json +0 -314
  228. package/src/lib/fonts/shade.json +0 -591
  229. package/src/lib/fonts/slick.json +0 -321
  230. package/src/lib/fonts/tiny.json +0 -69
  231. package/src/lib/hast-styled-text.ts +0 -59
  232. package/src/lib/index.ts +0 -22
  233. package/src/lib/keybinding.internal.test.ts +0 -342
  234. package/src/lib/keybinding.internal.ts +0 -182
  235. package/src/lib/objects-in-viewport.test.ts +0 -789
  236. package/src/lib/objects-in-viewport.ts +0 -153
  237. package/src/lib/output.capture.ts +0 -58
  238. package/src/lib/parse.keypress-kitty.protocol.test.ts +0 -347
  239. package/src/lib/parse.keypress-kitty.test.ts +0 -712
  240. package/src/lib/parse.keypress-kitty.ts +0 -470
  241. package/src/lib/parse.keypress.test.ts +0 -2020
  242. package/src/lib/parse.keypress.ts +0 -480
  243. package/src/lib/parse.mouse.test.ts +0 -552
  244. package/src/lib/parse.mouse.ts +0 -232
  245. package/src/lib/paste.ts +0 -18
  246. package/src/lib/queue.ts +0 -65
  247. package/src/lib/render-geometry.ts +0 -36
  248. package/src/lib/renderable.validations.test.ts +0 -87
  249. package/src/lib/renderable.validations.ts +0 -83
  250. package/src/lib/scroll-acceleration.ts +0 -98
  251. package/src/lib/selection.ts +0 -261
  252. package/src/lib/singleton.ts +0 -34
  253. package/src/lib/stdin-parser.test.ts +0 -2439
  254. package/src/lib/stdin-parser.ts +0 -2007
  255. package/src/lib/styled-text.ts +0 -178
  256. package/src/lib/terminal-capability-detection.test.ts +0 -226
  257. package/src/lib/terminal-capability-detection.ts +0 -98
  258. package/src/lib/terminal-palette.test.ts +0 -954
  259. package/src/lib/terminal-palette.ts +0 -441
  260. package/src/lib/tree-sitter/assets/README.md +0 -119
  261. package/src/lib/tree-sitter/assets/update.ts +0 -360
  262. package/src/lib/tree-sitter/assets.d.ts +0 -9
  263. package/src/lib/tree-sitter/cache.test.ts +0 -291
  264. package/src/lib/tree-sitter/client.test.ts +0 -1434
  265. package/src/lib/tree-sitter/client.ts +0 -847
  266. package/src/lib/tree-sitter/default-parsers.ts +0 -133
  267. package/src/lib/tree-sitter/download-utils.ts +0 -148
  268. package/src/lib/tree-sitter/index.ts +0 -75
  269. package/src/lib/tree-sitter/parser.worker.ts +0 -1098
  270. package/src/lib/tree-sitter/parsers-config.ts +0 -81
  271. package/src/lib/tree-sitter/resolve-ft.test.ts +0 -55
  272. package/src/lib/tree-sitter/resolve-ft.ts +0 -189
  273. package/src/lib/tree-sitter/types.ts +0 -132
  274. package/src/lib/tree-sitter/update-assets.ts +0 -10
  275. package/src/lib/tree-sitter-styled-text.test.ts +0 -1253
  276. package/src/lib/tree-sitter-styled-text.ts +0 -314
  277. package/src/lib/validate-dir-name.ts +0 -55
  278. package/src/lib/yoga.options.test.ts +0 -628
  279. package/src/lib/yoga.options.ts +0 -346
  280. package/src/native-event-worker-repro.test.ts +0 -48
  281. package/src/native-event-worker-repro.worker.ts +0 -9
  282. package/src/native-handle.test.ts +0 -221
  283. package/src/platform/ffi.test.ts +0 -516
  284. package/src/platform/ffi.ts +0 -653
  285. package/src/platform/runtime.test.ts +0 -52
  286. package/src/platform/runtime.ts +0 -121
  287. package/src/platform/test.ts +0 -45
  288. package/src/platform/worker-handler-cleanup.fixture.ts +0 -12
  289. package/src/platform/worker-onmessage-startup.fixture.ts +0 -10
  290. package/src/platform/worker-startup.fixture.ts +0 -9
  291. package/src/platform/worker.node-test.ts +0 -153
  292. package/src/platform/worker.ts +0 -425
  293. package/src/plugins/core-slot.ts +0 -579
  294. package/src/plugins/registry.ts +0 -402
  295. package/src/plugins/types.ts +0 -46
  296. package/src/post/effects.ts +0 -948
  297. package/src/post/filters.ts +0 -512
  298. package/src/post/matrices.ts +0 -288
  299. package/src/renderables/ASCIIFont.ts +0 -219
  300. package/src/renderables/Box.test.ts +0 -474
  301. package/src/renderables/Box.ts +0 -353
  302. package/src/renderables/Code.test.ts +0 -2388
  303. package/src/renderables/Code.ts +0 -555
  304. package/src/renderables/Diff.regression.test.ts +0 -381
  305. package/src/renderables/Diff.test.ts +0 -3212
  306. package/src/renderables/Diff.ts +0 -1278
  307. package/src/renderables/EditBufferRenderable.test.ts +0 -288
  308. package/src/renderables/EditBufferRenderable.ts +0 -1152
  309. package/src/renderables/FrameBuffer.ts +0 -47
  310. package/src/renderables/Input.test.ts +0 -1310
  311. package/src/renderables/Input.ts +0 -273
  312. package/src/renderables/LineNumberRenderable.ts +0 -725
  313. package/src/renderables/Markdown.ts +0 -2144
  314. package/src/renderables/ScrollBar.ts +0 -423
  315. package/src/renderables/ScrollBox.ts +0 -903
  316. package/src/renderables/Select.test.ts +0 -1134
  317. package/src/renderables/Select.ts +0 -535
  318. package/src/renderables/Slider.test.ts +0 -456
  319. package/src/renderables/Slider.ts +0 -342
  320. package/src/renderables/TabSelect.test.ts +0 -238
  321. package/src/renderables/TabSelect.ts +0 -447
  322. package/src/renderables/Text.selection-buffer.test.ts +0 -118
  323. package/src/renderables/Text.test.ts +0 -2662
  324. package/src/renderables/Text.ts +0 -147
  325. package/src/renderables/TextBufferRenderable.ts +0 -504
  326. package/src/renderables/TextNode.test.ts +0 -1137
  327. package/src/renderables/TextNode.ts +0 -379
  328. package/src/renderables/TextTable.test.ts +0 -1411
  329. package/src/renderables/TextTable.ts +0 -1434
  330. package/src/renderables/Textarea.ts +0 -425
  331. package/src/renderables/TimeToFirstDraw.ts +0 -89
  332. package/src/renderables/__snapshots__/Code.test.ts.snap +0 -13
  333. package/src/renderables/__snapshots__/Diff.test.ts.snap +0 -785
  334. package/src/renderables/__snapshots__/Text.test.ts.snap +0 -421
  335. package/src/renderables/__snapshots__/TextTable.test.ts.snap +0 -215
  336. package/src/renderables/__tests__/Code.test.ts +0 -55
  337. package/src/renderables/__tests__/LineNumberRenderable.scrollbox-simple.test.ts +0 -144
  338. package/src/renderables/__tests__/LineNumberRenderable.scrollbox.test.ts +0 -827
  339. package/src/renderables/__tests__/LineNumberRenderable.test.ts +0 -1906
  340. package/src/renderables/__tests__/LineNumberRenderable.wrapping.test.ts +0 -85
  341. package/src/renderables/__tests__/Markdown.code-colors.test.ts +0 -288
  342. package/src/renderables/__tests__/Markdown.test.ts +0 -4175
  343. package/src/renderables/__tests__/MultiRenderable.selection.test.ts +0 -87
  344. package/src/renderables/__tests__/Textarea.buffer.test.ts +0 -683
  345. package/src/renderables/__tests__/Textarea.destroyed-events.test.ts +0 -675
  346. package/src/renderables/__tests__/Textarea.editing.test.ts +0 -2120
  347. package/src/renderables/__tests__/Textarea.error-handling.test.ts +0 -35
  348. package/src/renderables/__tests__/Textarea.events.test.ts +0 -738
  349. package/src/renderables/__tests__/Textarea.highlights.test.ts +0 -590
  350. package/src/renderables/__tests__/Textarea.keybinding.test.ts +0 -3275
  351. package/src/renderables/__tests__/Textarea.paste.test.ts +0 -357
  352. package/src/renderables/__tests__/Textarea.rendering.test.ts +0 -1927
  353. package/src/renderables/__tests__/Textarea.scroll.test.ts +0 -733
  354. package/src/renderables/__tests__/Textarea.selection.test.ts +0 -1589
  355. package/src/renderables/__tests__/Textarea.stress.test.ts +0 -670
  356. package/src/renderables/__tests__/Textarea.undo-redo.test.ts +0 -383
  357. package/src/renderables/__tests__/Textarea.visual-lines.test.ts +0 -310
  358. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.code.test.ts.snap +0 -221
  359. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox-simple.test.ts.snap +0 -89
  360. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox.test.ts.snap +0 -457
  361. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.test.ts.snap +0 -158
  362. package/src/renderables/__tests__/__snapshots__/Textarea.rendering.test.ts.snap +0 -387
  363. package/src/renderables/__tests__/markdown-parser.test.ts +0 -241
  364. package/src/renderables/__tests__/renderable-test-utils.ts +0 -60
  365. package/src/renderables/composition/README.md +0 -8
  366. package/src/renderables/composition/VRenderable.ts +0 -32
  367. package/src/renderables/composition/constructs.ts +0 -127
  368. package/src/renderables/composition/vnode.ts +0 -277
  369. package/src/renderables/index.ts +0 -23
  370. package/src/renderables/markdown-parser.ts +0 -79
  371. package/src/renderer-theme-mode.ts +0 -199
  372. package/src/renderer.ts +0 -4975
  373. package/src/runtime-plugin-support-configure.ts +0 -63
  374. package/src/runtime-plugin-support.ts +0 -13
  375. package/src/runtime-plugin.ts +0 -626
  376. package/src/specs/terminal-startup.md +0 -64
  377. package/src/syntax-style.test.ts +0 -841
  378. package/src/syntax-style.ts +0 -274
  379. package/src/testing/README.md +0 -219
  380. package/src/testing/bun-test-node.ts +0 -880
  381. package/src/testing/capture-spans.test.ts +0 -216
  382. package/src/testing/integration.test.ts +0 -276
  383. package/src/testing/manual-clock.ts +0 -117
  384. package/src/testing/mock-keys.test.ts +0 -1378
  385. package/src/testing/mock-keys.ts +0 -457
  386. package/src/testing/mock-mouse.test.ts +0 -218
  387. package/src/testing/mock-mouse.ts +0 -247
  388. package/src/testing/mock-tree-sitter-client.ts +0 -81
  389. package/src/testing/spy.ts +0 -13
  390. package/src/testing/terminal-capabilities.ts +0 -47
  391. package/src/testing/test-recorder.test.ts +0 -410
  392. package/src/testing/test-recorder.ts +0 -133
  393. package/src/testing/test-renderer.ts +0 -375
  394. package/src/testing/test-renderer.wait.test.ts +0 -278
  395. package/src/testing/test-streams.ts +0 -31
  396. package/src/testing.ts +0 -9
  397. package/src/tests/__snapshots__/absolute-positioning.snapshot.test.ts.snap +0 -481
  398. package/src/tests/__snapshots__/renderable.snapshot.test.ts.snap +0 -19
  399. package/src/tests/__snapshots__/scrollbox.test.ts.snap +0 -29
  400. package/src/tests/absolute-positioning.snapshot.test.ts +0 -638
  401. package/src/tests/allocator-stats.test.ts +0 -53
  402. package/src/tests/audio.test.ts +0 -303
  403. package/src/tests/destroy-during-render.test.ts +0 -200
  404. package/src/tests/destroy-on-exit.fixture.ts +0 -36
  405. package/src/tests/destroy-on-exit.test.ts +0 -62
  406. package/src/tests/ffi-borrowed-pointer-callsites.test.ts +0 -153
  407. package/src/tests/hover-cursor.test.ts +0 -98
  408. package/src/tests/native-backed-measurement-lifecycle.test.ts +0 -133
  409. package/src/tests/native-backed-measurement-parity.test.ts +0 -322
  410. package/src/tests/native-span-feed-async.test.ts +0 -173
  411. package/src/tests/native-span-feed-close.test.ts +0 -120
  412. package/src/tests/native-span-feed-coverage.test.ts +0 -227
  413. package/src/tests/native-span-feed-edge-cases.test.ts +0 -384
  414. package/src/tests/native-span-feed-use-after-free.test.ts +0 -45
  415. package/src/tests/opacity.test.ts +0 -123
  416. package/src/tests/renderable.snapshot.test.ts +0 -524
  417. package/src/tests/renderable.test.ts +0 -1851
  418. package/src/tests/renderer.clock.test.ts +0 -270
  419. package/src/tests/renderer.console-startup.test.ts +0 -2151
  420. package/src/tests/renderer.control.test.ts +0 -458
  421. package/src/tests/renderer.core-slot-binding.test.ts +0 -962
  422. package/src/tests/renderer.cursor.test.ts +0 -26
  423. package/src/tests/renderer.custom-stdout.test.ts +0 -1112
  424. package/src/tests/renderer.destroy-during-render.test.ts +0 -147
  425. package/src/tests/renderer.focus-restore.test.ts +0 -257
  426. package/src/tests/renderer.focus.test.ts +0 -329
  427. package/src/tests/renderer.idle.test.ts +0 -219
  428. package/src/tests/renderer.input.test.ts +0 -2615
  429. package/src/tests/renderer.kitty-flags.test.ts +0 -195
  430. package/src/tests/renderer.lifecycle.test.ts +0 -34
  431. package/src/tests/renderer.mouse.test.ts +0 -1315
  432. package/src/tests/renderer.notifications.test.ts +0 -27
  433. package/src/tests/renderer.palette.test.ts +0 -1199
  434. package/src/tests/renderer.remote.test.ts +0 -247
  435. package/src/tests/renderer.render-stats.test.ts +0 -146
  436. package/src/tests/renderer.scrollback-surface.test.ts +0 -474
  437. package/src/tests/renderer.selection.test.ts +0 -138
  438. package/src/tests/renderer.slot-registry.test.ts +0 -684
  439. package/src/tests/renderer.tracker.test.ts +0 -202
  440. package/src/tests/renderer.useMouse.test.ts +0 -83
  441. package/src/tests/runtime-plugin-node-modules-cycle.fixture.ts +0 -76
  442. package/src/tests/runtime-plugin-node-modules-import-like-string.fixture.ts +0 -48
  443. package/src/tests/runtime-plugin-node-modules-mjs.fixture.ts +0 -43
  444. package/src/tests/runtime-plugin-node-modules-no-bare-rewrite.fixture.ts +0 -67
  445. package/src/tests/runtime-plugin-node-modules-package-type-cache.fixture.ts +0 -72
  446. package/src/tests/runtime-plugin-node-modules-runtime-specifier.fixture.ts +0 -44
  447. package/src/tests/runtime-plugin-node-modules-scoped-package-bare-rewrite.fixture.ts +0 -85
  448. package/src/tests/runtime-plugin-path-alias.fixture.ts +0 -43
  449. package/src/tests/runtime-plugin-resolve-roots.fixture.ts +0 -65
  450. package/src/tests/runtime-plugin-support-configure.fixture.ts +0 -31
  451. package/src/tests/runtime-plugin-support-late-addition.fixture.ts +0 -17
  452. package/src/tests/runtime-plugin-support.fixture.ts +0 -11
  453. package/src/tests/runtime-plugin-support.test.ts +0 -54
  454. package/src/tests/runtime-plugin-windows-file-url.fixture.ts +0 -30
  455. package/src/tests/runtime-plugin.fixture.ts +0 -40
  456. package/src/tests/runtime-plugin.test.ts +0 -369
  457. package/src/tests/scrollbox-culling-bug.test.ts +0 -114
  458. package/src/tests/scrollbox-hitgrid-resize.test.ts +0 -136
  459. package/src/tests/scrollbox-hitgrid.test.ts +0 -909
  460. package/src/tests/scrollbox.test.ts +0 -1895
  461. package/src/tests/wrap-resize-perf.test.ts +0 -276
  462. package/src/tests/yoga-api-coverage.test.ts +0 -409
  463. package/src/tests/yoga-callback-stress.test.ts +0 -122
  464. package/src/tests/yoga-ffi.test.ts +0 -117
  465. package/src/tests/yoga-setters.test.ts +0 -921
  466. package/src/tests/yoga-upstream/YGAlignBaselineTest.test.ts +0 -124
  467. package/src/tests/yoga-upstream/YGComputedBorderTest.test.ts +0 -27
  468. package/src/tests/yoga-upstream/YGComputedMarginTest.test.ts +0 -27
  469. package/src/tests/yoga-upstream/YGComputedPaddingTest.test.ts +0 -27
  470. package/src/tests/yoga-upstream/YGDirtiedTest.test.ts +0 -157
  471. package/src/tests/yoga-upstream/YGErrataTest.test.ts +0 -40
  472. package/src/tests/yoga-upstream/YGFlexBasisAuto.test.ts +0 -23
  473. package/src/tests/yoga-upstream/YGHasNewLayout.test.ts +0 -81
  474. package/src/tests/yoga-upstream/YGMeasureCacheTest.test.ts +0 -31
  475. package/src/tests/yoga-upstream/YGMeasureTest.test.ts +0 -70
  476. package/src/tests/yoga-upstream/generated/YGAbsolutePositionTest.test.ts +0 -1864
  477. package/src/tests/yoga-upstream/generated/YGAlignContentTest.test.ts +0 -5829
  478. package/src/tests/yoga-upstream/generated/YGAlignItemsTest.test.ts +0 -2444
  479. package/src/tests/yoga-upstream/generated/YGAlignSelfTest.test.ts +0 -293
  480. package/src/tests/yoga-upstream/generated/YGAndroidNewsFeed.test.ts +0 -315
  481. package/src/tests/yoga-upstream/generated/YGAspectRatioTest.test.ts +0 -231
  482. package/src/tests/yoga-upstream/generated/YGAutoTest.test.ts +0 -347
  483. package/src/tests/yoga-upstream/generated/YGBorderTest.test.ts +0 -256
  484. package/src/tests/yoga-upstream/generated/YGBoxSizingTest.test.ts +0 -2747
  485. package/src/tests/yoga-upstream/generated/YGDimensionTest.test.ts +0 -129
  486. package/src/tests/yoga-upstream/generated/YGDisplayContentsTest.test.ts +0 -109
  487. package/src/tests/yoga-upstream/generated/YGDisplayTest.test.ts +0 -1097
  488. package/src/tests/yoga-upstream/generated/YGFlexDirectionTest.test.ts +0 -4562
  489. package/src/tests/yoga-upstream/generated/YGFlexTest.test.ts +0 -677
  490. package/src/tests/yoga-upstream/generated/YGFlexWrapTest.test.ts +0 -2071
  491. package/src/tests/yoga-upstream/generated/YGGapTest.test.ts +0 -3303
  492. package/src/tests/yoga-upstream/generated/YGIntrinsicSizeTest.test.ts +0 -582
  493. package/src/tests/yoga-upstream/generated/YGJustifyContentTest.test.ts +0 -2150
  494. package/src/tests/yoga-upstream/generated/YGMarginTest.test.ts +0 -1963
  495. package/src/tests/yoga-upstream/generated/YGMinMaxDimensionTest.test.ts +0 -1446
  496. package/src/tests/yoga-upstream/generated/YGPaddingTest.test.ts +0 -354
  497. package/src/tests/yoga-upstream/generated/YGPercentageTest.test.ts +0 -1760
  498. package/src/tests/yoga-upstream/generated/YGRoundingTest.test.ts +0 -1161
  499. package/src/tests/yoga-upstream/generated/YGSizeOverflowTest.test.ts +0 -207
  500. package/src/tests/yoga-upstream/generated/YGStaticPositionTest.test.ts +0 -6338
  501. package/src/tests/yoga-upstream/tools/MeasureCounter.ts +0 -54
  502. package/src/tests/yoga-upstream/tools/utils.ts +0 -81
  503. package/src/text-buffer-view.test.ts +0 -722
  504. package/src/text-buffer-view.ts +0 -193
  505. package/src/text-buffer.test.ts +0 -512
  506. package/src/text-buffer.ts +0 -250
  507. package/src/types.ts +0 -199
  508. package/src/utils.ts +0 -88
  509. package/src/yoga.ts +0 -1146
  510. package/src/zig/ansi.zig +0 -502
  511. package/src/zig/audio.zig +0 -1206
  512. package/src/zig/bench/README.md +0 -50
  513. package/src/zig/bench/buffer-color-blending_bench.zig +0 -350
  514. package/src/zig/bench/buffer-draw-box_bench.zig +0 -786
  515. package/src/zig/bench/buffer-draw-text-buffer_bench.zig +0 -894
  516. package/src/zig/bench/edit-buffer_bench.zig +0 -476
  517. package/src/zig/bench/editor-view_bench.zig +0 -169
  518. package/src/zig/bench/native-span-feed_bench.zig +0 -107
  519. package/src/zig/bench/rope-markers_bench.zig +0 -713
  520. package/src/zig/bench/rope_bench.zig +0 -514
  521. package/src/zig/bench/styled-text_bench.zig +0 -476
  522. package/src/zig/bench/text-buffer-coords_bench.zig +0 -362
  523. package/src/zig/bench/text-buffer-view_bench.zig +0 -459
  524. package/src/zig/bench/text-chunk-graphemes_bench.zig +0 -272
  525. package/src/zig/bench/utf8_bench.zig +0 -799
  526. package/src/zig/bench-utils.zig +0 -431
  527. package/src/zig/bench.zig +0 -223
  528. package/src/zig/buffer-methods.zig +0 -215
  529. package/src/zig/buffer.zig +0 -2515
  530. package/src/zig/build.zig +0 -700
  531. package/src/zig/build.zig.zon +0 -19
  532. package/src/zig/edit-buffer.zig +0 -900
  533. package/src/zig/editor-view.zig +0 -812
  534. package/src/zig/event-bus.zig +0 -28
  535. package/src/zig/event-emitter.zig +0 -65
  536. package/src/zig/file-logger.zig +0 -92
  537. package/src/zig/grapheme.zig +0 -838
  538. package/src/zig/handles.zig +0 -267
  539. package/src/zig/lib.zig +0 -2813
  540. package/src/zig/link.zig +0 -349
  541. package/src/zig/logger.zig +0 -43
  542. package/src/zig/mem-registry.zig +0 -126
  543. package/src/zig/miniaudio_shim.c +0 -21
  544. package/src/zig/native-renderable.zig +0 -112
  545. package/src/zig/native-span-feed-bench-lib.zig +0 -7
  546. package/src/zig/native-span-feed.zig +0 -747
  547. package/src/zig/renderer-output.zig +0 -677
  548. package/src/zig/renderer.zig +0 -2106
  549. package/src/zig/rope.zig +0 -1220
  550. package/src/zig/split-scrollback.zig +0 -88
  551. package/src/zig/syntax-style.zig +0 -174
  552. package/src/zig/terminal.zig +0 -1652
  553. package/src/zig/test.zig +0 -82
  554. package/src/zig/tests/README.md +0 -18
  555. package/src/zig/tests/ansi_test.zig +0 -18
  556. package/src/zig/tests/audio_test.zig +0 -439
  557. package/src/zig/tests/buffer-methods_test.zig +0 -1111
  558. package/src/zig/tests/buffer_test.zig +0 -2926
  559. package/src/zig/tests/edit-buffer-history_test.zig +0 -299
  560. package/src/zig/tests/edit-buffer_test.zig +0 -1688
  561. package/src/zig/tests/editor-view_test.zig +0 -3454
  562. package/src/zig/tests/event-emitter_test.zig +0 -249
  563. package/src/zig/tests/grapheme_test.zig +0 -1329
  564. package/src/zig/tests/handles_test.zig +0 -123
  565. package/src/zig/tests/link_test.zig +0 -220
  566. package/src/zig/tests/mem-registry_test.zig +0 -473
  567. package/src/zig/tests/memory_leak_regression_test.zig +0 -234
  568. package/src/zig/tests/native-renderable_test.zig +0 -16
  569. package/src/zig/tests/native-span-feed_test.zig +0 -1266
  570. package/src/zig/tests/renderer_test.zig +0 -2604
  571. package/src/zig/tests/rope-nested_test.zig +0 -712
  572. package/src/zig/tests/rope_fuzz_test.zig +0 -238
  573. package/src/zig/tests/rope_test.zig +0 -2362
  574. package/src/zig/tests/segment-merge.test.zig +0 -148
  575. package/src/zig/tests/split-scrollback_test.zig +0 -70
  576. package/src/zig/tests/syntax-style_test.zig +0 -558
  577. package/src/zig/tests/terminal_test.zig +0 -1489
  578. package/src/zig/tests/test-renderer.zig +0 -126
  579. package/src/zig/tests/text-buffer-drawing_test.zig +0 -3291
  580. package/src/zig/tests/text-buffer-highlights_test.zig +0 -706
  581. package/src/zig/tests/text-buffer-iterators_test.zig +0 -776
  582. package/src/zig/tests/text-buffer-segment_test.zig +0 -320
  583. package/src/zig/tests/text-buffer-selection_test.zig +0 -1056
  584. package/src/zig/tests/text-buffer-selection_viewport_test.zig +0 -358
  585. package/src/zig/tests/text-buffer-view_test.zig +0 -3672
  586. package/src/zig/tests/text-buffer_test.zig +0 -2191
  587. package/src/zig/tests/unicode-width-map.zon +0 -3909
  588. package/src/zig/tests/utf8_no_zwj_test.zig +0 -260
  589. package/src/zig/tests/utf8_test.zig +0 -4060
  590. package/src/zig/tests/utf8_wcwidth_cursor_test.zig +0 -267
  591. package/src/zig/tests/utf8_wcwidth_test.zig +0 -357
  592. package/src/zig/tests/word-wrap-editing_test.zig +0 -498
  593. package/src/zig/tests/wrap-cache-perf_test.zig +0 -113
  594. package/src/zig/tests/yoga_test.zig +0 -65
  595. package/src/zig/text-buffer-iterators.zig +0 -496
  596. package/src/zig/text-buffer-segment.zig +0 -403
  597. package/src/zig/text-buffer-view.zig +0 -1428
  598. package/src/zig/text-buffer.zig +0 -1272
  599. package/src/zig/utf8.zig +0 -1954
  600. package/src/zig/utils.zig +0 -12
  601. package/src/zig/uucode-stub.zig +0 -76
  602. package/src/zig/vendor/miniaudio/miniaudio.h +0 -93468
  603. package/src/zig/yoga.zig +0 -658
  604. package/src/zig-structs.ts +0 -381
  605. package/src/zig.ts +0 -5260
  606. package/tsconfig.build.json +0 -17
  607. package/tsconfig.json +0 -27
  608. package/tsconfig.node-test.json +0 -138
  609. /package/{src/lib/tree-sitter/assets → assets}/javascript/highlights.scm +0 -0
  610. /package/{src/lib/tree-sitter/assets → assets}/javascript/tree-sitter-javascript.wasm +0 -0
  611. /package/{src/lib/tree-sitter/assets → assets}/markdown/highlights.scm +0 -0
  612. /package/{src/lib/tree-sitter/assets → assets}/markdown/injections.scm +0 -0
  613. /package/{src/lib/tree-sitter/assets → assets}/markdown/tree-sitter-markdown.wasm +0 -0
  614. /package/{src/lib/tree-sitter/assets → assets}/markdown_inline/highlights.scm +0 -0
  615. /package/{src/lib/tree-sitter/assets → assets}/markdown_inline/tree-sitter-markdown_inline.wasm +0 -0
  616. /package/{src/lib/tree-sitter/assets → assets}/typescript/highlights.scm +0 -0
  617. /package/{src/lib/tree-sitter/assets → assets}/typescript/tree-sitter-typescript.wasm +0 -0
  618. /package/{src/lib/tree-sitter/assets → assets}/zig/highlights.scm +0 -0
  619. /package/{src/lib/tree-sitter/assets → assets}/zig/tree-sitter-zig.wasm +0 -0
@@ -1,1895 +0,0 @@
1
- import { test, expect, beforeEach, afterEach, describe, spyOn } from "bun:test"
2
- import { createTestRenderer, type TestRenderer, type MockMouse, MockTreeSitterClient } from "../testing.js"
3
- import { ScrollBoxRenderable } from "../renderables/ScrollBox.js"
4
- import { BoxRenderable } from "../renderables/Box.js"
5
- import { TextRenderable } from "../renderables/Text.js"
6
- import { CodeRenderable } from "../renderables/Code.js"
7
- import { MarkdownRenderable } from "../renderables/Markdown.js"
8
- import { LinearScrollAccel, MacOSScrollAccel, type ScrollAcceleration } from "../lib/scroll-acceleration.js"
9
- import { SyntaxStyle } from "../syntax-style.js"
10
-
11
- // Test accelerator that returns a constant multiplier
12
- class ConstantScrollAccel implements ScrollAcceleration {
13
- constructor(private multiplier: number) {}
14
- tick(_now?: number): number {
15
- return this.multiplier
16
- }
17
- reset(): void {}
18
- }
19
-
20
- let testRenderer: TestRenderer
21
- let mockMouse: MockMouse
22
- let renderOnce: () => Promise<void>
23
- let captureCharFrame: () => string
24
- let mockTreeSitterClient: MockTreeSitterClient
25
- let mockTreeSitterClients: MockTreeSitterClient[]
26
-
27
- beforeEach(async () => {
28
- ;({
29
- renderer: testRenderer,
30
- mockMouse,
31
- renderOnce,
32
- captureCharFrame,
33
- } = await createTestRenderer({ width: 80, height: 24 }))
34
- mockTreeSitterClients = []
35
- mockTreeSitterClient = createMockTreeSitterClient()
36
- })
37
-
38
- afterEach(async () => {
39
- testRenderer.destroy()
40
- for (const client of mockTreeSitterClients) {
41
- client.resolveAllHighlightOnce()
42
- await client.destroy()
43
- }
44
- })
45
-
46
- function createMockTreeSitterClient(): MockTreeSitterClient {
47
- const client = new MockTreeSitterClient()
48
- client.setMockResult({ highlights: [] })
49
- mockTreeSitterClients.push(client)
50
- return client
51
- }
52
-
53
- describe("ScrollBoxRenderable - child delegation", () => {
54
- test("delegates add to content wrapper", () => {
55
- const scrollbox = new ScrollBoxRenderable(testRenderer, { id: "scrollbox" })
56
- const child = new BoxRenderable(testRenderer, { id: "child" })
57
-
58
- scrollbox.add(child)
59
-
60
- const children = scrollbox.getChildren()
61
- expect(children.length).toBe(1)
62
- expect(children[0].id).toBe("child")
63
- expect(child.parent).toBe(scrollbox.content)
64
- })
65
-
66
- test("delegates remove to content wrapper", () => {
67
- const scrollbox = new ScrollBoxRenderable(testRenderer, { id: "scrollbox" })
68
- const child = new BoxRenderable(testRenderer, { id: "child" })
69
-
70
- scrollbox.add(child)
71
- expect(scrollbox.getChildren().length).toBe(1)
72
-
73
- scrollbox.remove(child)
74
- expect(scrollbox.getChildren().length).toBe(0)
75
- })
76
-
77
- test("delegates insertBefore to content wrapper", () => {
78
- const scrollbox = new ScrollBoxRenderable(testRenderer, { id: "scrollbox" })
79
- const child1 = new BoxRenderable(testRenderer, { id: "child1" })
80
- const child2 = new BoxRenderable(testRenderer, { id: "child2" })
81
- const child3 = new BoxRenderable(testRenderer, { id: "child3" })
82
-
83
- scrollbox.add(child1)
84
- scrollbox.add(child2)
85
- scrollbox.insertBefore(child3, child2)
86
-
87
- const children = scrollbox.getChildren()
88
- expect(children.length).toBe(3)
89
- expect(children[0].id).toBe("child1")
90
- expect(children[1].id).toBe("child3")
91
- expect(children[2].id).toBe("child2")
92
- })
93
-
94
- test("destroyRecursively fully detaches internal parts without warnings", () => {
95
- const scrollbox = new ScrollBoxRenderable(testRenderer, { id: "scrollbox" })
96
- const child = new BoxRenderable(testRenderer, { id: "child" })
97
- scrollbox.add(child)
98
-
99
- const wrapper = scrollbox.wrapper
100
- const verticalScrollBar = scrollbox.verticalScrollBar
101
-
102
- const warnSpy = spyOn(console, "warn").mockImplementation(() => {})
103
- try {
104
- scrollbox.destroyRecursively()
105
- expect(warnSpy).not.toHaveBeenCalled()
106
- } finally {
107
- warnSpy.mockRestore()
108
- }
109
-
110
- expect(wrapper.isDestroyed).toBe(true)
111
- expect(wrapper.parent).toBeNull()
112
- expect(verticalScrollBar.isDestroyed).toBe(true)
113
- expect(verticalScrollBar.parent).toBeNull()
114
- expect(child.isDestroyed).toBe(true)
115
- expect(child.parent).toBeNull()
116
- })
117
-
118
- test("removing an internal part by reference detaches it from its real parent", () => {
119
- const scrollbox = new ScrollBoxRenderable(testRenderer, { id: "scrollbox" })
120
-
121
- const warnSpy = spyOn(console, "warn").mockImplementation(() => {})
122
- try {
123
- scrollbox.remove(scrollbox.verticalScrollBar)
124
- expect(warnSpy).not.toHaveBeenCalled()
125
- } finally {
126
- warnSpy.mockRestore()
127
- }
128
-
129
- expect(scrollbox.verticalScrollBar.parent).toBeNull()
130
- })
131
- })
132
-
133
- describe("ScrollBoxRenderable - culled content layout freshness", () => {
134
- test("rows revealed after off-screen relayouts render at fresh positions", async () => {
135
- const scrollbox = new ScrollBoxRenderable(testRenderer, {
136
- id: "reveal-scrollbox",
137
- width: 30,
138
- height: 6,
139
- viewportCulling: true,
140
- })
141
- testRenderer.root.add(scrollbox)
142
-
143
- const rows: BoxRenderable[] = []
144
- for (let i = 0; i < 40; i++) {
145
- const row = new BoxRenderable(testRenderer, { id: `reveal-row-${i}`, height: 1, flexShrink: 0 })
146
- row.add(new TextRenderable(testRenderer, { content: `row-${i}` }))
147
- scrollbox.add(row)
148
- rows.push(row)
149
- }
150
- await renderOnce()
151
-
152
- scrollbox.scrollTo(scrollbox.scrollHeight)
153
- await renderOnce()
154
-
155
- // While the top rows are culled (their subtrees skipped by traversal),
156
- // relayout several times so any staleness in skipped subtrees would have
157
- // multiple chances to accumulate before the rows are revealed again.
158
- for (const rowHeight of [2, 3, 2]) {
159
- rows[0].height = rowHeight
160
- await renderOnce()
161
- }
162
-
163
- scrollbox.scrollTo(0)
164
- await renderOnce()
165
-
166
- // row-0 is now 2 tall, so the following rows must have shifted down and
167
- // their text subtrees must render at fresh absolute positions.
168
- const lines = captureCharFrame().split("\n")
169
- expect(lines[0]).toContain("row-0")
170
- expect(lines[2]).toContain("row-1")
171
- expect(lines[3]).toContain("row-2")
172
- expect(lines[4]).toContain("row-3")
173
- })
174
- })
175
-
176
- describe("ScrollBoxRenderable - clipping", () => {
177
- test("clips nested scrollbox content to inner viewport (see issue #388)", async () => {
178
- const root = new BoxRenderable(testRenderer, {
179
- flexDirection: "column",
180
- gap: 0,
181
- width: 32,
182
- height: 16,
183
- })
184
-
185
- const outer = new ScrollBoxRenderable(testRenderer, {
186
- width: 30,
187
- height: 10,
188
- border: true,
189
- overflow: "hidden",
190
- scrollY: true,
191
- })
192
-
193
- const inner = new ScrollBoxRenderable(testRenderer, {
194
- width: 26,
195
- height: 6,
196
- border: true,
197
- overflow: "hidden",
198
- scrollY: true,
199
- })
200
-
201
- for (let index = 0; index < 6; index += 1) {
202
- inner.add(new TextRenderable(testRenderer, { content: `LEAK-${index}` }))
203
- }
204
-
205
- outer.add(inner)
206
- root.add(outer)
207
- testRenderer.root.add(root)
208
-
209
- await renderOnce()
210
-
211
- const frame = captureCharFrame()
212
- const innerViewportHeight = 4 // height 6 minus top/bottom border
213
- const visibleLines = frame.split("\n").filter((line) => line.includes("LEAK-"))
214
-
215
- expect(visibleLines.length).toBeLessThanOrEqual(innerViewportHeight)
216
- })
217
- })
218
-
219
- describe("ScrollBoxRenderable - padding behavior", () => {
220
- test("applies scrollbox padding to content while keeping scrollbar docked", async () => {
221
- const noPadding = new ScrollBoxRenderable(testRenderer, {
222
- left: 0,
223
- top: 0,
224
- width: 26,
225
- height: 9,
226
- border: true,
227
- scrollY: true,
228
- })
229
-
230
- const padded = new ScrollBoxRenderable(testRenderer, {
231
- left: 0,
232
- top: 11,
233
- width: 26,
234
- height: 9,
235
- border: true,
236
- scrollY: true,
237
- padding: 2,
238
- })
239
-
240
- for (let index = 0; index < 24; index += 1) {
241
- noPadding.add(new TextRenderable(testRenderer, { content: `NP-${index}` }))
242
- padded.add(new TextRenderable(testRenderer, { content: `PD-${index}` }))
243
- }
244
-
245
- testRenderer.root.add(noPadding)
246
- testRenderer.root.add(padded)
247
-
248
- await renderOnce()
249
-
250
- expect(noPadding.verticalScrollBar.visible).toBe(true)
251
- expect(padded.verticalScrollBar.visible).toBe(true)
252
- expect(noPadding.verticalScrollBar.x).toBe(noPadding.x + noPadding.width - 2)
253
- expect(padded.verticalScrollBar.x).toBe(padded.x + padded.width - 2)
254
-
255
- const frameLines = captureCharFrame().split("\n")
256
- const noPaddingRow = frameLines.find((line) => line.includes("NP-0"))
257
- const paddedRow = frameLines.find((line) => line.includes("PD-0"))
258
-
259
- expect(noPaddingRow).toBeDefined()
260
- expect(paddedRow).toBeDefined()
261
-
262
- const noPaddingTextX = noPaddingRow?.indexOf("NP-0") ?? -1
263
- const paddedTextX = paddedRow?.indexOf("PD-0") ?? -1
264
-
265
- expect(paddedTextX).toBeGreaterThan(noPaddingTextX)
266
- })
267
-
268
- test("padding setter updates content inset without moving scrollbar", async () => {
269
- const scrollBox = new ScrollBoxRenderable(testRenderer, {
270
- width: 26,
271
- height: 9,
272
- border: true,
273
- scrollY: true,
274
- })
275
-
276
- for (let index = 0; index < 24; index += 1) {
277
- scrollBox.add(new TextRenderable(testRenderer, { content: `PX-${index}` }))
278
- }
279
-
280
- testRenderer.root.add(scrollBox)
281
- await renderOnce()
282
-
283
- const beforeScrollbarX = scrollBox.verticalScrollBar.x
284
- const beforeFrameLines = captureCharFrame().split("\n")
285
- const beforeRow = beforeFrameLines.find((line) => line.includes("PX-0"))
286
- const beforeTextX = beforeRow?.indexOf("PX-0") ?? -1
287
-
288
- scrollBox.padding = 2
289
- await renderOnce()
290
-
291
- const afterFrameLines = captureCharFrame().split("\n")
292
- const afterRow = afterFrameLines.find((line) => line.includes("PX-0"))
293
- const afterTextX = afterRow?.indexOf("PX-0") ?? -1
294
-
295
- expect(scrollBox.verticalScrollBar.x).toBe(beforeScrollbarX)
296
- expect(afterTextX).toBeGreaterThan(beforeTextX)
297
- })
298
- })
299
-
300
- describe("ScrollBoxRenderable - destroyRecursively", () => {
301
- test("destroys internal ScrollBox components", () => {
302
- const parent = new ScrollBoxRenderable(testRenderer, { id: "scroll-parent" })
303
- const child = new BoxRenderable(testRenderer, { id: "child" })
304
-
305
- parent.add(child)
306
-
307
- const wrapper = parent.wrapper
308
- const viewport = parent.viewport
309
- const content = parent.content
310
- const horizontalScrollBar = parent.horizontalScrollBar
311
- const verticalScrollBar = parent.verticalScrollBar
312
-
313
- expect(parent.isDestroyed).toBe(false)
314
- expect(child.isDestroyed).toBe(false)
315
- expect(wrapper.isDestroyed).toBe(false)
316
- expect(viewport.isDestroyed).toBe(false)
317
- expect(content.isDestroyed).toBe(false)
318
- expect(horizontalScrollBar.isDestroyed).toBe(false)
319
- expect(verticalScrollBar.isDestroyed).toBe(false)
320
-
321
- parent.destroyRecursively()
322
-
323
- expect(parent.isDestroyed).toBe(true)
324
- expect(child.isDestroyed).toBe(true)
325
- expect(wrapper.isDestroyed).toBe(true)
326
- expect(viewport.isDestroyed).toBe(true)
327
- expect(content.isDestroyed).toBe(true)
328
- expect(horizontalScrollBar.isDestroyed).toBe(true)
329
- expect(verticalScrollBar.isDestroyed).toBe(true)
330
- })
331
- })
332
-
333
- describe("ScrollBoxRenderable - Mouse interaction", () => {
334
- test("scrolls with mouse wheel", async () => {
335
- const scrollBox = new ScrollBoxRenderable(testRenderer, {
336
- width: 50,
337
- height: 20,
338
- scrollAcceleration: new MacOSScrollAccel({ A: 0 }),
339
- })
340
- for (let i = 0; i < 50; i++) scrollBox.add(new TextRenderable(testRenderer, { content: `Line ${i}` }))
341
- testRenderer.root.add(scrollBox)
342
- await renderOnce()
343
-
344
- await mockMouse.scroll(25, 10, "down")
345
- await renderOnce()
346
- expect(scrollBox.scrollTop).toBeGreaterThan(0)
347
- })
348
-
349
- test("single isolated scroll has same distance as linear", async () => {
350
- const linearBox = new ScrollBoxRenderable(testRenderer, {
351
- width: 50,
352
- height: 20,
353
- scrollAcceleration: new LinearScrollAccel(),
354
- })
355
-
356
- for (let i = 0; i < 100; i++) linearBox.add(new TextRenderable(testRenderer, { content: `Line ${i}` }))
357
- testRenderer.root.add(linearBox)
358
- await renderOnce()
359
-
360
- await mockMouse.scroll(25, 10, "down")
361
- await renderOnce()
362
- const linearDistance = linearBox.scrollTop
363
-
364
- testRenderer.destroy()
365
- ;({
366
- renderer: testRenderer,
367
- mockMouse,
368
- renderOnce,
369
- captureCharFrame,
370
- } = await createTestRenderer({ width: 80, height: 24 }))
371
-
372
- const accelBox = new ScrollBoxRenderable(testRenderer, {
373
- width: 50,
374
- height: 20,
375
- scrollAcceleration: new MacOSScrollAccel(),
376
- })
377
-
378
- for (let i = 0; i < 100; i++) accelBox.add(new TextRenderable(testRenderer, { content: `Line ${i}` }))
379
- testRenderer.root.add(accelBox)
380
- await renderOnce()
381
-
382
- await mockMouse.scroll(25, 10, "down")
383
- await renderOnce()
384
-
385
- expect(accelBox.scrollTop).toBe(linearDistance)
386
- })
387
-
388
- test("acceleration makes rapid scrolls cover more distance", async () => {
389
- const scrollBox = new ScrollBoxRenderable(testRenderer, {
390
- width: 50,
391
- height: 20,
392
- scrollAcceleration: new MacOSScrollAccel({ A: 0.8, tau: 3, maxMultiplier: 6 }),
393
- })
394
- for (let i = 0; i < 200; i++) scrollBox.add(new TextRenderable(testRenderer, { content: `Line ${i}` }))
395
- testRenderer.root.add(scrollBox)
396
- await renderOnce()
397
-
398
- await mockMouse.scroll(25, 10, "down")
399
- await renderOnce()
400
- const slowScrollDistance = scrollBox.scrollTop
401
-
402
- scrollBox.scrollTop = 0
403
-
404
- for (let i = 0; i < 5; i++) {
405
- await mockMouse.scroll(25, 10, "down")
406
- await renderOnce()
407
- }
408
- const rapidScrollDistance = scrollBox.scrollTop
409
-
410
- expect(rapidScrollDistance).toBeGreaterThan(slowScrollDistance * 3)
411
- })
412
-
413
- test("multiplier < 1 slows down scroll distance", async () => {
414
- // Test with slowdown using a constant multiplier < 1
415
- const slowdownBox = new ScrollBoxRenderable(testRenderer, {
416
- width: 50,
417
- height: 20,
418
- scrollAcceleration: new ConstantScrollAccel(0.5),
419
- })
420
- for (let i = 0; i < 200; i++) slowdownBox.add(new TextRenderable(testRenderer, { content: `Line ${i}` }))
421
- testRenderer.root.add(slowdownBox)
422
- await renderOnce()
423
-
424
- // ConstantScrollAccel ignores timing, so no delay needed
425
- for (let i = 0; i < 5; i++) {
426
- await mockMouse.scroll(25, 10, "down")
427
- await renderOnce()
428
- }
429
- const slowdownDistance = slowdownBox.scrollTop
430
-
431
- testRenderer.destroy()
432
- ;({
433
- renderer: testRenderer,
434
- mockMouse,
435
- renderOnce,
436
- captureCharFrame,
437
- } = await createTestRenderer({
438
- width: 80,
439
- height: 24,
440
- }))
441
-
442
- // Compare with linear (no slowdown)
443
- const linearBox = new ScrollBoxRenderable(testRenderer, {
444
- width: 50,
445
- height: 20,
446
- scrollAcceleration: new LinearScrollAccel(),
447
- })
448
- for (let i = 0; i < 200; i++) linearBox.add(new TextRenderable(testRenderer, { content: `Line ${i}` }))
449
- testRenderer.root.add(linearBox)
450
- await renderOnce()
451
-
452
- // LinearScrollAccel ignores timing, so no delay needed
453
- for (let i = 0; i < 5; i++) {
454
- await mockMouse.scroll(25, 10, "down")
455
- await renderOnce()
456
- }
457
- const linearDistance = linearBox.scrollTop
458
-
459
- expect(slowdownDistance).toBeLessThan(linearDistance)
460
- expect(slowdownDistance).toBeGreaterThan(0)
461
- })
462
-
463
- test("multiplier < 1 accumulates fractional scroll amounts", async () => {
464
- const scrollBox = new ScrollBoxRenderable(testRenderer, {
465
- width: 50,
466
- height: 20,
467
- scrollAcceleration: new ConstantScrollAccel(0.3),
468
- })
469
- for (let i = 0; i < 200; i++) scrollBox.add(new TextRenderable(testRenderer, { content: `Line ${i}` }))
470
- testRenderer.root.add(scrollBox)
471
- await renderOnce()
472
-
473
- // With multiplier < 1, fractional amounts accumulate
474
- // It should take multiple scroll events to accumulate enough to scroll 1 full unit
475
- let scrolled = false
476
- for (let i = 0; i < 5; i++) {
477
- await mockMouse.scroll(25, 10, "down")
478
- await renderOnce()
479
- if (scrollBox.scrollTop > 0) {
480
- scrolled = true
481
- break
482
- }
483
- }
484
-
485
- expect(scrolled).toBe(true)
486
- expect(scrollBox.scrollTop).toBeGreaterThan(0)
487
- })
488
-
489
- test("horizontal scroll with multiplier < 1 works correctly", async () => {
490
- const scrollBox = new ScrollBoxRenderable(testRenderer, {
491
- width: 50,
492
- height: 20,
493
- scrollX: true,
494
- scrollAcceleration: new ConstantScrollAccel(0.4),
495
- })
496
-
497
- const wideBox = new BoxRenderable(testRenderer, { width: 300, height: 10 })
498
- scrollBox.add(wideBox)
499
- testRenderer.root.add(scrollBox)
500
- await renderOnce()
501
-
502
- await mockMouse.scroll(25, 10, "right")
503
- await renderOnce()
504
-
505
- // Should eventually scroll after multiple events due to accumulation
506
- let scrolled = false
507
- for (let i = 0; i < 5; i++) {
508
- await mockMouse.scroll(25, 10, "right")
509
- await renderOnce()
510
- if (scrollBox.scrollLeft > 0) {
511
- scrolled = true
512
- break
513
- }
514
- }
515
-
516
- expect(scrolled).toBe(true)
517
- })
518
-
519
- test("multiplier < 1 with acceleration work together", async () => {
520
- const scrollBox = new ScrollBoxRenderable(testRenderer, {
521
- width: 50,
522
- height: 20,
523
- scrollAcceleration: new ConstantScrollAccel(0.3),
524
- })
525
- for (let i = 0; i < 200; i++) scrollBox.add(new TextRenderable(testRenderer, { content: `Line ${i}` }))
526
- testRenderer.root.add(scrollBox)
527
- await renderOnce()
528
-
529
- // Multiple scrolls should accumulate fractional amounts
530
- for (let i = 0; i < 10; i++) {
531
- await mockMouse.scroll(25, 10, "down")
532
- await renderOnce()
533
- }
534
- const scrollDistance = scrollBox.scrollTop
535
-
536
- // With 0.3 multiplier and 10 scrolls: 10 * 1 * 0.3 = 3 pixels total
537
- // Math.trunc applied each time, so we get 2 pixels actually scrolled
538
- expect(scrollDistance).toBeGreaterThan(0)
539
- expect(scrollDistance).toBeLessThan(5)
540
- })
541
- })
542
-
543
- describe("ScrollBoxRenderable - Content Visibility", () => {
544
- test("renders markdown prose in scrollbox before highlighting completes", async () => {
545
- const syntaxStyle = SyntaxStyle.fromTheme([])
546
- const scrollBox = new ScrollBoxRenderable(testRenderer, {
547
- width: 100,
548
- height: 20,
549
- viewportCulling: true,
550
- })
551
-
552
- testRenderer.root.add(scrollBox)
553
-
554
- for (let i = 0; i < 20; i++) {
555
- const wrapper = new BoxRenderable(testRenderer, {
556
- id: `text-${i}`,
557
- paddingLeft: 3,
558
- marginTop: 1,
559
- flexShrink: 0,
560
- })
561
- const markdown = new MarkdownRenderable(testRenderer, {
562
- syntaxStyle,
563
- streaming: true,
564
- internalBlockMode: "top-level",
565
- content: `PARAGRAPH_${i}\n\n\`\`\`ts\nconst CODE_${i} = ${i}\n\`\`\``,
566
- treeSitterClient: mockTreeSitterClient,
567
- })
568
- wrapper.add(markdown)
569
- scrollBox.add(wrapper)
570
- }
571
-
572
- await renderOnce()
573
-
574
- const frame = captureCharFrame()
575
- expect(frame).toContain("PARAGRAPH_0")
576
- })
577
-
578
- test("maintains visibility when scrolling with many Code elements", async () => {
579
- const syntaxStyle = SyntaxStyle.fromTheme([])
580
-
581
- const parent = new BoxRenderable(testRenderer, {
582
- flexDirection: "column",
583
- gap: 1,
584
- })
585
-
586
- const header = new BoxRenderable(testRenderer, { flexShrink: 0 })
587
- header.add(new TextRenderable(testRenderer, { content: "Header Content" }))
588
-
589
- const scrollBox = new ScrollBoxRenderable(testRenderer, {
590
- flexGrow: 1,
591
- stickyScroll: true,
592
- stickyStart: "bottom",
593
- })
594
-
595
- const footer = new BoxRenderable(testRenderer, { flexShrink: 0 })
596
- footer.add(new TextRenderable(testRenderer, { content: "Footer Content" }))
597
-
598
- parent.add(header)
599
- parent.add(scrollBox)
600
- parent.add(footer)
601
- testRenderer.root.add(parent)
602
-
603
- await renderOnce()
604
- const initialFrame = captureCharFrame()
605
- expect(initialFrame).toContain("Header Content")
606
- expect(initialFrame).toContain("Footer Content")
607
-
608
- const codeContent = `
609
- # HELLO
610
-
611
- world
612
-
613
- ## HELLO World
614
-
615
- \`\`\`html
616
- <div class="example">
617
- <p>Content</p>
618
- </div>
619
- \`\`\`
620
- `
621
-
622
- const codes: CodeRenderable[] = []
623
- for (let i = 0; i < 100; i++) {
624
- const wrapper = new BoxRenderable(testRenderer, {
625
- marginTop: 2,
626
- marginBottom: 2,
627
- })
628
- const code = new CodeRenderable(testRenderer, {
629
- content: codeContent,
630
- filetype: "markdown",
631
- syntaxStyle,
632
- drawUnstyledText: false,
633
- treeSitterClient: mockTreeSitterClient,
634
- })
635
- codes.push(code)
636
- wrapper.add(code)
637
- scrollBox.add(wrapper)
638
- }
639
-
640
- await renderOnce()
641
-
642
- mockTreeSitterClient.resolveAllHighlightOnce()
643
- await Promise.all(codes.map((c) => c.highlightingDone))
644
- await renderOnce()
645
-
646
- scrollBox.scrollTo(scrollBox.scrollHeight)
647
- await renderOnce()
648
-
649
- mockTreeSitterClient.resolveAllHighlightOnce()
650
- await Promise.all(codes.map((c) => c.highlightingDone))
651
- await renderOnce()
652
-
653
- const frameAfterScroll = captureCharFrame()
654
-
655
- expect(frameAfterScroll).toContain("Header Content")
656
- expect(frameAfterScroll).toContain("Footer Content")
657
-
658
- const hasCodeContent =
659
- frameAfterScroll.includes("HELLO") ||
660
- frameAfterScroll.includes("world") ||
661
- frameAfterScroll.includes("<div") ||
662
- frameAfterScroll.includes("```")
663
-
664
- expect(hasCodeContent).toBe(true)
665
-
666
- const nonWhitespaceChars = frameAfterScroll.replace(/\s/g, "").length
667
- expect(nonWhitespaceChars).toBeGreaterThan(50)
668
- })
669
-
670
- test("maintains visibility when scrolling with many Code elements (setter-based, like SolidJS)", async () => {
671
- const syntaxStyle = SyntaxStyle.fromTheme([])
672
-
673
- const parent = new BoxRenderable(testRenderer, {
674
- flexDirection: "column",
675
- gap: 1,
676
- })
677
-
678
- const header = new BoxRenderable(testRenderer, { flexShrink: 0 })
679
- header.add(new TextRenderable(testRenderer, { content: "Header Content" }))
680
-
681
- const scrollBox = new ScrollBoxRenderable(testRenderer, {
682
- flexGrow: 1,
683
- stickyScroll: true,
684
- stickyStart: "bottom",
685
- })
686
-
687
- const footer = new BoxRenderable(testRenderer, { flexShrink: 0 })
688
- footer.add(new TextRenderable(testRenderer, { content: "Footer Content" }))
689
-
690
- parent.add(header)
691
- parent.add(scrollBox)
692
- parent.add(footer)
693
- testRenderer.root.add(parent)
694
-
695
- await renderOnce()
696
- const initialFrame = captureCharFrame()
697
- expect(initialFrame).toContain("Header Content")
698
- expect(initialFrame).toContain("Footer Content")
699
-
700
- const codeContent = `
701
- # HELLO
702
-
703
- world
704
-
705
- ## HELLO World
706
-
707
- \`\`\`html
708
- <div class="example">
709
- <p>Content</p>
710
- </div>
711
- \`\`\`
712
- `
713
-
714
- for (let i = 0; i < 100; i++) {
715
- const wrapper = new BoxRenderable(testRenderer, { id: `wrapper-${i}` })
716
- wrapper.marginTop = 2
717
- wrapper.marginBottom = 2
718
-
719
- const code = new CodeRenderable(testRenderer, {
720
- id: `code-${i}`,
721
- syntaxStyle,
722
- drawUnstyledText: false,
723
- treeSitterClient: mockTreeSitterClient,
724
- })
725
-
726
- wrapper.add(code)
727
- code.content = codeContent
728
- code.filetype = "markdown"
729
-
730
- scrollBox.add(wrapper)
731
- }
732
-
733
- await testRenderer.idle()
734
-
735
- mockTreeSitterClient.resolveAllHighlightOnce()
736
- await Promise.resolve()
737
-
738
- await testRenderer.idle()
739
-
740
- scrollBox.scrollTo(scrollBox.scrollHeight)
741
- await testRenderer.idle()
742
-
743
- const frameAfterScroll = captureCharFrame()
744
-
745
- expect(frameAfterScroll).toContain("Header Content")
746
- expect(frameAfterScroll).toContain("Footer Content")
747
-
748
- const hasCodeContent =
749
- frameAfterScroll.includes("HELLO") ||
750
- frameAfterScroll.includes("world") ||
751
- frameAfterScroll.includes("<div") ||
752
- frameAfterScroll.includes("```")
753
-
754
- expect(hasCodeContent).toBe(true)
755
-
756
- const nonWhitespaceChars = frameAfterScroll.replace(/\s/g, "").length
757
- expect(nonWhitespaceChars).toBeGreaterThan(50)
758
- })
759
-
760
- test("maintains visibility with simple Code elements (constructor)", async () => {
761
- const syntaxStyle = SyntaxStyle.fromTheme([])
762
-
763
- const parent = new BoxRenderable(testRenderer, {
764
- flexDirection: "column",
765
- gap: 1,
766
- })
767
-
768
- const header = new BoxRenderable(testRenderer, { flexShrink: 0 })
769
- header.add(new TextRenderable(testRenderer, { content: "Header" }))
770
-
771
- const scrollBox = new ScrollBoxRenderable(testRenderer, {
772
- flexGrow: 1,
773
- stickyScroll: true,
774
- stickyStart: "bottom",
775
- })
776
-
777
- const footer = new BoxRenderable(testRenderer, { flexShrink: 0 })
778
- footer.add(new TextRenderable(testRenderer, { content: "Footer" }))
779
-
780
- parent.add(header)
781
- parent.add(scrollBox)
782
- parent.add(footer)
783
- testRenderer.root.add(parent)
784
-
785
- await renderOnce()
786
-
787
- const codes: CodeRenderable[] = []
788
- for (let i = 0; i < 50; i++) {
789
- const wrapper = new BoxRenderable(testRenderer, {
790
- marginTop: 1,
791
- marginBottom: 1,
792
- })
793
- const code = new CodeRenderable(testRenderer, {
794
- content: `Item ${i}`,
795
- filetype: "markdown",
796
- syntaxStyle,
797
- drawUnstyledText: false,
798
- treeSitterClient: mockTreeSitterClient,
799
- })
800
- codes.push(code)
801
- wrapper.add(code)
802
- scrollBox.add(wrapper)
803
- }
804
-
805
- await renderOnce()
806
-
807
- mockTreeSitterClient.resolveAllHighlightOnce()
808
- await Promise.all(codes.map((c) => c.highlightingDone))
809
- await renderOnce()
810
-
811
- scrollBox.scrollTo(scrollBox.scrollHeight)
812
- await renderOnce()
813
-
814
- mockTreeSitterClient.resolveAllHighlightOnce()
815
- await Promise.all(codes.map((c) => c.highlightingDone))
816
- await renderOnce()
817
-
818
- const frame = captureCharFrame()
819
-
820
- expect(frame).toContain("Header")
821
- expect(frame).toContain("Footer")
822
-
823
- const hasItems = /Item \d+/.test(frame)
824
- expect(hasItems).toBe(true)
825
-
826
- const nonWhitespaceChars = frame.replace(/\s/g, "").length
827
- expect(nonWhitespaceChars).toBeGreaterThan(18)
828
- })
829
-
830
- test("maintains visibility with simple Code elements (setter-based, like SolidJS)", async () => {
831
- const syntaxStyle = SyntaxStyle.fromTheme([])
832
-
833
- const parent = new BoxRenderable(testRenderer, {
834
- flexDirection: "column",
835
- gap: 1,
836
- })
837
-
838
- const header = new BoxRenderable(testRenderer, { flexShrink: 0 })
839
- header.add(new TextRenderable(testRenderer, { content: "Header" }))
840
-
841
- const scrollBox = new ScrollBoxRenderable(testRenderer, {
842
- flexGrow: 1,
843
- stickyScroll: true,
844
- stickyStart: "bottom",
845
- })
846
-
847
- const footer = new BoxRenderable(testRenderer, { flexShrink: 0 })
848
- footer.add(new TextRenderable(testRenderer, { content: "Footer" }))
849
-
850
- parent.add(header)
851
- parent.add(scrollBox)
852
- parent.add(footer)
853
- testRenderer.root.add(parent)
854
-
855
- await renderOnce()
856
-
857
- const codes: CodeRenderable[] = []
858
- for (let i = 0; i < 50; i++) {
859
- const wrapper = new BoxRenderable(testRenderer, { id: `wrapper-${i}` })
860
- wrapper.marginTop = 1
861
- wrapper.marginBottom = 1
862
-
863
- const code = new CodeRenderable(testRenderer, {
864
- id: `code-${i}`,
865
- syntaxStyle,
866
- drawUnstyledText: false,
867
- treeSitterClient: mockTreeSitterClient,
868
- })
869
-
870
- wrapper.add(code)
871
- code.content = `Item ${i}`
872
- code.filetype = "markdown"
873
- codes.push(code)
874
-
875
- scrollBox.add(wrapper)
876
- }
877
-
878
- await renderOnce()
879
- mockTreeSitterClient.resolveAllHighlightOnce()
880
- await Promise.all(codes.map((c) => c.highlightingDone))
881
- await renderOnce()
882
-
883
- scrollBox.scrollTo(scrollBox.scrollHeight)
884
- await renderOnce()
885
-
886
- const frame = captureCharFrame()
887
-
888
- expect(frame).toContain("Header")
889
- expect(frame).toContain("Footer")
890
-
891
- const hasItems = /Item \d+/.test(frame)
892
- expect(hasItems).toBe(true)
893
-
894
- const nonWhitespaceChars = frame.replace(/\s/g, "").length
895
- expect(nonWhitespaceChars).toBeGreaterThan(18)
896
- })
897
-
898
- test("maintains visibility with TextRenderable elements", async () => {
899
- const parent = new BoxRenderable(testRenderer, {
900
- flexDirection: "column",
901
- gap: 1,
902
- })
903
-
904
- const header = new BoxRenderable(testRenderer, { flexShrink: 0 })
905
- header.add(new TextRenderable(testRenderer, { content: "Header" }))
906
-
907
- const scrollBox = new ScrollBoxRenderable(testRenderer, {
908
- flexGrow: 1,
909
- stickyScroll: true,
910
- stickyStart: "bottom",
911
- })
912
-
913
- const footer = new BoxRenderable(testRenderer, { flexShrink: 0 })
914
- footer.add(new TextRenderable(testRenderer, { content: "Footer" }))
915
-
916
- parent.add(header)
917
- parent.add(scrollBox)
918
- parent.add(footer)
919
- testRenderer.root.add(parent)
920
-
921
- await renderOnce()
922
-
923
- for (let i = 0; i < 50; i++) {
924
- const wrapper = new BoxRenderable(testRenderer, {
925
- marginTop: 1,
926
- marginBottom: 1,
927
- })
928
- wrapper.add(new TextRenderable(testRenderer, { content: `Item ${i}` }))
929
- scrollBox.add(wrapper)
930
- }
931
-
932
- await renderOnce()
933
-
934
- scrollBox.scrollTo(scrollBox.scrollHeight)
935
- await renderOnce()
936
-
937
- const frame = captureCharFrame()
938
-
939
- expect(frame).toContain("Header")
940
- expect(frame).toContain("Footer")
941
-
942
- const hasItems = /Item \d+/.test(frame)
943
- expect(hasItems).toBe(true)
944
-
945
- const nonWhitespaceChars = frame.replace(/\s/g, "").length
946
- expect(nonWhitespaceChars).toBeGreaterThan(20)
947
- })
948
-
949
- test("stays scrolled to bottom with growing code renderables in sticky scroll mode", async () => {
950
- const syntaxStyle = SyntaxStyle.fromTheme([])
951
- // Use manual-resolving mock client for deterministic behavior
952
- const autoResolvingClient = createMockTreeSitterClient()
953
-
954
- const parent = new BoxRenderable(testRenderer, {
955
- flexDirection: "column",
956
- gap: 1,
957
- })
958
-
959
- const header = new BoxRenderable(testRenderer, { flexShrink: 0 })
960
- header.add(new TextRenderable(testRenderer, { content: "Header" }))
961
-
962
- const scrollBox = new ScrollBoxRenderable(testRenderer, {
963
- flexGrow: 1,
964
- stickyScroll: true,
965
- stickyStart: "bottom",
966
- })
967
-
968
- const footer = new BoxRenderable(testRenderer, { flexShrink: 0 })
969
- footer.add(new TextRenderable(testRenderer, { content: "Footer" }))
970
-
971
- parent.add(header)
972
- parent.add(scrollBox)
973
- parent.add(footer)
974
- testRenderer.root.add(parent)
975
-
976
- const scrollPositions: number[] = []
977
- const maxScrollPositions: number[] = []
978
- const wrapper1 = new BoxRenderable(testRenderer, {
979
- marginTop: 1,
980
- marginBottom: 1,
981
- })
982
- const code1 = new CodeRenderable(testRenderer, {
983
- content: "console.log('hello')",
984
- filetype: "javascript",
985
- syntaxStyle,
986
- drawUnstyledText: false,
987
- treeSitterClient: autoResolvingClient,
988
- })
989
- wrapper1.add(code1)
990
- scrollBox.add(wrapper1)
991
-
992
- await renderOnce()
993
- autoResolvingClient.resolveAllHighlightOnce()
994
- await code1.highlightingDone
995
- await renderOnce()
996
-
997
- scrollPositions.push(scrollBox.scrollTop)
998
- maxScrollPositions.push(Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height))
999
- expect(scrollBox.scrollTop).toBe(maxScrollPositions[0])
1000
-
1001
- code1.content = `console.log('hello')
1002
- const foo = 'bar'
1003
- const baz = 'qux'
1004
- function test() {
1005
- return 42
1006
- }
1007
- console.log(test())`
1008
-
1009
- await renderOnce()
1010
- autoResolvingClient.resolveAllHighlightOnce()
1011
- await code1.highlightingDone
1012
- await renderOnce()
1013
-
1014
- scrollPositions.push(scrollBox.scrollTop)
1015
- maxScrollPositions.push(Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height))
1016
- expect(scrollBox.scrollTop).toBe(maxScrollPositions[1])
1017
-
1018
- const wrapper2 = new BoxRenderable(testRenderer, {
1019
- marginTop: 1,
1020
- marginBottom: 1,
1021
- })
1022
- const code2 = new CodeRenderable(testRenderer, {
1023
- content: "const x = 10\nconst y = 20",
1024
- filetype: "javascript",
1025
- syntaxStyle,
1026
- drawUnstyledText: false,
1027
- treeSitterClient: autoResolvingClient,
1028
- })
1029
- wrapper2.add(code2)
1030
- scrollBox.add(wrapper2)
1031
-
1032
- await renderOnce()
1033
- autoResolvingClient.resolveAllHighlightOnce()
1034
- await code2.highlightingDone
1035
- await renderOnce()
1036
-
1037
- scrollPositions.push(scrollBox.scrollTop)
1038
- maxScrollPositions.push(Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height))
1039
- expect(scrollBox.scrollTop).toBe(maxScrollPositions[2])
1040
-
1041
- code2.content = `const x = 10
1042
- const y = 20
1043
- const z = x + y
1044
- console.log(z)
1045
- function multiply(a, b) {
1046
- return a * b
1047
- }
1048
- const result = multiply(x, y)
1049
- console.log('Result:', result)`
1050
-
1051
- await renderOnce()
1052
- autoResolvingClient.resolveAllHighlightOnce()
1053
- await code2.highlightingDone
1054
- await renderOnce()
1055
-
1056
- scrollPositions.push(scrollBox.scrollTop)
1057
- maxScrollPositions.push(Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height))
1058
- expect(scrollBox.scrollTop).toBe(maxScrollPositions[3])
1059
-
1060
- const wrapper3 = new BoxRenderable(testRenderer, {
1061
- marginTop: 1,
1062
- marginBottom: 1,
1063
- })
1064
- const code3 = new CodeRenderable(testRenderer, {
1065
- content: "// Final code block\nconst final = 'done'",
1066
- filetype: "javascript",
1067
- syntaxStyle,
1068
- drawUnstyledText: false,
1069
- treeSitterClient: autoResolvingClient,
1070
- })
1071
- wrapper3.add(code3)
1072
- scrollBox.add(wrapper3)
1073
-
1074
- await renderOnce()
1075
- autoResolvingClient.resolveAllHighlightOnce()
1076
- await code3.highlightingDone
1077
- await renderOnce()
1078
-
1079
- scrollPositions.push(scrollBox.scrollTop)
1080
- maxScrollPositions.push(Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height))
1081
- expect(scrollBox.scrollTop).toBe(maxScrollPositions[4])
1082
-
1083
- code3.content = `// Final code block
1084
- const final = 'done'
1085
-
1086
- class DataProcessor {
1087
- constructor(data) {
1088
- this.data = data
1089
- }
1090
-
1091
- process() {
1092
- return this.data.map(item => item * 2)
1093
- }
1094
-
1095
- filter(predicate) {
1096
- return this.data.filter(predicate)
1097
- }
1098
-
1099
- reduce(fn, initial) {
1100
- return this.data.reduce(fn, initial)
1101
- }
1102
- }
1103
-
1104
- const processor = new DataProcessor([1, 2, 3, 4, 5])
1105
- console.log(processor.process())
1106
- console.log(processor.filter(x => x > 2))
1107
- console.log(processor.reduce((acc, val) => acc + val, 0))`
1108
-
1109
- await renderOnce()
1110
- autoResolvingClient.resolveAllHighlightOnce()
1111
- await code3.highlightingDone
1112
- await renderOnce()
1113
-
1114
- scrollPositions.push(scrollBox.scrollTop)
1115
- maxScrollPositions.push(Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height))
1116
- expect(scrollBox.scrollTop).toBe(maxScrollPositions[5])
1117
-
1118
- const frame = captureCharFrame()
1119
- expect(frame).toContain("Header")
1120
- expect(frame).toContain("Footer")
1121
-
1122
- const hasCodeContent =
1123
- frame.includes("console") ||
1124
- frame.includes("function") ||
1125
- frame.includes("const") ||
1126
- frame.includes("DataProcessor") ||
1127
- frame.includes("processor")
1128
-
1129
- expect(hasCodeContent).toBe(true)
1130
-
1131
- const nonWhitespaceChars = frame.replace(/\s/g, "").length
1132
- expect(nonWhitespaceChars).toBeGreaterThan(50)
1133
-
1134
- for (let i = 0; i < scrollPositions.length; i++) {
1135
- expect(scrollPositions[i]).toBe(maxScrollPositions[i])
1136
- }
1137
- })
1138
-
1139
- test("sticky scroll bottom stays at bottom after scrollBy/scrollTo is called", async () => {
1140
- const scrollBox = new ScrollBoxRenderable(testRenderer, {
1141
- width: 40,
1142
- height: 10,
1143
- stickyScroll: true,
1144
- stickyStart: "bottom",
1145
- })
1146
-
1147
- testRenderer.root.add(scrollBox)
1148
- await renderOnce()
1149
-
1150
- scrollBox.add(new TextRenderable(testRenderer, { content: `Line 0` }))
1151
- await renderOnce()
1152
-
1153
- scrollBox.scrollBy(100000)
1154
- await renderOnce()
1155
-
1156
- scrollBox.scrollTo(scrollBox.scrollHeight)
1157
- await renderOnce()
1158
-
1159
- for (let i = 1; i < 30; i++) {
1160
- scrollBox.add(new TextRenderable(testRenderer, { content: `Line ${i}` }))
1161
- await renderOnce()
1162
-
1163
- const maxScroll = Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height)
1164
-
1165
- if (i === 16) {
1166
- expect(scrollBox.scrollTop).toBe(maxScroll)
1167
- }
1168
- }
1169
- })
1170
-
1171
- test("scrolls CodeRenderable with LineNumberRenderable using mouse wheel", async () => {
1172
- const syntaxStyle = SyntaxStyle.fromTheme([])
1173
-
1174
- const scrollBox = new ScrollBoxRenderable(testRenderer, {
1175
- width: 40,
1176
- height: 10,
1177
- scrollY: true,
1178
- scrollX: false,
1179
- })
1180
-
1181
- // Create long code content that needs scrolling
1182
- let code = "Line 1\n"
1183
- for (let i = 2; i <= 30; i++) {
1184
- code += `Line ${i}\n`
1185
- }
1186
-
1187
- const { LineNumberRenderable } = await import("../renderables/LineNumberRenderable.js")
1188
- const codeRenderable = new CodeRenderable(testRenderer, {
1189
- content: code,
1190
- filetype: "javascript",
1191
- syntaxStyle,
1192
- drawUnstyledText: true,
1193
- treeSitterClient: mockTreeSitterClient,
1194
- width: "100%",
1195
- })
1196
-
1197
- const codeWithLines = new LineNumberRenderable(testRenderer, {
1198
- target: codeRenderable,
1199
- width: "100%",
1200
- })
1201
-
1202
- scrollBox.add(codeWithLines)
1203
- testRenderer.root.add(scrollBox)
1204
-
1205
- await renderOnce()
1206
-
1207
- mockTreeSitterClient.resolveAllHighlightOnce()
1208
- await codeRenderable.highlightingDone
1209
- await renderOnce()
1210
-
1211
- // Capture initial frame (should show top lines)
1212
- const frameTop = captureCharFrame()
1213
- expect(frameTop).toContain("Line 1")
1214
- expect(frameTop).not.toContain("Line 30")
1215
-
1216
- // Scroll down to bottom
1217
- for (let i = 0; i < 25; i++) {
1218
- await mockMouse.scroll(20, 5, "down")
1219
- await renderOnce()
1220
- }
1221
-
1222
- // Capture after scroll (should show bottom lines)
1223
- const frameBottom = captureCharFrame()
1224
- expect(frameBottom).toMatchSnapshot()
1225
- expect(frameBottom).toContain("Line 30")
1226
- expect(frameBottom).not.toContain("Line 1")
1227
- })
1228
-
1229
- test("sticky scroll bottom stays at bottom when gradually filled with code renderables", async () => {
1230
- const syntaxStyle = SyntaxStyle.fromTheme([])
1231
-
1232
- const scrollBox = new ScrollBoxRenderable(testRenderer, {
1233
- width: 40,
1234
- height: 10,
1235
- stickyScroll: true,
1236
- stickyStart: "bottom",
1237
- })
1238
-
1239
- testRenderer.root.add(scrollBox)
1240
- await renderOnce()
1241
-
1242
- const scrollPositions: number[] = []
1243
- const maxScrollPositions: number[] = []
1244
-
1245
- scrollPositions.push(scrollBox.scrollTop)
1246
- maxScrollPositions.push(Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height))
1247
-
1248
- for (let i = 0; i < 10; i++) {
1249
- const code = new CodeRenderable(testRenderer, {
1250
- syntaxStyle,
1251
- drawUnstyledText: false,
1252
- treeSitterClient: mockTreeSitterClient,
1253
- })
1254
-
1255
- let content = `// Block ${i}\n`
1256
- for (let j = 0; j <= i; j++) {
1257
- content += `const var${j} = ${j}\n`
1258
- }
1259
- code.content = content
1260
- code.filetype = "javascript"
1261
-
1262
- scrollBox.add(code)
1263
-
1264
- await renderOnce()
1265
- mockTreeSitterClient.resolveAllHighlightOnce()
1266
- await code.highlightingDone
1267
- await renderOnce()
1268
-
1269
- const maxScroll = Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height)
1270
- scrollPositions.push(scrollBox.scrollTop)
1271
- maxScrollPositions.push(maxScroll)
1272
- }
1273
-
1274
- for (let i = 0; i < scrollPositions.length; i++) {
1275
- expect(scrollPositions[i]).toBe(maxScrollPositions[i])
1276
- }
1277
- })
1278
-
1279
- test("clips nested scrollboxes when multiple stacked children overflow (app-style tool blocks)", async () => {
1280
- const custom = await createTestRenderer({ width: 120, height: 40 })
1281
- const { renderer, renderOnce, captureCharFrame } = custom
1282
-
1283
- const root = new BoxRenderable(renderer, { flexDirection: "column", width: 118, height: 38, gap: 0 })
1284
- const header = new BoxRenderable(renderer, { height: 3, border: true })
1285
- header.add(new TextRenderable(testRenderer, { content: "HEADER" }))
1286
- root.add(header)
1287
-
1288
- const outer = new ScrollBoxRenderable(renderer, { height: 25, border: true, overflow: "hidden", scrollY: true })
1289
- expect((outer as any)._overflow).toBe("hidden")
1290
-
1291
- const addToolBlock = (id: number) => {
1292
- const wrapper = new BoxRenderable(renderer, { border: true, padding: 0, marginTop: 0, marginBottom: 0 })
1293
- const inner = new ScrollBoxRenderable(renderer, {
1294
- height: 10,
1295
- border: true,
1296
- overflow: "hidden",
1297
- scrollY: true,
1298
- contentOptions: { paddingTop: 0, paddingBottom: 0, paddingLeft: 0, paddingRight: 0 },
1299
- })
1300
- expect((inner as any)._overflow).toBe("hidden")
1301
- for (let i = 0; i < 15; i += 1) {
1302
- inner.add(new TextRenderable(renderer, { content: `[tool ${id}] line ${i}` }))
1303
- }
1304
- wrapper.add(inner)
1305
- outer.add(wrapper)
1306
- }
1307
-
1308
- addToolBlock(1)
1309
- addToolBlock(2)
1310
- addToolBlock(3)
1311
-
1312
- root.add(outer)
1313
-
1314
- const footer = new BoxRenderable(renderer, { height: 3, border: true })
1315
- footer.add(new TextRenderable(renderer, { content: "FOOTER" }))
1316
- root.add(footer)
1317
-
1318
- renderer.root.add(root)
1319
- await renderer.idle()
1320
- expect(outer.width).toBeGreaterThan(0)
1321
- expect(outer.height).toBeGreaterThan(0)
1322
-
1323
- const frame = captureCharFrame()
1324
-
1325
- // The third tool block should be clipped entirely (outer height fits ~two blocks).
1326
- expect(frame).not.toMatch(/\[tool 3\] line 1/)
1327
-
1328
- renderer.destroy()
1329
- })
1330
-
1331
- test("does not overdraw above header when scrolling nested tool blocks upward", async () => {
1332
- const custom = await createTestRenderer({ width: 120, height: 24 })
1333
- const { renderer, renderOnce, captureCharFrame } = custom
1334
-
1335
- const root = new BoxRenderable(renderer, { flexDirection: "column", width: 118, height: 22, gap: 0 })
1336
- const header = new BoxRenderable(renderer, { height: 3, border: true })
1337
- header.add(new TextRenderable(renderer, { content: "HEADER" }))
1338
- root.add(header)
1339
-
1340
- const outer = new ScrollBoxRenderable(renderer, { height: 14, border: true, overflow: "hidden", scrollY: true })
1341
- const inner = new ScrollBoxRenderable(renderer, { height: 10, border: true, overflow: "hidden", scrollY: true })
1342
- for (let i = 0; i < 12; i += 1) {
1343
- inner.add(new TextRenderable(renderer, { content: `[tool] line ${i}` }))
1344
- }
1345
- outer.add(inner)
1346
- root.add(outer)
1347
-
1348
- const footer = new BoxRenderable(renderer, { height: 3, border: true })
1349
- footer.add(new TextRenderable(renderer, { content: "FOOTER" }))
1350
- root.add(footer)
1351
-
1352
- renderer.root.add(root)
1353
- await renderOnce()
1354
-
1355
- // Scroll up to try to draw above header
1356
- inner.scrollTo({ x: 0, y: -100 })
1357
- outer.scrollTo({ x: 0, y: -100 })
1358
- await renderOnce()
1359
-
1360
- const frame = captureCharFrame()
1361
- const headerIndex = frame.indexOf("HEADER")
1362
- const firstToolIndex = frame.indexOf("[tool] line 0")
1363
-
1364
- expect(headerIndex).toBeGreaterThan(-1)
1365
- expect(firstToolIndex).toBeGreaterThan(headerIndex)
1366
-
1367
- renderer.destroy()
1368
- })
1369
-
1370
- // Regression test for issue #530: sticky scroll jumps to top after manual scroll
1371
- test("resets _hasManualScroll when user scrolls back to sticky position (issue #530)", async () => {
1372
- const scrollBox = new ScrollBoxRenderable(testRenderer, {
1373
- width: 40,
1374
- height: 10,
1375
- stickyScroll: true,
1376
- stickyStart: "bottom",
1377
- })
1378
-
1379
- testRenderer.root.add(scrollBox)
1380
-
1381
- // Add enough content to overflow the viewport
1382
- for (let i = 0; i < 20; i++) {
1383
- scrollBox.add(new TextRenderable(testRenderer, { content: `Line ${i}` }))
1384
- }
1385
- await renderOnce()
1386
-
1387
- const maxScroll = Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height)
1388
- expect(scrollBox.scrollTop).toBe(maxScroll)
1389
- expect((scrollBox as any)._hasManualScroll).toBe(false)
1390
-
1391
- // User scrolls up manually - this sets _hasManualScroll = true
1392
- scrollBox.scrollTo(5)
1393
- await renderOnce()
1394
-
1395
- expect(scrollBox.scrollTop).toBe(5)
1396
- expect((scrollBox as any)._hasManualScroll).toBe(true)
1397
-
1398
- // User scrolls back to bottom - this should reset _hasManualScroll = false
1399
- const newMaxScroll = Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height)
1400
- scrollBox.scrollTo(newMaxScroll)
1401
- await renderOnce()
1402
-
1403
- expect(scrollBox.scrollTop).toBe(newMaxScroll)
1404
- // This is the fix: _hasManualScroll should be reset when back at sticky position
1405
- expect((scrollBox as any)._hasManualScroll).toBe(false)
1406
-
1407
- // Add more content - should stay at bottom because sticky scroll is re-enabled
1408
- for (let i = 20; i < 30; i++) {
1409
- scrollBox.add(new TextRenderable(testRenderer, { content: `Line ${i}` }))
1410
- await renderOnce()
1411
-
1412
- const expectedMaxScroll = Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height)
1413
- // Without the fix, this would fail: scroll would jump to top
1414
- expect(scrollBox.scrollTop).toBe(expectedMaxScroll)
1415
- }
1416
- })
1417
-
1418
- // Regression test for issue #709: content size recalculation should not clear manual scroll state
1419
- test("does not reset _hasManualScroll during content size recalculation (issue #709)", async () => {
1420
- const scrollBox = new ScrollBoxRenderable(testRenderer, {
1421
- width: 40,
1422
- height: 10,
1423
- stickyScroll: true,
1424
- stickyStart: "bottom",
1425
- })
1426
-
1427
- testRenderer.root.add(scrollBox)
1428
-
1429
- for (let i = 0; i < 30; i++) {
1430
- scrollBox.add(new TextRenderable(testRenderer, { id: `line-${i}`, content: `Line ${i}` }))
1431
- }
1432
- await renderOnce()
1433
-
1434
- const initialMaxScroll = Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height)
1435
- expect(scrollBox.scrollTop).toBe(initialMaxScroll)
1436
- expect((scrollBox as any)._hasManualScroll).toBe(false)
1437
-
1438
- scrollBox.scrollTo(5)
1439
- await renderOnce()
1440
-
1441
- expect(scrollBox.scrollTop).toBe(5)
1442
- expect((scrollBox as any)._hasManualScroll).toBe(true)
1443
-
1444
- // Force a size recalculation that programmatically clamps scrollTop to 0.
1445
- // This must not be treated as a user returning to sticky position.
1446
- for (let i = 0; i < 28; i++) {
1447
- const line = scrollBox.getRenderable(`line-${i}`)
1448
- if (line) scrollBox.remove(line)
1449
- }
1450
- await renderOnce()
1451
-
1452
- expect(Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height)).toBe(0)
1453
- expect(scrollBox.scrollTop).toBe(0)
1454
- expect((scrollBox as any)._hasManualScroll).toBe(true)
1455
-
1456
- // When content grows again, we should keep manual-scroll mode and stay away from sticky bottom.
1457
- for (let i = 30; i < 50; i++) {
1458
- scrollBox.add(new TextRenderable(testRenderer, { id: `line-${i}`, content: `Line ${i}` }))
1459
- }
1460
- await renderOnce()
1461
-
1462
- const newMaxScroll = Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height)
1463
- expect(newMaxScroll).toBeGreaterThan(0)
1464
- expect((scrollBox as any)._hasManualScroll).toBe(true)
1465
- expect(scrollBox.scrollTop).toBe(0)
1466
- })
1467
-
1468
- // Regression test for issue #1087: recalculateBarProps else branch must not force scroll
1469
- // when _hasManualScroll and _stickyScrollBottom are both true (race during streaming)
1470
- test("recalculateBarProps does not force scroll when _hasManualScroll and _stickyScrollBottom are both true (issue #1087)", async () => {
1471
- const scrollBox = new ScrollBoxRenderable(testRenderer, {
1472
- width: 40,
1473
- height: 10,
1474
- stickyScroll: true,
1475
- stickyStart: "bottom",
1476
- })
1477
-
1478
- testRenderer.root.add(scrollBox)
1479
-
1480
- for (let i = 0; i < 30; i++) {
1481
- scrollBox.add(new TextRenderable(testRenderer, { id: `line-${i}`, content: `Line ${i}` }))
1482
- }
1483
- await renderOnce()
1484
-
1485
- // User scrolls up manually
1486
- scrollBox.scrollTo(5)
1487
- await renderOnce()
1488
-
1489
- expect(scrollBox.scrollTop).toBe(5)
1490
- expect((scrollBox as any)._hasManualScroll).toBe(true)
1491
-
1492
- // Simulate the race condition: _stickyScrollBottom is still true from before
1493
- // updateStickyState had a chance to clear it (state during streaming)
1494
- ;(scrollBox as any)._stickyScrollBottom = true
1495
-
1496
- // Add more content to trigger recalculateBarProps via onSizeChange
1497
- for (let i = 30; i < 50; i++) {
1498
- scrollBox.add(new TextRenderable(testRenderer, { id: `line-${i}`, content: `Line ${i}` }))
1499
- }
1500
- await renderOnce()
1501
-
1502
- // With the fix: scrollTop stays at 5 because _hasManualScroll guards the else branch
1503
- // Without the fix: scrollTop would be forced to newMaxScrollTop (bottom)
1504
- expect(scrollBox.scrollTop).toBe(5)
1505
- expect((scrollBox as any)._hasManualScroll).toBe(true)
1506
- })
1507
-
1508
- test("recalculateBarProps does not re-engage top sticky when manually scrolled to bottom during content growth", async () => {
1509
- const scrollBox = new ScrollBoxRenderable(testRenderer, {
1510
- width: 40,
1511
- height: 10,
1512
- stickyScroll: true,
1513
- stickyStart: "top",
1514
- })
1515
-
1516
- testRenderer.root.add(scrollBox)
1517
-
1518
- for (let i = 0; i < 30; i++) {
1519
- scrollBox.add(new TextRenderable(testRenderer, { id: `line-${i}`, content: `Line ${i}` }))
1520
- }
1521
- await renderOnce()
1522
-
1523
- const maxScroll = Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height)
1524
- scrollBox.scrollTo(maxScroll)
1525
- await renderOnce()
1526
-
1527
- expect(scrollBox.scrollTop).toBe(maxScroll)
1528
- expect((scrollBox as any)._hasManualScroll).toBe(true)
1529
-
1530
- scrollBox.add(new TextRenderable(testRenderer, { id: "line-30", content: "Line 30" }))
1531
- await renderOnce()
1532
-
1533
- expect(scrollBox.scrollTop).toBe(maxScroll)
1534
- expect((scrollBox as any)._hasManualScroll).toBe(true)
1535
- })
1536
-
1537
- // Regression test for issue #1087: re-engagement path resets _hasManualScroll when
1538
- // user scrolls back to bottom during streaming, allowing sticky to resume.
1539
- // NOTE: This test passes on pre-1088 code by accident - the else branch forces
1540
- // scroll to bottom which satisfies the final assertion. The fix adds the proper
1541
- // re-engagement path via _hasManualScroll reset instead of forced scrolling.
1542
- test("recalculateBarProps re-engages sticky when user scrolls to bottom during content growth (issue #1087)", async () => {
1543
- const scrollBox = new ScrollBoxRenderable(testRenderer, {
1544
- width: 40,
1545
- height: 10,
1546
- stickyScroll: true,
1547
- stickyStart: "bottom",
1548
- })
1549
-
1550
- testRenderer.root.add(scrollBox)
1551
-
1552
- for (let i = 0; i < 30; i++) {
1553
- scrollBox.add(new TextRenderable(testRenderer, { id: `line-${i}`, content: `Line ${i}` }))
1554
- }
1555
- await renderOnce()
1556
-
1557
- // User scrolls up manually
1558
- scrollBox.scrollTo(5)
1559
- await renderOnce()
1560
-
1561
- expect((scrollBox as any)._hasManualScroll).toBe(true)
1562
-
1563
- // Simulate the race condition: both flags true, user at bottom
1564
- ;(scrollBox as any)._stickyScrollBottom = true
1565
-
1566
- // Set scroll position to bottom directly (bypassing setter to keep _hasManualScroll true)
1567
- // When one line is added, newMaxScrollTop = oldMaxScrollTop + 1,
1568
- // so scrollTop = oldMaxScrollTop satisfies scrollTop >= newMaxScrollTop - 1
1569
- const maxScroll = Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height)
1570
- scrollBox.verticalScrollBar.scrollPosition = maxScroll
1571
- ;(scrollBox as any)._hasManualScroll = true
1572
-
1573
- // Add one line — triggers recalculateBarProps which sees scrollTop >= newMaxScrollTop - 1
1574
- scrollBox.add(new TextRenderable(testRenderer, { id: "line-50", content: "Line 50" }))
1575
- await renderOnce()
1576
-
1577
- // With the fix: _hasManualScroll resets to false, sticky re-engages
1578
- expect((scrollBox as any)._hasManualScroll).toBe(false)
1579
-
1580
- // More content should now follow to bottom because sticky is re-engaged
1581
- for (let i = 51; i < 60; i++) {
1582
- scrollBox.add(new TextRenderable(testRenderer, { id: `line-${i}`, content: `Line ${i}` }))
1583
- }
1584
- await renderOnce()
1585
-
1586
- const newMaxScroll = Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height)
1587
- expect(scrollBox.scrollTop).toBe(newMaxScroll)
1588
- })
1589
-
1590
- for (const stickyStart of ["top", "left", "right"] as const) {
1591
- test(`recalculateBarProps re-engages ${stickyStart} sticky when already back at the sticky edge during content growth`, async () => {
1592
- const isVertical = stickyStart === "top"
1593
- const scrollBox = new ScrollBoxRenderable(testRenderer, {
1594
- width: isVertical ? 40 : 20,
1595
- height: isVertical ? 10 : 5,
1596
- scrollX: !isVertical,
1597
- scrollY: isVertical,
1598
- stickyScroll: true,
1599
- stickyStart,
1600
- ...(isVertical
1601
- ? {}
1602
- : {
1603
- contentOptions: {
1604
- flexDirection: "row",
1605
- },
1606
- }),
1607
- })
1608
-
1609
- testRenderer.root.add(scrollBox)
1610
-
1611
- if (isVertical) {
1612
- for (let i = 0; i < 30; i++) {
1613
- scrollBox.add(new TextRenderable(testRenderer, { id: `line-${i}`, content: `Line ${i}` }))
1614
- }
1615
- } else {
1616
- for (let i = 0; i < 10; i++) {
1617
- scrollBox.add(new BoxRenderable(testRenderer, { id: `box-${i}`, width: 10, height: 2 }))
1618
- }
1619
- }
1620
- await renderOnce()
1621
-
1622
- const initialMaxScrollTop = Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height)
1623
- const initialMaxScrollLeft = Math.max(0, scrollBox.scrollWidth - scrollBox.viewport.width)
1624
-
1625
- if (stickyStart === "top") {
1626
- scrollBox.scrollTo(initialMaxScrollTop)
1627
- await renderOnce()
1628
- scrollBox.verticalScrollBar.scrollPosition = 0
1629
- } else if (stickyStart === "left") {
1630
- scrollBox.scrollTo({ x: initialMaxScrollLeft, y: 0 })
1631
- await renderOnce()
1632
- scrollBox.horizontalScrollBar.scrollPosition = 0
1633
- } else {
1634
- scrollBox.scrollTo({ x: 5, y: 0 })
1635
- await renderOnce()
1636
- scrollBox.horizontalScrollBar.scrollPosition = initialMaxScrollLeft
1637
- }
1638
-
1639
- ;(scrollBox as any)._hasManualScroll = true
1640
-
1641
- if (isVertical) {
1642
- scrollBox.add(new TextRenderable(testRenderer, { id: "line-new", content: "Line new" }))
1643
- } else {
1644
- scrollBox.add(new BoxRenderable(testRenderer, { id: "box-new", width: 1, height: 2 }))
1645
- }
1646
- await renderOnce()
1647
-
1648
- const newMaxScrollLeft = Math.max(0, scrollBox.scrollWidth - scrollBox.viewport.width)
1649
-
1650
- expect((scrollBox as any)._hasManualScroll).toBe(false)
1651
- if (stickyStart === "right") {
1652
- expect(scrollBox.scrollLeft).toBe(newMaxScrollLeft)
1653
- } else if (stickyStart === "left") {
1654
- expect(scrollBox.scrollLeft).toBe(0)
1655
- } else {
1656
- expect(scrollBox.scrollTop).toBe(0)
1657
- }
1658
- })
1659
- }
1660
-
1661
- test("scrollChildIntoView does nothing when child is already visible", async () => {
1662
- const scrollBox = new ScrollBoxRenderable(testRenderer, {
1663
- width: 40,
1664
- height: 10,
1665
- })
1666
-
1667
- for (let i = 0; i < 10; i++) {
1668
- scrollBox.add(new BoxRenderable(testRenderer, { id: `child-${i}`, height: 1 }))
1669
- }
1670
- testRenderer.root.add(scrollBox)
1671
- await renderOnce()
1672
-
1673
- expect(scrollBox.scrollTop).toBe(0)
1674
-
1675
- scrollBox.scrollChildIntoView("child-2")
1676
- expect(scrollBox.scrollTop).toBe(0)
1677
- })
1678
-
1679
- test("scrollChildIntoView scrolls down to reveal child below viewport", async () => {
1680
- const scrollBox = new ScrollBoxRenderable(testRenderer, {
1681
- width: 40,
1682
- height: 10,
1683
- })
1684
-
1685
- for (let i = 0; i < 30; i++) {
1686
- scrollBox.add(new BoxRenderable(testRenderer, { id: `child-${i}`, height: 1 }))
1687
- }
1688
- testRenderer.root.add(scrollBox)
1689
- await renderOnce()
1690
-
1691
- scrollBox.scrollChildIntoView("child-25")
1692
- await renderOnce()
1693
-
1694
- const child = scrollBox.content.findDescendantById("child-25")!
1695
- expect(child.y).toBeGreaterThanOrEqual(scrollBox.viewport.y)
1696
- expect(child.y + child.height).toBe(scrollBox.viewport.y + scrollBox.viewport.height)
1697
- })
1698
-
1699
- test("scrollChildIntoView scrolls up to reveal child above viewport", async () => {
1700
- const scrollBox = new ScrollBoxRenderable(testRenderer, {
1701
- width: 40,
1702
- height: 10,
1703
- })
1704
-
1705
- for (let i = 0; i < 30; i++) {
1706
- scrollBox.add(new BoxRenderable(testRenderer, { id: `child-${i}`, height: 1 }))
1707
- }
1708
- testRenderer.root.add(scrollBox)
1709
- await renderOnce()
1710
-
1711
- scrollBox.scrollTo(20)
1712
- await renderOnce()
1713
-
1714
- scrollBox.scrollChildIntoView("child-15")
1715
- await renderOnce()
1716
-
1717
- const child = scrollBox.content.findDescendantById("child-15")!
1718
- expect(child.y).toBe(scrollBox.viewport.y)
1719
- expect(child.y + child.height).toBeLessThanOrEqual(scrollBox.viewport.y + scrollBox.viewport.height)
1720
- })
1721
-
1722
- test("scrollChildIntoView finds nested descendants by id", async () => {
1723
- const scrollBox = new ScrollBoxRenderable(testRenderer, {
1724
- width: 40,
1725
- height: 10,
1726
- })
1727
-
1728
- for (let i = 0; i < 30; i++) {
1729
- const wrapper = new BoxRenderable(testRenderer, { id: `wrapper-${i}`, height: 1 })
1730
- wrapper.add(new BoxRenderable(testRenderer, { id: `nested-${i}`, height: 1 }))
1731
- scrollBox.add(wrapper)
1732
- }
1733
- testRenderer.root.add(scrollBox)
1734
- await renderOnce()
1735
-
1736
- scrollBox.scrollChildIntoView("nested-25")
1737
- await renderOnce()
1738
-
1739
- const child = scrollBox.content.findDescendantById("nested-25")!
1740
- expect(child.y).toBeGreaterThanOrEqual(scrollBox.viewport.y)
1741
- expect(child.y + child.height).toBeLessThanOrEqual(scrollBox.viewport.y + scrollBox.viewport.height)
1742
- })
1743
-
1744
- test("scrollChildIntoView does nothing for nonexistent child", async () => {
1745
- const scrollBox = new ScrollBoxRenderable(testRenderer, {
1746
- width: 40,
1747
- height: 10,
1748
- })
1749
-
1750
- for (let i = 0; i < 20; i++) {
1751
- scrollBox.add(new BoxRenderable(testRenderer, { id: `child-${i}`, height: 1 }))
1752
- }
1753
- testRenderer.root.add(scrollBox)
1754
- await renderOnce()
1755
-
1756
- scrollBox.scrollTo(5)
1757
- await renderOnce()
1758
-
1759
- const scrollTopBefore = scrollBox.scrollTop
1760
- scrollBox.scrollChildIntoView("does-not-exist")
1761
- expect(scrollBox.scrollTop).toBe(scrollTopBefore)
1762
- })
1763
-
1764
- test("scrollChildIntoView handles horizontal scrolling", async () => {
1765
- const scrollBox = new ScrollBoxRenderable(testRenderer, {
1766
- width: 20,
1767
- height: 10,
1768
- scrollX: true,
1769
- contentOptions: {
1770
- flexDirection: "row",
1771
- },
1772
- })
1773
-
1774
- for (let i = 0; i < 6; i++) {
1775
- scrollBox.add(new BoxRenderable(testRenderer, { id: `child-${i}`, width: 10, height: 2 }))
1776
- }
1777
-
1778
- testRenderer.root.add(scrollBox)
1779
- await renderOnce()
1780
-
1781
- scrollBox.scrollLeft = 30
1782
- await renderOnce()
1783
-
1784
- scrollBox.scrollChildIntoView("child-0")
1785
- await renderOnce()
1786
-
1787
- const child = scrollBox.content.findDescendantById("child-0")!
1788
- expect(child.x).toBe(scrollBox.viewport.x)
1789
- expect(child.x + child.width).toBeLessThanOrEqual(scrollBox.viewport.x + scrollBox.viewport.width)
1790
- })
1791
-
1792
- test("scrollChildIntoView follows nearest behavior for oversized children", async () => {
1793
- const scrollBox = new ScrollBoxRenderable(testRenderer, {
1794
- width: 40,
1795
- height: 10,
1796
- })
1797
-
1798
- scrollBox.add(new BoxRenderable(testRenderer, { id: "oversized", height: 30 }))
1799
- testRenderer.root.add(scrollBox)
1800
- await renderOnce()
1801
-
1802
- scrollBox.scrollChildIntoView("oversized")
1803
- expect(scrollBox.scrollTop).toBe(0)
1804
-
1805
- scrollBox.scrollTo(10)
1806
- await renderOnce()
1807
-
1808
- scrollBox.scrollChildIntoView("oversized")
1809
- expect(scrollBox.scrollTop).toBe(10)
1810
- })
1811
-
1812
- // Regression test for issue #530: edge case when content fits in viewport
1813
- test("resets _hasManualScroll for stickyStart=bottom when content fits in viewport (issue #530)", async () => {
1814
- const scrollBox = new ScrollBoxRenderable(testRenderer, {
1815
- width: 40,
1816
- height: 20,
1817
- stickyScroll: true,
1818
- stickyStart: "bottom",
1819
- })
1820
-
1821
- testRenderer.root.add(scrollBox)
1822
-
1823
- // Add content that fits in viewport (no actual scrolling needed)
1824
- scrollBox.add(new TextRenderable(testRenderer, { content: "Line 0" }))
1825
- scrollBox.add(new TextRenderable(testRenderer, { content: "Line 1" }))
1826
- await renderOnce()
1827
-
1828
- // maxScrollTop should be effectively zero since content fits; ScrollBox treats <= 1 as layout noise.
1829
- const maxScroll = Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height)
1830
- expect(maxScroll).toBeLessThanOrEqual(1)
1831
-
1832
- // Simulate accidental scroll attempts (common with trackpads)
1833
- scrollBox.scrollTo(0)
1834
- await renderOnce()
1835
-
1836
- // Even though we're at scrollTop=0, for stickyStart="bottom" with maxScrollTop=0,
1837
- // we're effectively at both top AND bottom, so _hasManualScroll should be false
1838
- expect((scrollBox as any)._hasManualScroll).toBe(false)
1839
-
1840
- // Add more content that causes overflow - should stay at bottom
1841
- for (let i = 2; i < 40; i++) {
1842
- scrollBox.add(new TextRenderable(testRenderer, { content: `Line ${i}` }))
1843
- await renderOnce()
1844
-
1845
- const expectedMaxScroll = Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height)
1846
- if (expectedMaxScroll > 0) {
1847
- expect(scrollBox.scrollTop).toBe(expectedMaxScroll)
1848
- }
1849
- }
1850
- })
1851
-
1852
- test("updateStickyState resets _hasManualScroll when reaching bottom even during concurrent sticky operations", async () => {
1853
- const scrollBox = new ScrollBoxRenderable(testRenderer, {
1854
- width: 40,
1855
- height: 10,
1856
- stickyScroll: true,
1857
- stickyStart: "bottom",
1858
- })
1859
-
1860
- testRenderer.root.add(scrollBox)
1861
-
1862
- // Add enough content to make scrolling possible
1863
- for (let i = 0; i < 30; i++) {
1864
- scrollBox.add(new TextRenderable(testRenderer, { id: `line-${i}`, content: `Line ${i}` }))
1865
- }
1866
- await renderOnce()
1867
-
1868
- const maxScrollTop = Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height)
1869
-
1870
- // Simulate: user scrolled to bottom while concurrent sticky operation was running
1871
- scrollBox.verticalScrollBar.scrollPosition = maxScrollTop
1872
- ;(scrollBox as any)._hasManualScroll = true
1873
- ;(scrollBox as any)._isApplyingStickyScroll = true
1874
-
1875
- // Trigger updateStickyState (called when scrollTop changes)
1876
- const updateStickyState = (scrollBox as any).updateStickyState.bind(scrollBox)
1877
- updateStickyState()
1878
-
1879
- // With fix: flag should reset even though _isApplyingStickyScroll was true
1880
- expect((scrollBox as any)._hasManualScroll).toBe(false)
1881
- expect((scrollBox as any)._stickyScrollBottom).toBe(true)
1882
-
1883
- // Clear the flag for subsequent behavior check
1884
- ;(scrollBox as any)._isApplyingStickyScroll = false
1885
-
1886
- // Adding more content should now cause viewport to stay locked to bottom
1887
- for (let i = 30; i < 40; i++) {
1888
- scrollBox.add(new TextRenderable(testRenderer, { id: `line-${i}`, content: `Line ${i}` }))
1889
- }
1890
- await renderOnce()
1891
-
1892
- const newMaxScrollTop = Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height)
1893
- expect(scrollBox.scrollTop).toBe(newMaxScrollTop)
1894
- })
1895
- })