@xincli/opentui-core 0.4.6

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 (476) hide show
  1. package/README.md +63 -0
  2. package/dev/keypress-debug-renderer.ts +148 -0
  3. package/dev/keypress-debug.ts +43 -0
  4. package/dev/print-env-vars.ts +32 -0
  5. package/dev/test-tmux-graphics-334.sh +68 -0
  6. package/dev/test-tmux-theme-queries.sh +151 -0
  7. package/dev/thai-debug-test.ts +68 -0
  8. package/dev/theme-mode.ts +290 -0
  9. package/docs/development.md +144 -0
  10. package/package.json +102 -0
  11. package/prebuilt/aarch64-android/libopentui.so +0 -0
  12. package/scripts/build-android.ts +226 -0
  13. package/scripts/build-native-termux.sh +525 -0
  14. package/scripts/build.ts +530 -0
  15. package/scripts/dist-test.ts +312 -0
  16. package/scripts/package-prebuilt.ts +110 -0
  17. package/scripts/publish.ts +60 -0
  18. package/scripts/test-node-hook.mjs +16 -0
  19. package/scripts/test-node.ts +241 -0
  20. package/scripts/vendor-deps.sh +73 -0
  21. package/src/NativeSpanFeed.ts +375 -0
  22. package/src/Renderable.ts +1878 -0
  23. package/src/__snapshots__/buffer.test.ts.snap +28 -0
  24. package/src/animation/Timeline.test.ts +2709 -0
  25. package/src/animation/Timeline.ts +598 -0
  26. package/src/ansi.ts +22 -0
  27. package/src/audio.ts +479 -0
  28. package/src/benchmark/attenuation-benchmark.ts +81 -0
  29. package/src/benchmark/audio-playback-benchmark.ts +344 -0
  30. package/src/benchmark/box-draw-benchmark.ts +1042 -0
  31. package/src/benchmark/colormatrix-benchmark.ts +123 -0
  32. package/src/benchmark/gain-benchmark.ts +80 -0
  33. package/src/benchmark/latest-all-bench-run.json +707 -0
  34. package/src/benchmark/latest-async-bench-run.json +336 -0
  35. package/src/benchmark/latest-default-bench-run.json +657 -0
  36. package/src/benchmark/latest-large-bench-run.json +707 -0
  37. package/src/benchmark/latest-quick-bench-run.json +207 -0
  38. package/src/benchmark/layout-benchmark.ts +2547 -0
  39. package/src/benchmark/markdown-benchmark.ts +1796 -0
  40. package/src/benchmark/native-span-feed-async-benchmark.ts +355 -0
  41. package/src/benchmark/native-span-feed-benchmark.md +56 -0
  42. package/src/benchmark/native-span-feed-benchmark.ts +596 -0
  43. package/src/benchmark/native-span-feed-compare.ts +280 -0
  44. package/src/benchmark/render-traversal-benchmark.ts +928 -0
  45. package/src/benchmark/text-buffer-render-benchmark.ts +874 -0
  46. package/src/benchmark/text-table-benchmark.ts +948 -0
  47. package/src/buffer.test.ts +292 -0
  48. package/src/buffer.ts +564 -0
  49. package/src/console.test.ts +629 -0
  50. package/src/console.ts +1253 -0
  51. package/src/edit-buffer.test.ts +1852 -0
  52. package/src/edit-buffer.ts +417 -0
  53. package/src/editor-view.test.ts +1056 -0
  54. package/src/editor-view.ts +290 -0
  55. package/src/index.ts +25 -0
  56. package/src/lib/KeyHandler.integration.test.ts +292 -0
  57. package/src/lib/KeyHandler.stopPropagation.test.ts +289 -0
  58. package/src/lib/KeyHandler.test.ts +662 -0
  59. package/src/lib/KeyHandler.ts +222 -0
  60. package/src/lib/RGBA.test.ts +60 -0
  61. package/src/lib/RGBA.ts +343 -0
  62. package/src/lib/ascii.font.ts +330 -0
  63. package/src/lib/border.test.ts +83 -0
  64. package/src/lib/border.ts +170 -0
  65. package/src/lib/bunfs.test.ts +27 -0
  66. package/src/lib/bunfs.ts +18 -0
  67. package/src/lib/clipboard.test.ts +63 -0
  68. package/src/lib/clipboard.ts +41 -0
  69. package/src/lib/clock.ts +35 -0
  70. package/src/lib/data-paths.test.ts +133 -0
  71. package/src/lib/data-paths.ts +109 -0
  72. package/src/lib/debounce.ts +106 -0
  73. package/src/lib/detect-links.test.ts +98 -0
  74. package/src/lib/detect-links.ts +56 -0
  75. package/src/lib/env.test.ts +228 -0
  76. package/src/lib/env.ts +209 -0
  77. package/src/lib/extmarks-history.ts +51 -0
  78. package/src/lib/extmarks-multiwidth.test.ts +324 -0
  79. package/src/lib/extmarks.test.ts +3547 -0
  80. package/src/lib/extmarks.ts +861 -0
  81. package/src/lib/fonts/block.json +405 -0
  82. package/src/lib/fonts/grid.json +265 -0
  83. package/src/lib/fonts/huge.json +741 -0
  84. package/src/lib/fonts/pallet.json +314 -0
  85. package/src/lib/fonts/shade.json +591 -0
  86. package/src/lib/fonts/slick.json +321 -0
  87. package/src/lib/fonts/tiny.json +69 -0
  88. package/src/lib/hast-styled-text.ts +59 -0
  89. package/src/lib/index.ts +22 -0
  90. package/src/lib/keybinding.internal.test.ts +342 -0
  91. package/src/lib/keybinding.internal.ts +182 -0
  92. package/src/lib/objects-in-viewport.test.ts +789 -0
  93. package/src/lib/objects-in-viewport.ts +153 -0
  94. package/src/lib/output.capture.ts +58 -0
  95. package/src/lib/parse.keypress-kitty.protocol.test.ts +347 -0
  96. package/src/lib/parse.keypress-kitty.test.ts +712 -0
  97. package/src/lib/parse.keypress-kitty.ts +470 -0
  98. package/src/lib/parse.keypress.test.ts +2020 -0
  99. package/src/lib/parse.keypress.ts +480 -0
  100. package/src/lib/parse.mouse.test.ts +552 -0
  101. package/src/lib/parse.mouse.ts +232 -0
  102. package/src/lib/paste.ts +18 -0
  103. package/src/lib/queue.ts +65 -0
  104. package/src/lib/render-geometry.ts +36 -0
  105. package/src/lib/renderable.validations.test.ts +87 -0
  106. package/src/lib/renderable.validations.ts +83 -0
  107. package/src/lib/scroll-acceleration.ts +98 -0
  108. package/src/lib/selection.ts +261 -0
  109. package/src/lib/singleton.ts +34 -0
  110. package/src/lib/stdin-parser.test.ts +2439 -0
  111. package/src/lib/stdin-parser.ts +2007 -0
  112. package/src/lib/styled-text.ts +178 -0
  113. package/src/lib/terminal-capability-detection.test.ts +226 -0
  114. package/src/lib/terminal-capability-detection.ts +98 -0
  115. package/src/lib/terminal-palette.test.ts +954 -0
  116. package/src/lib/terminal-palette.ts +441 -0
  117. package/src/lib/tree-sitter/assets/README.md +119 -0
  118. package/src/lib/tree-sitter/assets/javascript/highlights.scm +205 -0
  119. package/src/lib/tree-sitter/assets/javascript/tree-sitter-javascript.wasm +0 -0
  120. package/src/lib/tree-sitter/assets/markdown/highlights.scm +150 -0
  121. package/src/lib/tree-sitter/assets/markdown/injections.scm +27 -0
  122. package/src/lib/tree-sitter/assets/markdown/tree-sitter-markdown.wasm +0 -0
  123. package/src/lib/tree-sitter/assets/markdown_inline/highlights.scm +115 -0
  124. package/src/lib/tree-sitter/assets/markdown_inline/tree-sitter-markdown_inline.wasm +0 -0
  125. package/src/lib/tree-sitter/assets/typescript/highlights.scm +604 -0
  126. package/src/lib/tree-sitter/assets/typescript/tree-sitter-typescript.wasm +0 -0
  127. package/src/lib/tree-sitter/assets/update.ts +360 -0
  128. package/src/lib/tree-sitter/assets/zig/highlights.scm +284 -0
  129. package/src/lib/tree-sitter/assets/zig/tree-sitter-zig.wasm +0 -0
  130. package/src/lib/tree-sitter/assets.d.ts +9 -0
  131. package/src/lib/tree-sitter/cache.test.ts +291 -0
  132. package/src/lib/tree-sitter/client.test.ts +1434 -0
  133. package/src/lib/tree-sitter/client.ts +847 -0
  134. package/src/lib/tree-sitter/default-parsers.ts +133 -0
  135. package/src/lib/tree-sitter/download-utils.ts +148 -0
  136. package/src/lib/tree-sitter/index.ts +75 -0
  137. package/src/lib/tree-sitter/parser.worker.ts +1098 -0
  138. package/src/lib/tree-sitter/parsers-config.ts +81 -0
  139. package/src/lib/tree-sitter/resolve-ft.test.ts +55 -0
  140. package/src/lib/tree-sitter/resolve-ft.ts +189 -0
  141. package/src/lib/tree-sitter/types.ts +132 -0
  142. package/src/lib/tree-sitter/update-assets.ts +10 -0
  143. package/src/lib/tree-sitter-styled-text.test.ts +1253 -0
  144. package/src/lib/tree-sitter-styled-text.ts +314 -0
  145. package/src/lib/validate-dir-name.ts +55 -0
  146. package/src/lib/yoga.options.test.ts +628 -0
  147. package/src/lib/yoga.options.ts +346 -0
  148. package/src/native-event-worker-repro.test.ts +48 -0
  149. package/src/native-event-worker-repro.worker.ts +9 -0
  150. package/src/native-handle.test.ts +221 -0
  151. package/src/platform/ffi.test.ts +516 -0
  152. package/src/platform/ffi.ts +653 -0
  153. package/src/platform/runtime.test.ts +52 -0
  154. package/src/platform/runtime.ts +121 -0
  155. package/src/platform/test.ts +45 -0
  156. package/src/platform/worker-handler-cleanup.fixture.ts +12 -0
  157. package/src/platform/worker-onmessage-startup.fixture.ts +10 -0
  158. package/src/platform/worker-startup.fixture.ts +9 -0
  159. package/src/platform/worker.node-test.ts +153 -0
  160. package/src/platform/worker.ts +425 -0
  161. package/src/plugins/core-slot.ts +579 -0
  162. package/src/plugins/registry.ts +402 -0
  163. package/src/plugins/types.ts +46 -0
  164. package/src/post/effects.ts +948 -0
  165. package/src/post/filters.ts +512 -0
  166. package/src/post/matrices.ts +288 -0
  167. package/src/renderables/ASCIIFont.ts +219 -0
  168. package/src/renderables/Box.test.ts +474 -0
  169. package/src/renderables/Box.ts +353 -0
  170. package/src/renderables/Code.test.ts +2388 -0
  171. package/src/renderables/Code.ts +555 -0
  172. package/src/renderables/Diff.regression.test.ts +381 -0
  173. package/src/renderables/Diff.test.ts +3212 -0
  174. package/src/renderables/Diff.ts +1278 -0
  175. package/src/renderables/EditBufferRenderable.test.ts +288 -0
  176. package/src/renderables/EditBufferRenderable.ts +1152 -0
  177. package/src/renderables/FrameBuffer.ts +47 -0
  178. package/src/renderables/Input.test.ts +1310 -0
  179. package/src/renderables/Input.ts +273 -0
  180. package/src/renderables/LineNumberRenderable.ts +725 -0
  181. package/src/renderables/Markdown.ts +2144 -0
  182. package/src/renderables/ScrollBar.ts +423 -0
  183. package/src/renderables/ScrollBox.ts +903 -0
  184. package/src/renderables/Select.test.ts +1134 -0
  185. package/src/renderables/Select.ts +535 -0
  186. package/src/renderables/Slider.test.ts +456 -0
  187. package/src/renderables/Slider.ts +342 -0
  188. package/src/renderables/TabSelect.test.ts +238 -0
  189. package/src/renderables/TabSelect.ts +447 -0
  190. package/src/renderables/Text.selection-buffer.test.ts +118 -0
  191. package/src/renderables/Text.test.ts +2662 -0
  192. package/src/renderables/Text.ts +147 -0
  193. package/src/renderables/TextBufferRenderable.ts +504 -0
  194. package/src/renderables/TextNode.test.ts +1137 -0
  195. package/src/renderables/TextNode.ts +379 -0
  196. package/src/renderables/TextTable.test.ts +1411 -0
  197. package/src/renderables/TextTable.ts +1434 -0
  198. package/src/renderables/Textarea.ts +425 -0
  199. package/src/renderables/TimeToFirstDraw.ts +89 -0
  200. package/src/renderables/__snapshots__/Code.test.ts.snap +13 -0
  201. package/src/renderables/__snapshots__/Diff.test.ts.snap +785 -0
  202. package/src/renderables/__snapshots__/Text.test.ts.snap +421 -0
  203. package/src/renderables/__snapshots__/TextTable.test.ts.snap +215 -0
  204. package/src/renderables/__tests__/Code.test.ts +55 -0
  205. package/src/renderables/__tests__/LineNumberRenderable.scrollbox-simple.test.ts +144 -0
  206. package/src/renderables/__tests__/LineNumberRenderable.scrollbox.test.ts +827 -0
  207. package/src/renderables/__tests__/LineNumberRenderable.test.ts +1906 -0
  208. package/src/renderables/__tests__/LineNumberRenderable.wrapping.test.ts +85 -0
  209. package/src/renderables/__tests__/Markdown.code-colors.test.ts +288 -0
  210. package/src/renderables/__tests__/Markdown.test.ts +4175 -0
  211. package/src/renderables/__tests__/MultiRenderable.selection.test.ts +87 -0
  212. package/src/renderables/__tests__/Textarea.buffer.test.ts +683 -0
  213. package/src/renderables/__tests__/Textarea.destroyed-events.test.ts +675 -0
  214. package/src/renderables/__tests__/Textarea.editing.test.ts +2120 -0
  215. package/src/renderables/__tests__/Textarea.error-handling.test.ts +35 -0
  216. package/src/renderables/__tests__/Textarea.events.test.ts +738 -0
  217. package/src/renderables/__tests__/Textarea.highlights.test.ts +590 -0
  218. package/src/renderables/__tests__/Textarea.keybinding.test.ts +3275 -0
  219. package/src/renderables/__tests__/Textarea.paste.test.ts +357 -0
  220. package/src/renderables/__tests__/Textarea.rendering.test.ts +1927 -0
  221. package/src/renderables/__tests__/Textarea.scroll.test.ts +733 -0
  222. package/src/renderables/__tests__/Textarea.selection.test.ts +1589 -0
  223. package/src/renderables/__tests__/Textarea.stress.test.ts +670 -0
  224. package/src/renderables/__tests__/Textarea.undo-redo.test.ts +383 -0
  225. package/src/renderables/__tests__/Textarea.visual-lines.test.ts +310 -0
  226. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.code.test.ts.snap +221 -0
  227. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox-simple.test.ts.snap +89 -0
  228. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox.test.ts.snap +457 -0
  229. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.test.ts.snap +158 -0
  230. package/src/renderables/__tests__/__snapshots__/Textarea.rendering.test.ts.snap +387 -0
  231. package/src/renderables/__tests__/markdown-parser.test.ts +241 -0
  232. package/src/renderables/__tests__/renderable-test-utils.ts +60 -0
  233. package/src/renderables/composition/README.md +8 -0
  234. package/src/renderables/composition/VRenderable.ts +32 -0
  235. package/src/renderables/composition/constructs.ts +127 -0
  236. package/src/renderables/composition/vnode.ts +277 -0
  237. package/src/renderables/index.ts +23 -0
  238. package/src/renderables/markdown-parser.ts +79 -0
  239. package/src/renderer-theme-mode.ts +199 -0
  240. package/src/renderer.ts +4975 -0
  241. package/src/runtime-plugin-support-configure.ts +63 -0
  242. package/src/runtime-plugin-support.ts +13 -0
  243. package/src/runtime-plugin.ts +626 -0
  244. package/src/specs/terminal-startup.md +64 -0
  245. package/src/syntax-style.test.ts +841 -0
  246. package/src/syntax-style.ts +274 -0
  247. package/src/testing/README.md +219 -0
  248. package/src/testing/bun-test-node.ts +880 -0
  249. package/src/testing/capture-spans.test.ts +216 -0
  250. package/src/testing/integration.test.ts +276 -0
  251. package/src/testing/manual-clock.ts +117 -0
  252. package/src/testing/mock-keys.test.ts +1378 -0
  253. package/src/testing/mock-keys.ts +457 -0
  254. package/src/testing/mock-mouse.test.ts +218 -0
  255. package/src/testing/mock-mouse.ts +247 -0
  256. package/src/testing/mock-tree-sitter-client.ts +81 -0
  257. package/src/testing/spy.ts +13 -0
  258. package/src/testing/terminal-capabilities.ts +47 -0
  259. package/src/testing/test-recorder.test.ts +410 -0
  260. package/src/testing/test-recorder.ts +133 -0
  261. package/src/testing/test-renderer.ts +375 -0
  262. package/src/testing/test-renderer.wait.test.ts +278 -0
  263. package/src/testing/test-streams.ts +31 -0
  264. package/src/testing.ts +9 -0
  265. package/src/tests/__snapshots__/absolute-positioning.snapshot.test.ts.snap +481 -0
  266. package/src/tests/__snapshots__/renderable.snapshot.test.ts.snap +19 -0
  267. package/src/tests/__snapshots__/scrollbox.test.ts.snap +29 -0
  268. package/src/tests/absolute-positioning.snapshot.test.ts +638 -0
  269. package/src/tests/allocator-stats.test.ts +53 -0
  270. package/src/tests/audio.test.ts +303 -0
  271. package/src/tests/destroy-during-render.test.ts +200 -0
  272. package/src/tests/destroy-on-exit.fixture.ts +36 -0
  273. package/src/tests/destroy-on-exit.test.ts +62 -0
  274. package/src/tests/ffi-borrowed-pointer-callsites.test.ts +153 -0
  275. package/src/tests/hover-cursor.test.ts +98 -0
  276. package/src/tests/native-backed-measurement-lifecycle.test.ts +133 -0
  277. package/src/tests/native-backed-measurement-parity.test.ts +322 -0
  278. package/src/tests/native-span-feed-async.test.ts +173 -0
  279. package/src/tests/native-span-feed-close.test.ts +120 -0
  280. package/src/tests/native-span-feed-coverage.test.ts +227 -0
  281. package/src/tests/native-span-feed-edge-cases.test.ts +384 -0
  282. package/src/tests/native-span-feed-use-after-free.test.ts +45 -0
  283. package/src/tests/opacity.test.ts +123 -0
  284. package/src/tests/renderable.snapshot.test.ts +524 -0
  285. package/src/tests/renderable.test.ts +1851 -0
  286. package/src/tests/renderer.clock.test.ts +270 -0
  287. package/src/tests/renderer.console-startup.test.ts +2151 -0
  288. package/src/tests/renderer.control.test.ts +458 -0
  289. package/src/tests/renderer.core-slot-binding.test.ts +962 -0
  290. package/src/tests/renderer.cursor.test.ts +26 -0
  291. package/src/tests/renderer.custom-stdout.test.ts +1112 -0
  292. package/src/tests/renderer.destroy-during-render.test.ts +147 -0
  293. package/src/tests/renderer.focus-restore.test.ts +257 -0
  294. package/src/tests/renderer.focus.test.ts +329 -0
  295. package/src/tests/renderer.idle.test.ts +219 -0
  296. package/src/tests/renderer.input.test.ts +2615 -0
  297. package/src/tests/renderer.kitty-flags.test.ts +195 -0
  298. package/src/tests/renderer.lifecycle.test.ts +34 -0
  299. package/src/tests/renderer.mouse.test.ts +1315 -0
  300. package/src/tests/renderer.notifications.test.ts +27 -0
  301. package/src/tests/renderer.palette.test.ts +1199 -0
  302. package/src/tests/renderer.remote.test.ts +247 -0
  303. package/src/tests/renderer.render-stats.test.ts +146 -0
  304. package/src/tests/renderer.scrollback-surface.test.ts +474 -0
  305. package/src/tests/renderer.selection.test.ts +138 -0
  306. package/src/tests/renderer.slot-registry.test.ts +684 -0
  307. package/src/tests/renderer.tracker.test.ts +202 -0
  308. package/src/tests/renderer.useMouse.test.ts +83 -0
  309. package/src/tests/runtime-plugin-node-modules-cycle.fixture.ts +76 -0
  310. package/src/tests/runtime-plugin-node-modules-import-like-string.fixture.ts +48 -0
  311. package/src/tests/runtime-plugin-node-modules-mjs.fixture.ts +43 -0
  312. package/src/tests/runtime-plugin-node-modules-no-bare-rewrite.fixture.ts +67 -0
  313. package/src/tests/runtime-plugin-node-modules-package-type-cache.fixture.ts +72 -0
  314. package/src/tests/runtime-plugin-node-modules-runtime-specifier.fixture.ts +44 -0
  315. package/src/tests/runtime-plugin-node-modules-scoped-package-bare-rewrite.fixture.ts +85 -0
  316. package/src/tests/runtime-plugin-path-alias.fixture.ts +43 -0
  317. package/src/tests/runtime-plugin-resolve-roots.fixture.ts +65 -0
  318. package/src/tests/runtime-plugin-support-configure.fixture.ts +31 -0
  319. package/src/tests/runtime-plugin-support-late-addition.fixture.ts +17 -0
  320. package/src/tests/runtime-plugin-support.fixture.ts +11 -0
  321. package/src/tests/runtime-plugin-support.test.ts +54 -0
  322. package/src/tests/runtime-plugin-windows-file-url.fixture.ts +30 -0
  323. package/src/tests/runtime-plugin.fixture.ts +40 -0
  324. package/src/tests/runtime-plugin.test.ts +369 -0
  325. package/src/tests/scrollbox-culling-bug.test.ts +114 -0
  326. package/src/tests/scrollbox-hitgrid-resize.test.ts +136 -0
  327. package/src/tests/scrollbox-hitgrid.test.ts +909 -0
  328. package/src/tests/scrollbox.test.ts +1895 -0
  329. package/src/tests/wrap-resize-perf.test.ts +276 -0
  330. package/src/tests/yoga-api-coverage.test.ts +409 -0
  331. package/src/tests/yoga-callback-stress.test.ts +122 -0
  332. package/src/tests/yoga-ffi.test.ts +117 -0
  333. package/src/tests/yoga-setters.test.ts +921 -0
  334. package/src/tests/yoga-upstream/YGAlignBaselineTest.test.ts +124 -0
  335. package/src/tests/yoga-upstream/YGComputedBorderTest.test.ts +27 -0
  336. package/src/tests/yoga-upstream/YGComputedMarginTest.test.ts +27 -0
  337. package/src/tests/yoga-upstream/YGComputedPaddingTest.test.ts +27 -0
  338. package/src/tests/yoga-upstream/YGDirtiedTest.test.ts +157 -0
  339. package/src/tests/yoga-upstream/YGErrataTest.test.ts +40 -0
  340. package/src/tests/yoga-upstream/YGFlexBasisAuto.test.ts +23 -0
  341. package/src/tests/yoga-upstream/YGHasNewLayout.test.ts +81 -0
  342. package/src/tests/yoga-upstream/YGMeasureCacheTest.test.ts +31 -0
  343. package/src/tests/yoga-upstream/YGMeasureTest.test.ts +70 -0
  344. package/src/tests/yoga-upstream/generated/YGAbsolutePositionTest.test.ts +1864 -0
  345. package/src/tests/yoga-upstream/generated/YGAlignContentTest.test.ts +5829 -0
  346. package/src/tests/yoga-upstream/generated/YGAlignItemsTest.test.ts +2444 -0
  347. package/src/tests/yoga-upstream/generated/YGAlignSelfTest.test.ts +293 -0
  348. package/src/tests/yoga-upstream/generated/YGAndroidNewsFeed.test.ts +315 -0
  349. package/src/tests/yoga-upstream/generated/YGAspectRatioTest.test.ts +231 -0
  350. package/src/tests/yoga-upstream/generated/YGAutoTest.test.ts +347 -0
  351. package/src/tests/yoga-upstream/generated/YGBorderTest.test.ts +256 -0
  352. package/src/tests/yoga-upstream/generated/YGBoxSizingTest.test.ts +2747 -0
  353. package/src/tests/yoga-upstream/generated/YGDimensionTest.test.ts +129 -0
  354. package/src/tests/yoga-upstream/generated/YGDisplayContentsTest.test.ts +109 -0
  355. package/src/tests/yoga-upstream/generated/YGDisplayTest.test.ts +1097 -0
  356. package/src/tests/yoga-upstream/generated/YGFlexDirectionTest.test.ts +4562 -0
  357. package/src/tests/yoga-upstream/generated/YGFlexTest.test.ts +677 -0
  358. package/src/tests/yoga-upstream/generated/YGFlexWrapTest.test.ts +2071 -0
  359. package/src/tests/yoga-upstream/generated/YGGapTest.test.ts +3303 -0
  360. package/src/tests/yoga-upstream/generated/YGIntrinsicSizeTest.test.ts +582 -0
  361. package/src/tests/yoga-upstream/generated/YGJustifyContentTest.test.ts +2150 -0
  362. package/src/tests/yoga-upstream/generated/YGMarginTest.test.ts +1963 -0
  363. package/src/tests/yoga-upstream/generated/YGMinMaxDimensionTest.test.ts +1446 -0
  364. package/src/tests/yoga-upstream/generated/YGPaddingTest.test.ts +354 -0
  365. package/src/tests/yoga-upstream/generated/YGPercentageTest.test.ts +1760 -0
  366. package/src/tests/yoga-upstream/generated/YGRoundingTest.test.ts +1161 -0
  367. package/src/tests/yoga-upstream/generated/YGSizeOverflowTest.test.ts +207 -0
  368. package/src/tests/yoga-upstream/generated/YGStaticPositionTest.test.ts +6338 -0
  369. package/src/tests/yoga-upstream/tools/MeasureCounter.ts +54 -0
  370. package/src/tests/yoga-upstream/tools/utils.ts +81 -0
  371. package/src/text-buffer-view.test.ts +722 -0
  372. package/src/text-buffer-view.ts +193 -0
  373. package/src/text-buffer.test.ts +512 -0
  374. package/src/text-buffer.ts +250 -0
  375. package/src/types.ts +199 -0
  376. package/src/utils.ts +88 -0
  377. package/src/yoga.ts +1146 -0
  378. package/src/zig/ansi.zig +502 -0
  379. package/src/zig/audio.zig +1206 -0
  380. package/src/zig/bench/README.md +50 -0
  381. package/src/zig/bench/buffer-color-blending_bench.zig +350 -0
  382. package/src/zig/bench/buffer-draw-box_bench.zig +786 -0
  383. package/src/zig/bench/buffer-draw-text-buffer_bench.zig +894 -0
  384. package/src/zig/bench/edit-buffer_bench.zig +476 -0
  385. package/src/zig/bench/editor-view_bench.zig +169 -0
  386. package/src/zig/bench/native-span-feed_bench.zig +107 -0
  387. package/src/zig/bench/rope-markers_bench.zig +713 -0
  388. package/src/zig/bench/rope_bench.zig +514 -0
  389. package/src/zig/bench/styled-text_bench.zig +476 -0
  390. package/src/zig/bench/text-buffer-coords_bench.zig +362 -0
  391. package/src/zig/bench/text-buffer-view_bench.zig +459 -0
  392. package/src/zig/bench/text-chunk-graphemes_bench.zig +272 -0
  393. package/src/zig/bench/utf8_bench.zig +799 -0
  394. package/src/zig/bench-utils.zig +431 -0
  395. package/src/zig/bench.zig +223 -0
  396. package/src/zig/buffer-methods.zig +215 -0
  397. package/src/zig/buffer.zig +2515 -0
  398. package/src/zig/build.zig +700 -0
  399. package/src/zig/build.zig.zon +19 -0
  400. package/src/zig/edit-buffer.zig +900 -0
  401. package/src/zig/editor-view.zig +812 -0
  402. package/src/zig/event-bus.zig +28 -0
  403. package/src/zig/event-emitter.zig +65 -0
  404. package/src/zig/file-logger.zig +92 -0
  405. package/src/zig/grapheme.zig +838 -0
  406. package/src/zig/handles.zig +267 -0
  407. package/src/zig/lib.zig +2813 -0
  408. package/src/zig/link.zig +349 -0
  409. package/src/zig/logger.zig +43 -0
  410. package/src/zig/mem-registry.zig +126 -0
  411. package/src/zig/miniaudio_shim.c +21 -0
  412. package/src/zig/native-renderable.zig +112 -0
  413. package/src/zig/native-span-feed-bench-lib.zig +7 -0
  414. package/src/zig/native-span-feed.zig +747 -0
  415. package/src/zig/renderer-output.zig +677 -0
  416. package/src/zig/renderer.zig +2106 -0
  417. package/src/zig/rope.zig +1220 -0
  418. package/src/zig/split-scrollback.zig +88 -0
  419. package/src/zig/syntax-style.zig +174 -0
  420. package/src/zig/terminal.zig +1652 -0
  421. package/src/zig/test.zig +82 -0
  422. package/src/zig/tests/README.md +18 -0
  423. package/src/zig/tests/ansi_test.zig +18 -0
  424. package/src/zig/tests/audio_test.zig +439 -0
  425. package/src/zig/tests/buffer-methods_test.zig +1111 -0
  426. package/src/zig/tests/buffer_test.zig +2926 -0
  427. package/src/zig/tests/edit-buffer-history_test.zig +299 -0
  428. package/src/zig/tests/edit-buffer_test.zig +1688 -0
  429. package/src/zig/tests/editor-view_test.zig +3454 -0
  430. package/src/zig/tests/event-emitter_test.zig +249 -0
  431. package/src/zig/tests/grapheme_test.zig +1329 -0
  432. package/src/zig/tests/handles_test.zig +123 -0
  433. package/src/zig/tests/link_test.zig +220 -0
  434. package/src/zig/tests/mem-registry_test.zig +473 -0
  435. package/src/zig/tests/memory_leak_regression_test.zig +234 -0
  436. package/src/zig/tests/native-renderable_test.zig +16 -0
  437. package/src/zig/tests/native-span-feed_test.zig +1266 -0
  438. package/src/zig/tests/renderer_test.zig +2604 -0
  439. package/src/zig/tests/rope-nested_test.zig +712 -0
  440. package/src/zig/tests/rope_fuzz_test.zig +238 -0
  441. package/src/zig/tests/rope_test.zig +2362 -0
  442. package/src/zig/tests/segment-merge.test.zig +148 -0
  443. package/src/zig/tests/split-scrollback_test.zig +70 -0
  444. package/src/zig/tests/syntax-style_test.zig +558 -0
  445. package/src/zig/tests/terminal_test.zig +1489 -0
  446. package/src/zig/tests/test-renderer.zig +126 -0
  447. package/src/zig/tests/text-buffer-drawing_test.zig +3291 -0
  448. package/src/zig/tests/text-buffer-highlights_test.zig +706 -0
  449. package/src/zig/tests/text-buffer-iterators_test.zig +776 -0
  450. package/src/zig/tests/text-buffer-segment_test.zig +320 -0
  451. package/src/zig/tests/text-buffer-selection_test.zig +1056 -0
  452. package/src/zig/tests/text-buffer-selection_viewport_test.zig +358 -0
  453. package/src/zig/tests/text-buffer-view_test.zig +3672 -0
  454. package/src/zig/tests/text-buffer_test.zig +2191 -0
  455. package/src/zig/tests/unicode-width-map.zon +3909 -0
  456. package/src/zig/tests/utf8_no_zwj_test.zig +260 -0
  457. package/src/zig/tests/utf8_test.zig +4060 -0
  458. package/src/zig/tests/utf8_wcwidth_cursor_test.zig +267 -0
  459. package/src/zig/tests/utf8_wcwidth_test.zig +357 -0
  460. package/src/zig/tests/word-wrap-editing_test.zig +498 -0
  461. package/src/zig/tests/wrap-cache-perf_test.zig +113 -0
  462. package/src/zig/tests/yoga_test.zig +65 -0
  463. package/src/zig/text-buffer-iterators.zig +496 -0
  464. package/src/zig/text-buffer-segment.zig +403 -0
  465. package/src/zig/text-buffer-view.zig +1428 -0
  466. package/src/zig/text-buffer.zig +1272 -0
  467. package/src/zig/utf8.zig +1954 -0
  468. package/src/zig/utils.zig +12 -0
  469. package/src/zig/uucode-stub.zig +76 -0
  470. package/src/zig/vendor/miniaudio/miniaudio.h +93468 -0
  471. package/src/zig/yoga.zig +658 -0
  472. package/src/zig-structs.ts +381 -0
  473. package/src/zig.ts +5260 -0
  474. package/tsconfig.build.json +17 -0
  475. package/tsconfig.json +27 -0
  476. package/tsconfig.node-test.json +138 -0
@@ -0,0 +1,1434 @@
1
+ import { test, expect, beforeEach, afterEach, beforeAll, describe } from "bun:test"
2
+ import { TreeSitterClient } from "./client.js"
3
+ import { tmpdir } from "os"
4
+ import { join } from "path"
5
+ import { existsSync } from "fs"
6
+ import { mkdir, writeFile, unlink } from "fs/promises"
7
+ import { getDataPaths } from "../data-paths.js"
8
+ import { clearEnvCache } from "../env.js"
9
+ import { destroySingleton } from "../singleton.js"
10
+ import { destroyTreeSitterClient, getTreeSitterClient } from "./index.js"
11
+
12
+ describe("TreeSitterClient", () => {
13
+ let client: TreeSitterClient
14
+ let dataPath: string
15
+
16
+ const sharedDataPath = join(tmpdir(), "tree-sitter-shared-test-data")
17
+
18
+ beforeAll(async () => {
19
+ await mkdir(sharedDataPath, { recursive: true })
20
+ })
21
+
22
+ beforeEach(async () => {
23
+ dataPath = sharedDataPath
24
+ client = new TreeSitterClient({
25
+ dataPath,
26
+ })
27
+ })
28
+
29
+ afterEach(async () => {
30
+ if (client) {
31
+ await client.destroy()
32
+ }
33
+ })
34
+
35
+ test("should initialize successfully", async () => {
36
+ await client.initialize()
37
+ expect(client.isInitialized()).toBe(true)
38
+ })
39
+
40
+ test("should lazily start the worker during initialize when auto start is disabled", async () => {
41
+ const lazyClient = new TreeSitterClient({ dataPath }, { autoStartWorker: false })
42
+
43
+ try {
44
+ await lazyClient.initialize()
45
+
46
+ expect(lazyClient.isInitialized()).toBe(true)
47
+ expect(await lazyClient.preloadParser("javascript")).toBe(true)
48
+ } finally {
49
+ await lazyClient.destroy()
50
+ }
51
+ })
52
+
53
+ test("should initialize with a URL worker path override", async () => {
54
+ const workerPath = existsSync(new URL("./parser.worker.js", import.meta.url))
55
+ ? new URL("./parser.worker.js", import.meta.url)
56
+ : new URL("./parser.worker.ts", import.meta.url)
57
+ const urlClient = new TreeSitterClient({
58
+ dataPath,
59
+ workerPath,
60
+ })
61
+
62
+ try {
63
+ await urlClient.initialize()
64
+
65
+ expect(urlClient.isInitialized()).toBe(true)
66
+ expect(await urlClient.preloadParser("javascript")).toBe(true)
67
+ } finally {
68
+ await urlClient.destroy()
69
+ }
70
+ })
71
+
72
+ test("should wait for default parsers before resolving concurrent initialization", async () => {
73
+ let resolveRegistrationStarted!: () => void
74
+ let resolveRegistration!: () => void
75
+ let registrationCompleted = false
76
+
77
+ const registrationStarted = new Promise<void>((resolve) => {
78
+ resolveRegistrationStarted = resolve
79
+ })
80
+ const registrationGate = new Promise<void>((resolve) => {
81
+ resolveRegistration = resolve
82
+ })
83
+
84
+ const clientInternals = client as unknown as { registerDefaultParsers: () => Promise<void> }
85
+ const registerDefaultParsers = clientInternals.registerDefaultParsers.bind(client)
86
+
87
+ clientInternals.registerDefaultParsers = async () => {
88
+ resolveRegistrationStarted()
89
+ await registrationGate
90
+ await registerDefaultParsers()
91
+ registrationCompleted = true
92
+ }
93
+
94
+ const firstInitialize = client.initialize()
95
+ const secondInitialize = client.initialize()
96
+
97
+ await registrationStarted
98
+
99
+ let secondResolved = false
100
+ const observedSecondInitialize = secondInitialize.then(() => {
101
+ secondResolved = true
102
+ })
103
+
104
+ await new Promise((resolve) => setTimeout(resolve, 25))
105
+
106
+ expect(secondResolved).toBe(false)
107
+ expect(client.isInitialized()).toBe(false)
108
+
109
+ resolveRegistration()
110
+
111
+ await Promise.all([firstInitialize, observedSecondInitialize])
112
+
113
+ expect(registrationCompleted).toBe(true)
114
+ expect(client.isInitialized()).toBe(true)
115
+ })
116
+
117
+ test("should reject initialization when destroyed during default parser registration", async () => {
118
+ let resolveRegistrationStarted!: () => void
119
+ let resolveRegistration!: () => void
120
+ const registrationStarted = new Promise<void>((resolve) => {
121
+ resolveRegistrationStarted = resolve
122
+ })
123
+ const registrationGate = new Promise<void>((resolve) => {
124
+ resolveRegistration = resolve
125
+ })
126
+ const clientInternals = client as unknown as { registerDefaultParsers: () => Promise<void> }
127
+ const registerDefaultParsers = clientInternals.registerDefaultParsers.bind(client)
128
+
129
+ clientInternals.registerDefaultParsers = async () => {
130
+ resolveRegistrationStarted()
131
+ await registrationGate
132
+ await registerDefaultParsers()
133
+ }
134
+
135
+ const initializeOutcome = client.initialize().then(
136
+ () => ({ status: "fulfilled" as const }),
137
+ (error: unknown) => ({ status: "rejected" as const, error }),
138
+ )
139
+
140
+ try {
141
+ await registrationStarted
142
+ await client.destroy()
143
+ resolveRegistration()
144
+
145
+ const outcome = await initializeOutcome
146
+ expect(outcome.status).toBe("rejected")
147
+ if (outcome.status === "rejected") {
148
+ expect(outcome.error).toBeInstanceOf(Error)
149
+ expect((outcome.error as Error).message).toBe("Client destroyed during initialization")
150
+ }
151
+ expect(client.isInitialized()).toBe(false)
152
+ } finally {
153
+ resolveRegistration()
154
+ await initializeOutcome
155
+ await client.destroy()
156
+ }
157
+ })
158
+
159
+ test("should preload parsers for supported filetypes", async () => {
160
+ await client.initialize()
161
+
162
+ const hasJavaScript = await client.preloadParser("javascript")
163
+ expect(hasJavaScript).toBe(true)
164
+
165
+ const hasJavaScriptReact = await client.preloadParser("javascriptreact")
166
+ expect(hasJavaScriptReact).toBe(true)
167
+
168
+ const hasTypeScript = await client.preloadParser("typescript")
169
+ expect(hasTypeScript).toBe(true)
170
+
171
+ const hasTypeScriptReact = await client.preloadParser("typescriptreact")
172
+ expect(hasTypeScriptReact).toBe(true)
173
+ })
174
+
175
+ test("should return false for unsupported filetypes", async () => {
176
+ await client.initialize()
177
+
178
+ const hasUnsupported = await client.preloadParser("unsupported-language")
179
+ expect(hasUnsupported).toBe(false)
180
+ })
181
+
182
+ test("should create buffer with supported filetype", async () => {
183
+ await client.initialize()
184
+
185
+ const jsCode = 'const hello = "world";'
186
+ const hasParser = await client.createBuffer(1, jsCode, "javascript")
187
+
188
+ expect(hasParser).toBe(true)
189
+
190
+ const buffer = client.getBuffer(1)
191
+ expect(buffer).toBeDefined()
192
+ expect(buffer?.hasParser).toBe(true)
193
+ expect(buffer?.content).toBe(jsCode)
194
+ expect(buffer?.filetype).toBe("javascript")
195
+ })
196
+
197
+ test("should create buffer without parser for unsupported filetype", async () => {
198
+ await client.initialize()
199
+
200
+ const content = "some random content"
201
+ const hasParser = await client.createBuffer(1, content, "unsupported")
202
+
203
+ expect(hasParser).toBe(false)
204
+
205
+ const buffer = client.getBuffer(1)
206
+ expect(buffer).toBeDefined()
207
+ expect(buffer?.hasParser).toBe(false)
208
+ })
209
+
210
+ test("should emit highlights:response event when buffer is updated", async () => {
211
+ await client.initialize()
212
+
213
+ const jsCode = 'const hello = "world";'
214
+ await client.createBuffer(1, jsCode, "javascript")
215
+
216
+ let highlightReceived = false
217
+ let receivedBufferId: number | undefined
218
+ let receivedVersion: number | undefined
219
+
220
+ client.on("highlights:response", (bufferId, version, highlights) => {
221
+ highlightReceived = true
222
+ receivedBufferId = bufferId
223
+ receivedVersion = version
224
+ })
225
+
226
+ await new Promise((resolve) => setTimeout(resolve, 100))
227
+
228
+ const newCode = 'const hello = "world";\nconst foo = 42;'
229
+ const edits = [
230
+ {
231
+ startIndex: jsCode.length,
232
+ oldEndIndex: jsCode.length,
233
+ newEndIndex: newCode.length,
234
+ startPosition: { row: 0, column: jsCode.length },
235
+ oldEndPosition: { row: 0, column: jsCode.length },
236
+ newEndPosition: { row: 1, column: 14 },
237
+ },
238
+ ]
239
+
240
+ await client.updateBuffer(1, edits, newCode, 2)
241
+
242
+ await new Promise((resolve) => setTimeout(resolve, 200))
243
+
244
+ expect(highlightReceived).toBe(true)
245
+ expect(receivedBufferId).toBe(1)
246
+ expect(receivedVersion).toBe(2)
247
+ })
248
+
249
+ test("should handle buffer removal", async () => {
250
+ await client.initialize()
251
+
252
+ const jsCode = 'const hello = "world";'
253
+ await client.createBuffer(1, jsCode, "javascript")
254
+
255
+ let bufferDisposed = false
256
+ client.on("buffer:disposed", (bufferId) => {
257
+ if (bufferId === 1) {
258
+ bufferDisposed = true
259
+ }
260
+ })
261
+
262
+ await client.removeBuffer(1)
263
+
264
+ expect(bufferDisposed).toBe(true)
265
+ expect(client.getBuffer(1)).toBeUndefined()
266
+ })
267
+
268
+ test("should handle multiple buffers", async () => {
269
+ await client.initialize()
270
+
271
+ const jsCode = 'const hello = "world";'
272
+ const tsCode = "interface Test { value: string }"
273
+
274
+ await client.createBuffer(1, jsCode, "javascript")
275
+ await client.createBuffer(2, tsCode, "typescript")
276
+
277
+ const buffers = client.getAllBuffers()
278
+ expect(buffers).toHaveLength(2)
279
+
280
+ const jsBuffer = client.getBuffer(1)
281
+ const tsBuffer = client.getBuffer(2)
282
+
283
+ expect(jsBuffer?.filetype).toBe("javascript")
284
+ expect(tsBuffer?.filetype).toBe("typescript")
285
+ expect(jsBuffer?.hasParser).toBe(true)
286
+ expect(tsBuffer?.hasParser).toBe(true)
287
+ })
288
+
289
+ test("should handle buffer reset", async () => {
290
+ await client.initialize()
291
+
292
+ const jsCode = 'const hello = "world";'
293
+ await client.createBuffer(1, jsCode, "javascript")
294
+
295
+ const newContent = "function test() { return 42; }"
296
+ await client.resetBuffer(1, 2, newContent)
297
+
298
+ const buffer = client.getBuffer(1)
299
+ expect(buffer?.content).toBe(newContent)
300
+ expect(buffer?.version).toBe(2)
301
+ })
302
+
303
+ test("should emit error events for invalid operations", async () => {
304
+ await client.initialize()
305
+
306
+ let errorReceived = false
307
+ let errorMessage = ""
308
+
309
+ client.on("error", (error, bufferId) => {
310
+ errorReceived = true
311
+ errorMessage = error
312
+ })
313
+
314
+ await client.resetBuffer(999, 1, "test")
315
+
316
+ expect(errorReceived).toBe(true)
317
+ expect(errorMessage).toContain("Cannot reset buffer with no parser")
318
+ })
319
+
320
+ test("should prevent duplicate buffer creation", async () => {
321
+ await client.initialize()
322
+
323
+ const jsCode = 'const hello = "world";'
324
+ await client.createBuffer(1, jsCode, "javascript")
325
+
326
+ await expect(client.createBuffer(1, "other code", "javascript")).rejects.toThrow("Buffer with id 1 already exists")
327
+ })
328
+
329
+ test("should handle performance metrics", async () => {
330
+ await client.initialize()
331
+
332
+ const performance = await client.getPerformance()
333
+ expect(performance).toBeDefined()
334
+ expect(typeof performance.averageParseTime).toBe("number")
335
+ expect(typeof performance.averageQueryTime).toBe("number")
336
+ expect(Array.isArray(performance.parseTimes)).toBe(true)
337
+ expect(Array.isArray(performance.queryTimes)).toBe(true)
338
+ })
339
+
340
+ test("should handle concurrent buffer operations", async () => {
341
+ await client.initialize()
342
+
343
+ const promises = []
344
+
345
+ for (let i = 0; i < 5; i++) {
346
+ const code = `const var${i} = ${i};`
347
+ promises.push(client.createBuffer(i, code, "javascript"))
348
+ }
349
+
350
+ const results = await Promise.all(promises)
351
+ expect(results.every((result) => result === true)).toBe(true)
352
+
353
+ const buffers = client.getAllBuffers()
354
+ expect(buffers).toHaveLength(5)
355
+ })
356
+
357
+ test("should clean up resources on destroy", async () => {
358
+ await client.initialize()
359
+
360
+ const jsCode = 'const hello = "world";'
361
+ await client.createBuffer(1, jsCode, "javascript")
362
+
363
+ expect(client.getAllBuffers()).toHaveLength(1)
364
+
365
+ await client.destroy()
366
+
367
+ expect(client.isInitialized()).toBe(false)
368
+ expect(client.getAllBuffers()).toHaveLength(0)
369
+ })
370
+
371
+ test("should perform one-shot highlighting", async () => {
372
+ await client.initialize()
373
+
374
+ const jsCode = 'const hello = "world";\nfunction test() { return 42; }'
375
+ const result = await client.highlightOnce(jsCode, "javascript")
376
+
377
+ expect(result.highlights).toBeDefined()
378
+ expect(result.highlights!.length).toBeGreaterThan(0)
379
+
380
+ const firstHighlight = result.highlights![0]
381
+ expect(Array.isArray(firstHighlight)).toBe(true)
382
+ expect(firstHighlight).toHaveLength(3)
383
+ expect(typeof firstHighlight[0]).toBe("number")
384
+ expect(typeof firstHighlight[1]).toBe("number")
385
+ expect(typeof firstHighlight[2]).toBe("string")
386
+
387
+ const groups = result.highlights!.map((hl) => hl[2])
388
+ expect(groups.length).toBeGreaterThan(0)
389
+ expect(groups).toContain("keyword")
390
+ })
391
+
392
+ test("should handle one-shot highlighting for unsupported filetype", async () => {
393
+ await client.initialize()
394
+
395
+ const result = await client.highlightOnce("some content", "unsupported-lang")
396
+
397
+ expect(result.highlights).toBeUndefined()
398
+ expect(result.warning).toContain("No parser available for filetype unsupported-lang")
399
+ }, 5000)
400
+
401
+ test("should perform multiple one-shot highlights independently", async () => {
402
+ await client.initialize()
403
+
404
+ const jsCode = 'const hello = "world";'
405
+ const tsCode = "interface Test { value: string }"
406
+
407
+ const [jsResult, tsResult] = await Promise.all([
408
+ client.highlightOnce(jsCode, "javascript"),
409
+ client.highlightOnce(tsCode, "typescript"),
410
+ ])
411
+
412
+ expect(jsResult.highlights).toBeDefined()
413
+ expect(tsResult.highlights).toBeDefined()
414
+ expect(jsResult.highlights!.length).toBeGreaterThan(0)
415
+ expect(tsResult.highlights!.length).toBeGreaterThan(0)
416
+
417
+ jsResult.highlights!.forEach((hl) => {
418
+ expect(Array.isArray(hl)).toBe(true)
419
+ expect(hl).toHaveLength(3)
420
+ })
421
+
422
+ tsResult.highlights!.forEach((hl) => {
423
+ expect(Array.isArray(hl)).toBe(true)
424
+ expect(hl).toHaveLength(3)
425
+ })
426
+
427
+ expect(client.getAllBuffers()).toHaveLength(0)
428
+ })
429
+
430
+ test("should perform one-shot highlighting for react parser aliases", async () => {
431
+ await client.initialize()
432
+
433
+ const jsxCode = 'const view = <div className="card">hello</div>'
434
+ const tsxCode = 'const view: JSX.Element = <div className="card">hello</div>'
435
+
436
+ const [jsxResult, tsxResult] = await Promise.all([
437
+ client.highlightOnce(jsxCode, "javascriptreact"),
438
+ client.highlightOnce(tsxCode, "typescriptreact"),
439
+ ])
440
+
441
+ expect(jsxResult.highlights).toBeDefined()
442
+ expect(tsxResult.highlights).toBeDefined()
443
+ expect(jsxResult.highlights!.length).toBeGreaterThan(0)
444
+ expect(tsxResult.highlights!.length).toBeGreaterThan(0)
445
+
446
+ const jsxGroups = jsxResult.highlights!.map((hl) => hl[2])
447
+ const tsxGroups = tsxResult.highlights!.map((hl) => hl[2])
448
+
449
+ expect(jsxGroups).toContain("keyword")
450
+ expect(tsxGroups).toContain("keyword")
451
+ })
452
+
453
+ test("should handle Devanagari characters and highlight ranges after them correctly", async () => {
454
+ await client.initialize()
455
+
456
+ const jsCode = 'const greeting = "नमस्ते";\nconst x = 42;'
457
+ const result = await client.highlightOnce(jsCode, "javascript")
458
+
459
+ expect(result.highlights).toBeDefined()
460
+ expect(result.highlights!.length).toBeGreaterThan(0)
461
+
462
+ const keywordHighlights = result.highlights!.filter((hl) => hl[2] === "keyword")
463
+ expect(keywordHighlights.length).toBeGreaterThanOrEqual(2)
464
+
465
+ const constHighlights = keywordHighlights.filter((hl) => {
466
+ const text = jsCode.substring(hl[0], hl[1])
467
+ return text === "const"
468
+ })
469
+
470
+ expect(constHighlights).toHaveLength(2)
471
+
472
+ const firstConst = constHighlights[0]
473
+ const secondConst = constHighlights[1]
474
+
475
+ expect(jsCode.substring(firstConst[0], firstConst[1])).toBe("const")
476
+ expect(jsCode.substring(secondConst[0], secondConst[1])).toBe("const")
477
+
478
+ expect(firstConst[0]).toBe(0)
479
+ expect(firstConst[1]).toBe(5)
480
+
481
+ expect(secondConst[0]).toBeGreaterThan(firstConst[1])
482
+ const textBetween = jsCode.substring(firstConst[1], secondConst[0])
483
+ expect(textBetween).toContain("नमस्ते")
484
+
485
+ const numberHighlight = result.highlights!.find((hl) => {
486
+ const text = jsCode.substring(hl[0], hl[1])
487
+ return text === "42" && hl[2] === "number"
488
+ })
489
+
490
+ expect(numberHighlight).toBeDefined()
491
+ if (numberHighlight) {
492
+ const [start, end] = numberHighlight
493
+ const actualText = jsCode.substring(start, end)
494
+ expect(actualText).toBe("42")
495
+
496
+ const secondLine = jsCode.split("\n")[1]
497
+ const secondLineStart = jsCode.indexOf(secondLine)
498
+ const expectedStart = secondLineStart + secondLine.indexOf("42")
499
+ expect(start).toBe(expectedStart)
500
+ }
501
+ })
502
+
503
+ test("should support local file paths for parser configuration", async () => {
504
+ const testQueryPath = join(dataPath, `test-highlights-${Date.now()}.scm`)
505
+ const simpleQuery = "(identifier) @variable"
506
+ await writeFile(testQueryPath, simpleQuery, "utf8")
507
+
508
+ try {
509
+ client.addFiletypeParser({
510
+ filetype: "test-lang",
511
+ aliases: ["test-lang-react"],
512
+ queries: {
513
+ highlights: [testQueryPath],
514
+ },
515
+ wasm: "https://github.com/tree-sitter/tree-sitter-javascript/releases/download/v0.23.1/tree-sitter-javascript.wasm",
516
+ })
517
+
518
+ await client.initialize()
519
+
520
+ const hasParser = await client.preloadParser("test-lang")
521
+ expect(hasParser).toBe(true)
522
+
523
+ const hasAliasParser = await client.preloadParser("test-lang-react")
524
+ expect(hasAliasParser).toBe(true)
525
+
526
+ const testCode = "const myVariable = 42;"
527
+ const result = await client.highlightOnce(testCode, "test-lang")
528
+ const aliasResult = await client.highlightOnce(testCode, "test-lang-react")
529
+
530
+ expect(result.highlights).toBeDefined()
531
+ expect(aliasResult.highlights).toBeDefined()
532
+ expect(result.error).toBeUndefined()
533
+ expect(aliasResult.error).toBeUndefined()
534
+ expect(result.warning).toBeUndefined()
535
+ expect(aliasResult.warning).toBeUndefined()
536
+ } finally {
537
+ try {
538
+ await unlink(testQueryPath)
539
+ } catch (e) {
540
+ // Ignore cleanup errors
541
+ }
542
+ }
543
+ })
544
+
545
+ test("should handle concurrent highlightOnce calls efficiently (no duplicate parser loading)", async () => {
546
+ const workerLogs: string[] = []
547
+
548
+ client.on("worker:log", (_logType, message) => {
549
+ if (message.includes("Loading from local path:")) {
550
+ workerLogs.push(message)
551
+ }
552
+ })
553
+
554
+ await client.initialize()
555
+
556
+ const jsCode = 'const hello = "world"; function test() { return 42; }'
557
+ const promises = Array.from({ length: 5 }, () => client.highlightOnce(jsCode, "javascript"))
558
+
559
+ const results = await Promise.all(promises)
560
+
561
+ for (const result of results) {
562
+ expect(result.highlights).toBeDefined()
563
+ expect(result.highlights!.length).toBeGreaterThan(0)
564
+ expect(result.error).toBeUndefined()
565
+ }
566
+
567
+ const firstResult = results[0]
568
+ for (let i = 1; i < results.length; i++) {
569
+ expect(results[i].highlights).toEqual(firstResult.highlights)
570
+ }
571
+
572
+ await new Promise((resolve) => setTimeout(resolve, 100))
573
+
574
+ const languageLoadLogs = workerLogs.filter((log) => log.includes("tree-sitter-javascript.wasm"))
575
+ const queryLoadLogs = workerLogs.filter((log) => log.includes("highlights.scm"))
576
+
577
+ expect(languageLoadLogs.length).toBeLessThanOrEqual(1)
578
+ expect(queryLoadLogs.length).toBeLessThanOrEqual(1)
579
+ }, 15000)
580
+
581
+ test("should reuse canonical parser assets for aliased filetypes", async () => {
582
+ const workerLogs: string[] = []
583
+
584
+ client.on("worker:log", (_logType, message) => {
585
+ if (message.includes("Loading from local path:")) {
586
+ workerLogs.push(message)
587
+ }
588
+ })
589
+
590
+ await client.initialize()
591
+
592
+ const jsxCode = 'const view = <div className="card">hello</div>'
593
+ const [canonicalResult, aliasResult] = await Promise.all([
594
+ client.highlightOnce(jsxCode, "javascript"),
595
+ client.highlightOnce(jsxCode, "javascriptreact"),
596
+ ])
597
+
598
+ expect(canonicalResult.highlights).toBeDefined()
599
+ expect(aliasResult.highlights).toBeDefined()
600
+ expect(canonicalResult.error).toBeUndefined()
601
+ expect(aliasResult.error).toBeUndefined()
602
+
603
+ await new Promise((resolve) => setTimeout(resolve, 100))
604
+
605
+ const languageLoadLogs = workerLogs.filter((log) => log.includes("tree-sitter-javascript.wasm"))
606
+ const queryLoadLogs = workerLogs.filter(
607
+ (log) => log.includes("assets") && log.includes("javascript") && log.includes("highlights.scm"),
608
+ )
609
+
610
+ expect(languageLoadLogs.length).toBeLessThanOrEqual(1)
611
+ expect(queryLoadLogs.length).toBeLessThanOrEqual(1)
612
+ expect(workerLogs.some((log) => log.includes("javascriptreact"))).toBe(false)
613
+ }, 15000)
614
+ })
615
+
616
+ describe("TreeSitterClient Injections", () => {
617
+ let dataPath: string
618
+
619
+ const injectionsDataPath = join(tmpdir(), "tree-sitter-injections-test-data")
620
+
621
+ beforeAll(async () => {
622
+ await mkdir(injectionsDataPath, { recursive: true })
623
+ })
624
+
625
+ beforeEach(async () => {
626
+ dataPath = injectionsDataPath
627
+ })
628
+
629
+ test("should highlight inline code in markdown using markdown_inline injection", async () => {
630
+ const client = new TreeSitterClient({ dataPath })
631
+
632
+ try {
633
+ await client.initialize()
634
+
635
+ const markdownCode = `# Hello World
636
+
637
+ The \`CodeRenderable\` component provides syntax highlighting.
638
+
639
+ You can use \`const x = 42\` in your code.`
640
+
641
+ const result = await client.highlightOnce(markdownCode, "markdown")
642
+
643
+ expect(result.highlights).toBeDefined()
644
+ expect(result.highlights!.length).toBeGreaterThan(0)
645
+
646
+ const groups = result.highlights!.map((hl) => hl[2])
647
+ const hasInlineCodeHighlights = groups.some((g) => g.includes("markup.raw"))
648
+
649
+ expect(hasInlineCodeHighlights).toBe(true)
650
+ } finally {
651
+ await client.destroy()
652
+ }
653
+ }, 10000)
654
+
655
+ test("should highlight code blocks in markdown using language-specific injection", async () => {
656
+ const client = new TreeSitterClient({ dataPath })
657
+
658
+ try {
659
+ await client.initialize()
660
+
661
+ const markdownCode = `# Code Example
662
+
663
+ \`\`\`typescript
664
+ const hello: string = "world";
665
+ function test() { return 42; }
666
+ \`\`\`
667
+
668
+ Some text here.`
669
+
670
+ const result = await client.highlightOnce(markdownCode, "markdown")
671
+
672
+ expect(result.highlights).toBeDefined()
673
+ expect(result.highlights!.length).toBeGreaterThan(0)
674
+
675
+ const groups = result.highlights!.map((hl) => hl[2])
676
+ const hasTypeScriptHighlights = groups.some((g) => g === "keyword" || g === "type" || g === "function")
677
+
678
+ expect(hasTypeScriptHighlights).toBe(true)
679
+ } finally {
680
+ await client.destroy()
681
+ }
682
+ }, 10000)
683
+
684
+ test("should highlight tsx code blocks in markdown using language-specific injection", async () => {
685
+ const client = new TreeSitterClient({ dataPath })
686
+
687
+ try {
688
+ await client.initialize()
689
+
690
+ const markdownCode = `# Code Example
691
+
692
+ \`\`\`tsx
693
+ const view: JSX.Element = <div>Hello</div>;
694
+ \`\`\`
695
+
696
+ Some text here.`
697
+
698
+ const result = await client.highlightOnce(markdownCode, "markdown")
699
+
700
+ expect(result.highlights).toBeDefined()
701
+ expect(result.highlights!.length).toBeGreaterThan(0)
702
+
703
+ const constHighlight = result.highlights!.find((hl) => {
704
+ const text = markdownCode.substring(hl[0], hl[1])
705
+ return text === "const" && hl[2] === "keyword"
706
+ })
707
+
708
+ expect(constHighlight).toBeDefined()
709
+ } finally {
710
+ await client.destroy()
711
+ }
712
+ }, 10000)
713
+
714
+ test("should return correct offsets for injected code in markdown code blocks", async () => {
715
+ const client = new TreeSitterClient({ dataPath })
716
+
717
+ try {
718
+ await client.initialize()
719
+
720
+ const markdownCode = `# Title\n\n\`\`\`typescript\nconst x = 42;\n\`\`\``
721
+
722
+ const result = await client.highlightOnce(markdownCode, "markdown")
723
+
724
+ expect(result.highlights).toBeDefined()
725
+ expect(result.highlights!.length).toBeGreaterThan(0)
726
+
727
+ const constHighlight = result.highlights!.find((hl) => {
728
+ const text = markdownCode.substring(hl[0], hl[1])
729
+ return text === "const" && hl[2] === "keyword"
730
+ })
731
+
732
+ expect(constHighlight).toBeDefined()
733
+ if (constHighlight) {
734
+ const [start, end, group] = constHighlight
735
+ const text = markdownCode.substring(start, end)
736
+
737
+ expect(text).toBe("const")
738
+ expect(group).toBe("keyword")
739
+ expect(start).toBe(23)
740
+ expect(end).toBe(28)
741
+ }
742
+
743
+ const numberHighlight = result.highlights!.find((hl) => {
744
+ const text = markdownCode.substring(hl[0], hl[1])
745
+ return text === "42" && hl[2] === "number"
746
+ })
747
+
748
+ expect(numberHighlight).toBeDefined()
749
+ if (numberHighlight) {
750
+ const [start, end, group] = numberHighlight
751
+ const text = markdownCode.substring(start, end)
752
+
753
+ expect(text).toBe("42")
754
+ expect(group).toBe("number")
755
+ expect(start).toBe(33)
756
+ expect(end).toBe(35)
757
+ }
758
+ } finally {
759
+ await client.destroy()
760
+ }
761
+ }, 10000)
762
+
763
+ test("should return highlights sorted by start offset for injected code", async () => {
764
+ const client = new TreeSitterClient({ dataPath })
765
+
766
+ try {
767
+ await client.initialize()
768
+
769
+ const markdownCode = `# Documentation
770
+
771
+ Some text with \`inline code\` here.
772
+
773
+ \`\`\`typescript
774
+ const first = 1;
775
+ const second = 2;
776
+ \`\`\`
777
+
778
+ More text with \`another inline\` code.
779
+
780
+ \`\`\`javascript
781
+ function test() {
782
+ return 42;
783
+ }
784
+ \`\`\``
785
+
786
+ const result = await client.highlightOnce(markdownCode, "markdown")
787
+
788
+ expect(result.highlights).toBeDefined()
789
+ expect(result.highlights!.length).toBeGreaterThan(0)
790
+
791
+ for (let i = 1; i < result.highlights!.length; i++) {
792
+ const prevStart = result.highlights![i - 1][0]
793
+ const currStart = result.highlights![i][0]
794
+ expect(currStart).toBeGreaterThanOrEqual(prevStart)
795
+ }
796
+ } finally {
797
+ await client.destroy()
798
+ }
799
+ }, 10000)
800
+
801
+ test("should handle markdown with injections and return valid highlights", async () => {
802
+ const client = new TreeSitterClient({ dataPath })
803
+
804
+ try {
805
+ await client.initialize()
806
+
807
+ const markdownCode = `# Heading
808
+
809
+ Some **bold** text with \`inline code\`.
810
+
811
+ \`\`\`typescript
812
+ const x: string = "hello";
813
+ \`\`\`
814
+
815
+ [Link text](https://example.com)`
816
+
817
+ const result = await client.highlightOnce(markdownCode, "markdown")
818
+
819
+ expect(result.highlights).toBeDefined()
820
+ expect(result.highlights!.length).toBeGreaterThan(0)
821
+
822
+ const overlaps: Array<[number, number]> = []
823
+ for (let i = 0; i < result.highlights!.length; i++) {
824
+ for (let j = i + 1; j < result.highlights!.length; j++) {
825
+ const [start1, end1] = result.highlights![i]
826
+ const [start2, end2] = result.highlights![j]
827
+
828
+ if (start2 < end1) {
829
+ overlaps.push([i, j])
830
+ }
831
+ }
832
+ }
833
+
834
+ expect(overlaps.length).toBeGreaterThanOrEqual(0)
835
+
836
+ const injectionHighlights = result.highlights!.filter((hl) => hl[2].includes("injection"))
837
+ expect(injectionHighlights).toBeDefined()
838
+
839
+ const concealHighlights = result.highlights!.filter((hl) => hl[2] === "conceal")
840
+ expect(concealHighlights).toBeDefined()
841
+
842
+ const blockHighlights = result.highlights!.filter((hl) => hl[2] === "markup.raw.block")
843
+ expect(blockHighlights).toBeDefined()
844
+ } finally {
845
+ await client.destroy()
846
+ }
847
+ }, 10000)
848
+
849
+ test("should handle fast concurrent markdown highlighting requests with injections", async () => {
850
+ const client = new TreeSitterClient({ dataPath })
851
+
852
+ const errors: string[] = []
853
+ client.on("error", (error) => {
854
+ errors.push(error)
855
+ })
856
+
857
+ client.on("worker:log", (logType, message) => {
858
+ if (logType === "error") {
859
+ errors.push(message)
860
+ }
861
+ })
862
+
863
+ try {
864
+ await client.initialize()
865
+
866
+ const markdownCode = `# OpenTUI Documentation
867
+
868
+ ## Getting Started
869
+
870
+ OpenTUI is a modern terminal UI framework built on **tree-sitter** and WebGPU.
871
+
872
+ ### Installation
873
+
874
+ \`\`\`bash
875
+ bun install opentui
876
+ \`\`\`
877
+
878
+ ### Quick Example
879
+
880
+ \`\`\`typescript
881
+ import { createCliRenderer, BoxRenderable } from 'opentui';
882
+
883
+ const renderer = await createCliRenderer();
884
+ const box = new BoxRenderable(renderer, {
885
+ border: true,
886
+ title: "Hello World"
887
+ });
888
+ renderer.root.add(box);
889
+ \`\`\`
890
+
891
+ The \`CodeRenderable\` component provides syntax highlighting.
892
+
893
+ | Property | Type | Description |
894
+ |----------|------|-------------|
895
+ | content | string | Code to display |
896
+ | filetype | string | Language type |`
897
+
898
+ const jsCode = `function test() {
899
+ const hello = "world";
900
+ return hello;
901
+ }`
902
+
903
+ const tsCode = `interface User {
904
+ name: string;
905
+ age: number;
906
+ }
907
+
908
+ const user: User = { name: "Alice", age: 25 };`
909
+
910
+ const promises = []
911
+ for (let i = 0; i < 5; i++) {
912
+ promises.push(client.highlightOnce(markdownCode, "markdown"))
913
+ }
914
+
915
+ const results = await Promise.allSettled(promises)
916
+
917
+ for (let i = 0; i < results.length; i++) {
918
+ const result = results[i]
919
+ if (result.status === "fulfilled") {
920
+ expect(result.value.error).toBeUndefined()
921
+ expect(result.value.highlights).toBeDefined()
922
+ } else {
923
+ throw new Error(`Request ${i} was rejected: ${result.reason}`)
924
+ }
925
+ }
926
+
927
+ await new Promise((resolve) => setTimeout(resolve, 500))
928
+
929
+ const hasMemoryErrors = errors.some((err) => err.includes("Out of bounds memory access"))
930
+ expect(hasMemoryErrors).toBe(false)
931
+ } finally {
932
+ await client.destroy()
933
+ }
934
+ }, 15000)
935
+ })
936
+
937
+ describe("TreeSitterClient Conceal Values", () => {
938
+ let dataPath: string
939
+
940
+ const concealDataPath = join(tmpdir(), "tree-sitter-conceal-test-data")
941
+
942
+ beforeAll(async () => {
943
+ await mkdir(concealDataPath, { recursive: true })
944
+ })
945
+
946
+ beforeEach(async () => {
947
+ dataPath = concealDataPath
948
+ })
949
+
950
+ test("should return conceal values from normal (non-injected) queries", async () => {
951
+ const client = new TreeSitterClient({ dataPath })
952
+
953
+ try {
954
+ await client.initialize()
955
+
956
+ const markdownCode = `![Image Alt Text](https://example.com/image.png)`
957
+
958
+ const result = await client.highlightOnce(markdownCode, "markdown")
959
+
960
+ expect(result.highlights).toBeDefined()
961
+ expect(result.error).toBeUndefined()
962
+
963
+ const concealedHighlights = result.highlights!.filter((hl) => {
964
+ const meta = (hl as any)[3]
965
+ return meta && meta.conceal !== undefined
966
+ })
967
+
968
+ expect(concealedHighlights.length).toBeGreaterThan(0)
969
+
970
+ concealedHighlights.forEach((hl) => {
971
+ const meta = (hl as any)[3]
972
+ expect(meta.conceal).toBeDefined()
973
+ })
974
+ } finally {
975
+ await client.destroy()
976
+ }
977
+ }, 10000)
978
+
979
+ test("should return conceal values from injected queries (markdown_inline)", async () => {
980
+ const client = new TreeSitterClient({ dataPath })
981
+
982
+ try {
983
+ await client.initialize()
984
+
985
+ const markdownCode = `Here is a [link](https://example.com) in text.`
986
+
987
+ const result = await client.highlightOnce(markdownCode, "markdown")
988
+
989
+ expect(result.highlights).toBeDefined()
990
+ expect(result.error).toBeUndefined()
991
+
992
+ const concealedHighlights = result.highlights!.filter((hl) => {
993
+ const meta = (hl as any)[3]
994
+ return meta && meta.conceal !== undefined
995
+ })
996
+
997
+ expect(concealedHighlights.length).toBeGreaterThan(0)
998
+
999
+ concealedHighlights.forEach((hl) => {
1000
+ const meta = (hl as any)[3]
1001
+ expect(meta.conceal).toBeDefined()
1002
+ expect(meta.isInjection).toBeDefined()
1003
+ })
1004
+
1005
+ const closingBracketHighlight = concealedHighlights.find((hl) => {
1006
+ const text = markdownCode.substring(hl[0], hl[1])
1007
+ const meta = (hl as any)[3]
1008
+ return text === "]" && meta.conceal !== ""
1009
+ })
1010
+
1011
+ if (closingBracketHighlight) {
1012
+ const meta = (closingBracketHighlight as any)[3]
1013
+ expect(meta.conceal).toBeDefined()
1014
+ }
1015
+ } finally {
1016
+ await client.destroy()
1017
+ }
1018
+ }, 10000)
1019
+
1020
+ test("should distinguish conceal values between normal and injected queries", async () => {
1021
+ const client = new TreeSitterClient({ dataPath })
1022
+
1023
+ try {
1024
+ await client.initialize()
1025
+
1026
+ const markdownCode = `Here is a [link](https://example.com) and ![image](https://example.com/img.png).`
1027
+
1028
+ const result = await client.highlightOnce(markdownCode, "markdown")
1029
+
1030
+ expect(result.highlights).toBeDefined()
1031
+ expect(result.error).toBeUndefined()
1032
+
1033
+ const concealedHighlights = result.highlights!.filter((hl) => {
1034
+ const meta = (hl as any)[3]
1035
+ return meta && meta.conceal !== undefined
1036
+ })
1037
+
1038
+ expect(concealedHighlights.length).toBeGreaterThan(0)
1039
+
1040
+ const normalConceal = concealedHighlights.filter((hl) => {
1041
+ const meta = (hl as any)[3]
1042
+ return !meta.isInjection
1043
+ })
1044
+
1045
+ const injectedConceal = concealedHighlights.filter((hl) => {
1046
+ const meta = (hl as any)[3]
1047
+ return meta.isInjection
1048
+ })
1049
+
1050
+ expect(injectedConceal.length).toBeGreaterThan(0)
1051
+
1052
+ injectedConceal.forEach((hl) => {
1053
+ const meta = (hl as any)[3]
1054
+ expect(meta.conceal).toBeDefined()
1055
+ expect(meta.isInjection).toBe(true)
1056
+ })
1057
+
1058
+ concealedHighlights.forEach((hl) => {
1059
+ const meta = (hl as any)[3]
1060
+ expect(meta.conceal).toBeDefined()
1061
+ expect(typeof meta.isInjection).toBe("boolean")
1062
+ })
1063
+ } finally {
1064
+ await client.destroy()
1065
+ }
1066
+ }, 10000)
1067
+
1068
+ test("should handle pattern index lookups correctly for injections", async () => {
1069
+ const client = new TreeSitterClient({ dataPath })
1070
+
1071
+ try {
1072
+ await client.initialize()
1073
+
1074
+ const markdownCode = `A [link](url) here.`
1075
+
1076
+ const result = await client.highlightOnce(markdownCode, "markdown")
1077
+
1078
+ expect(result.highlights).toBeDefined()
1079
+ expect(result.error).toBeUndefined()
1080
+
1081
+ const concealedHighlights = result.highlights!.filter((hl) => {
1082
+ const meta = (hl as any)[3]
1083
+ return meta && meta.conceal !== undefined
1084
+ })
1085
+
1086
+ expect(concealedHighlights.length).toBeGreaterThan(0)
1087
+
1088
+ concealedHighlights.forEach((hl) => {
1089
+ const meta = (hl as any)[3]
1090
+ expect(meta.conceal).toBeDefined()
1091
+ })
1092
+ } finally {
1093
+ await client.destroy()
1094
+ }
1095
+ }, 10000)
1096
+
1097
+ test("should handle multiple injected languages with different conceal patterns", async () => {
1098
+ const client = new TreeSitterClient({ dataPath })
1099
+
1100
+ try {
1101
+ await client.initialize()
1102
+
1103
+ const markdownCode = `# Title
1104
+
1105
+ Inline \`code\` and a [link](url) here.
1106
+
1107
+ \`\`\`typescript
1108
+ const x = 42;
1109
+ \`\`\`
1110
+
1111
+ More text with ![image](img.png) and **bold**.`
1112
+
1113
+ const result = await client.highlightOnce(markdownCode, "markdown")
1114
+
1115
+ expect(result.highlights).toBeDefined()
1116
+ expect(result.error).toBeUndefined()
1117
+
1118
+ const concealedHighlights = result.highlights!.filter((hl) => {
1119
+ const meta = (hl as any)[3]
1120
+ return meta && meta.conceal !== undefined
1121
+ })
1122
+
1123
+ expect(concealedHighlights.length).toBeGreaterThan(0)
1124
+
1125
+ const byLang = new Map<string, any[]>()
1126
+ concealedHighlights.forEach((hl) => {
1127
+ const meta = (hl as any)[3]
1128
+ const lang = meta.isInjection ? meta.injectionLang || "injected" : "normal"
1129
+ if (!byLang.has(lang)) {
1130
+ byLang.set(lang, [])
1131
+ }
1132
+ byLang.get(lang)!.push(hl)
1133
+ })
1134
+
1135
+ expect(byLang.size).toBeGreaterThan(0)
1136
+
1137
+ byLang.forEach((highlights) => {
1138
+ expect(highlights.length).toBeGreaterThan(0)
1139
+ highlights.forEach((hl: any) => {
1140
+ const meta = hl[3]
1141
+ expect(meta.conceal).toBeDefined()
1142
+ })
1143
+ })
1144
+ } finally {
1145
+ await client.destroy()
1146
+ }
1147
+ }, 10000)
1148
+
1149
+ test("should preserve non-empty conceal replacements like space character", async () => {
1150
+ const client = new TreeSitterClient({ dataPath })
1151
+
1152
+ try {
1153
+ await client.initialize()
1154
+
1155
+ const markdownCode = `Check [this link](https://example.com) out!`
1156
+
1157
+ const result = await client.highlightOnce(markdownCode, "markdown")
1158
+
1159
+ expect(result.highlights).toBeDefined()
1160
+ expect(result.error).toBeUndefined()
1161
+
1162
+ const closingBracket = result.highlights!.find((hl) => {
1163
+ const text = markdownCode.substring(hl[0], hl[1])
1164
+ const meta = (hl as any)[3]
1165
+ return text === "]" && hl[2] === "conceal" && meta?.conceal !== undefined
1166
+ })
1167
+
1168
+ if (closingBracket) {
1169
+ const meta = (closingBracket as any)[3]
1170
+ expect(meta).toBeDefined()
1171
+ expect(meta.conceal).toBeDefined()
1172
+ expect(meta.conceal).toBe(" ")
1173
+ expect(meta.conceal.length).toBeGreaterThan(0)
1174
+ }
1175
+ } finally {
1176
+ await client.destroy()
1177
+ }
1178
+ }, 10000)
1179
+ })
1180
+
1181
+ describe("TreeSitterClient Edge Cases", () => {
1182
+ let dataPath: string
1183
+
1184
+ const edgeCaseDataPath = join(tmpdir(), "tree-sitter-edge-case-test-data")
1185
+ const reactiveDataPathRoot = join(tmpdir(), "tree-sitter-reactive-data-path-test")
1186
+
1187
+ beforeAll(async () => {
1188
+ await mkdir(edgeCaseDataPath, { recursive: true })
1189
+ await mkdir(reactiveDataPathRoot, { recursive: true })
1190
+ })
1191
+
1192
+ beforeEach(async () => {
1193
+ dataPath = edgeCaseDataPath
1194
+ })
1195
+
1196
+ test("should handle initialization timeout", async () => {
1197
+ const client = new TreeSitterClient({
1198
+ dataPath,
1199
+ workerPath: "invalid-path",
1200
+ initTimeout: 500,
1201
+ })
1202
+
1203
+ await expect(client.initialize()).rejects.toThrow(/Worker error|Worker initialization timed out/)
1204
+
1205
+ await client.destroy()
1206
+ })
1207
+
1208
+ test("should handle operations before initialization", async () => {
1209
+ const client = new TreeSitterClient({ dataPath })
1210
+
1211
+ expect(client.isInitialized()).toBe(false)
1212
+ expect(client.getAllBuffers()).toHaveLength(0)
1213
+ expect(client.getBuffer(1)).toBeUndefined()
1214
+
1215
+ await client.destroy()
1216
+ })
1217
+
1218
+ test("should handle destroy() during pending initialization", async () => {
1219
+ const client = new TreeSitterClient({ dataPath })
1220
+
1221
+ // Start init but don't await
1222
+ const initPromise = client.initialize()
1223
+ void initPromise.catch(() => {})
1224
+
1225
+ // Immediately destroy
1226
+ await client.destroy()
1227
+
1228
+ // Init promise should reject with specific error
1229
+ await expect(initPromise).rejects.toThrow("Client destroyed during initialization")
1230
+
1231
+ expect(client.isInitialized()).toBe(false)
1232
+ })
1233
+
1234
+ test("should reject initialization while worker termination is pending", async () => {
1235
+ const client = new TreeSitterClient({ dataPath })
1236
+ await client.initialize()
1237
+
1238
+ const internals = client as unknown as {
1239
+ worker?: { terminate: () => void | Promise<number> }
1240
+ }
1241
+ const worker = internals.worker
1242
+ expect(worker).toBeDefined()
1243
+ if (!worker) {
1244
+ throw new Error("Expected initialized client to have a worker")
1245
+ }
1246
+
1247
+ let resolveTermination!: () => void
1248
+ const terminationGate = new Promise<void>((resolve) => {
1249
+ resolveTermination = resolve
1250
+ })
1251
+ const originalTerminate = worker.terminate.bind(worker)
1252
+ worker.terminate = async () => {
1253
+ await terminationGate
1254
+ const result = originalTerminate()
1255
+ return result && typeof (result as PromiseLike<number>).then === "function" ? await result : 0
1256
+ }
1257
+
1258
+ const destroyPromise = client.destroy()
1259
+
1260
+ try {
1261
+ await expect(client.initialize()).rejects.toThrow("Cannot initialize while client is being destroyed")
1262
+ expect(client.isInitialized()).toBe(false)
1263
+
1264
+ resolveTermination()
1265
+ await destroyPromise
1266
+
1267
+ await client.initialize()
1268
+ expect(client.isInitialized()).toBe(true)
1269
+ expect(internals.worker).not.toBe(worker)
1270
+ } finally {
1271
+ resolveTermination()
1272
+ await destroyPromise
1273
+ await client.destroy()
1274
+ }
1275
+ })
1276
+
1277
+ test("should retain the worker when termination fails so destroy can be retried", async () => {
1278
+ const client = new TreeSitterClient({ dataPath })
1279
+ await client.initialize()
1280
+
1281
+ const internals = client as unknown as {
1282
+ worker?: { terminate: () => void | Promise<number> }
1283
+ }
1284
+ const worker = internals.worker
1285
+ expect(worker).toBeDefined()
1286
+ if (!worker) {
1287
+ throw new Error("Expected initialized client to have a worker")
1288
+ }
1289
+
1290
+ const originalTerminate = worker.terminate.bind(worker)
1291
+ worker.terminate = async () => {
1292
+ throw new Error("synthetic termination failure")
1293
+ }
1294
+
1295
+ await expect(client.destroy()).rejects.toThrow("synthetic termination failure")
1296
+ expect(internals.worker).toBe(worker)
1297
+ await expect(client.initialize()).rejects.toThrow("retry destroy()")
1298
+
1299
+ worker.terminate = originalTerminate
1300
+ await client.destroy()
1301
+ expect(internals.worker).toBeUndefined()
1302
+ })
1303
+
1304
+ test("should reject pending requests when an initialized worker errors", async () => {
1305
+ const client = new TreeSitterClient({ dataPath })
1306
+ await client.initialize()
1307
+
1308
+ const internals = client as unknown as {
1309
+ messageCallbacks: Map<string, unknown>
1310
+ worker?: {
1311
+ onerror: ((event: { message: string; error?: unknown }) => void) | null
1312
+ postMessage: (message: { type?: string }) => void
1313
+ }
1314
+ }
1315
+ const worker = internals.worker
1316
+ expect(worker).toBeDefined()
1317
+ if (!worker) {
1318
+ throw new Error("Expected initialized client to have a worker")
1319
+ }
1320
+
1321
+ const originalPostMessage = worker.postMessage.bind(worker)
1322
+ const blockedTypes = new Set(["GET_PERFORMANCE", "PRELOAD_PARSER", "ONESHOT_HIGHLIGHT"])
1323
+ worker.postMessage = (message) => {
1324
+ if (!blockedTypes.has(message.type ?? "")) {
1325
+ originalPostMessage(message)
1326
+ }
1327
+ }
1328
+ const observe = <T>(promise: Promise<T>) =>
1329
+ promise.then(
1330
+ (value) => ({ status: "fulfilled" as const, value }),
1331
+ (error: unknown) => ({ status: "rejected" as const, error }),
1332
+ )
1333
+ const outcomes = [
1334
+ observe(client.getPerformance()),
1335
+ observe(client.preloadParser("javascript")),
1336
+ observe(client.highlightOnce("const value = 1", "javascript")),
1337
+ ]
1338
+
1339
+ try {
1340
+ expect(internals.messageCallbacks.size).toBe(3)
1341
+ expect(worker.onerror).not.toBeNull()
1342
+ worker.onerror?.({ message: "synthetic post-init failure" })
1343
+
1344
+ expect(client.isInitialized()).toBe(false)
1345
+ expect(internals.messageCallbacks.size).toBe(0)
1346
+ for (const outcome of await Promise.all(outcomes)) {
1347
+ expect(outcome.status).toBe("rejected")
1348
+ if (outcome.status === "rejected") {
1349
+ expect(outcome.error).toBeInstanceOf(Error)
1350
+ expect((outcome.error as Error).message).toContain("synthetic post-init failure")
1351
+ }
1352
+ }
1353
+ } finally {
1354
+ await client.destroy()
1355
+ await Promise.all(outcomes)
1356
+ }
1357
+ })
1358
+
1359
+ test("should handle worker errors gracefully", async () => {
1360
+ const client = new TreeSitterClient({ dataPath })
1361
+
1362
+ let errorReceived = false
1363
+ client.on("error", () => {
1364
+ errorReceived = true
1365
+ })
1366
+
1367
+ const hasParser = await client.createBuffer(1, "test", "javascript", 1, false)
1368
+ expect(hasParser).toBe(false)
1369
+ expect(errorReceived).toBe(true)
1370
+
1371
+ await client.destroy()
1372
+ })
1373
+
1374
+ test("should handle data path changes with reactive getTreeSitterClient", async () => {
1375
+ const originalXdgDataHome = process.env.XDG_DATA_HOME
1376
+
1377
+ process.env.XDG_DATA_HOME = reactiveDataPathRoot
1378
+ clearEnvCache()
1379
+ destroySingleton("data-paths-opentui")
1380
+ await destroyTreeSitterClient()
1381
+
1382
+ const dataPathsManager = getDataPaths()
1383
+ let client: any
1384
+
1385
+ try {
1386
+ client = getTreeSitterClient()
1387
+ await client.initialize()
1388
+
1389
+ const initialDataPath = dataPathsManager.globalDataPath
1390
+
1391
+ dataPathsManager.appName = "test-app-changed"
1392
+
1393
+ await new Promise((resolve) => setTimeout(resolve, 100))
1394
+
1395
+ const newDataPath = dataPathsManager.globalDataPath
1396
+ expect(newDataPath).not.toBe(initialDataPath)
1397
+ expect(newDataPath).toContain("test-app-changed")
1398
+
1399
+ if (!client.isInitialized()) {
1400
+ await client.initialize()
1401
+ }
1402
+
1403
+ const hasParser = await client.preloadParser("javascript")
1404
+ expect(hasParser).toBe(true)
1405
+ } finally {
1406
+ await destroyTreeSitterClient()
1407
+ destroySingleton("data-paths-opentui")
1408
+
1409
+ if (originalXdgDataHome === undefined) {
1410
+ delete process.env.XDG_DATA_HOME
1411
+ } else {
1412
+ process.env.XDG_DATA_HOME = originalXdgDataHome
1413
+ }
1414
+ clearEnvCache()
1415
+ }
1416
+ })
1417
+
1418
+ test("should remove the reactive data path listener when the singleton client is destroyed", async () => {
1419
+ await destroyTreeSitterClient()
1420
+ destroySingleton("data-paths-opentui")
1421
+
1422
+ const dataPathsManager = getDataPaths()
1423
+
1424
+ expect(dataPathsManager.listenerCount("paths:changed")).toBe(0)
1425
+
1426
+ getTreeSitterClient()
1427
+ expect(dataPathsManager.listenerCount("paths:changed")).toBe(1)
1428
+
1429
+ await destroyTreeSitterClient()
1430
+ expect(dataPathsManager.listenerCount("paths:changed")).toBe(0)
1431
+
1432
+ destroySingleton("data-paths-opentui")
1433
+ })
1434
+ })