@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,41 @@
1
+ // OSC 52 clipboard support for terminal applications.
2
+ // Delegates to native Zig implementation for ANSI sequence generation.
3
+
4
+ import type { RendererHandle, RenderLib } from "../zig.js"
5
+
6
+ export enum ClipboardTarget {
7
+ Clipboard = 0,
8
+ Primary = 1,
9
+ Select = 2,
10
+ Secondary = 3,
11
+ }
12
+
13
+ export class Clipboard {
14
+ private lib: RenderLib
15
+ private rendererPtr: RendererHandle
16
+
17
+ constructor(lib: RenderLib, rendererPtr: RendererHandle) {
18
+ this.lib = lib
19
+ this.rendererPtr = rendererPtr
20
+ }
21
+
22
+ public copyToClipboardOSC52(text: string, target: ClipboardTarget = ClipboardTarget.Clipboard): boolean {
23
+ if (!this.isOsc52Supported()) {
24
+ return false
25
+ }
26
+ const textUtf8 = this.lib.encoder.encode(text)
27
+ return this.lib.copyToClipboardOSC52(this.rendererPtr, target, textUtf8)
28
+ }
29
+
30
+ public clearClipboardOSC52(target: ClipboardTarget = ClipboardTarget.Clipboard): boolean {
31
+ if (!this.isOsc52Supported()) {
32
+ return false
33
+ }
34
+ return this.lib.clearClipboardOSC52(this.rendererPtr, target)
35
+ }
36
+
37
+ public isOsc52Supported(): boolean {
38
+ const caps = this.lib.getTerminalCapabilities(this.rendererPtr)
39
+ return caps?.osc52_support !== "unsupported"
40
+ }
41
+ }
@@ -0,0 +1,35 @@
1
+ export type TimerHandle = ReturnType<typeof globalThis.setTimeout> | number
2
+
3
+ export interface Clock {
4
+ now(): number
5
+ setTimeout(fn: () => void, delayMs: number): TimerHandle
6
+ clearTimeout(handle: TimerHandle): void
7
+ setInterval(fn: () => void, delayMs: number): TimerHandle
8
+ clearInterval(handle: TimerHandle): void
9
+ }
10
+
11
+ export class SystemClock implements Clock {
12
+ public now(): number {
13
+ if (!globalThis.performance || typeof globalThis.performance.now !== "function") {
14
+ throw new Error("SystemClock requires globalThis.performance.now()")
15
+ }
16
+
17
+ return globalThis.performance.now()
18
+ }
19
+
20
+ public setTimeout(fn: () => void, delayMs: number): TimerHandle {
21
+ return globalThis.setTimeout(fn, delayMs)
22
+ }
23
+
24
+ public clearTimeout(handle: TimerHandle): void {
25
+ globalThis.clearTimeout(handle as ReturnType<typeof globalThis.setTimeout>)
26
+ }
27
+
28
+ public setInterval(fn: () => void, delayMs: number): TimerHandle {
29
+ return globalThis.setInterval(fn, delayMs)
30
+ }
31
+
32
+ public clearInterval(handle: TimerHandle): void {
33
+ globalThis.clearInterval(handle as ReturnType<typeof globalThis.setTimeout>)
34
+ }
35
+ }
@@ -0,0 +1,133 @@
1
+ import { test, expect } from "bun:test"
2
+ import { DataPathsManager } from "./data-paths.js"
3
+
4
+ test("DataPathsManager validates appName", () => {
5
+ const manager = new DataPathsManager()
6
+
7
+ // Valid names should work
8
+ expect(() => {
9
+ manager.appName = "myapp"
10
+ }).not.toThrow()
11
+
12
+ expect(() => {
13
+ manager.appName = "my-app"
14
+ }).not.toThrow()
15
+
16
+ expect(() => {
17
+ manager.appName = "my_app"
18
+ }).not.toThrow()
19
+
20
+ expect(() => {
21
+ manager.appName = "MyApp123"
22
+ }).not.toThrow()
23
+
24
+ // Invalid names should throw
25
+ expect(() => {
26
+ manager.appName = ""
27
+ }).toThrow("Invalid app name")
28
+
29
+ expect(() => {
30
+ manager.appName = " "
31
+ }).toThrow("Invalid app name")
32
+
33
+ expect(() => {
34
+ manager.appName = "app/name"
35
+ }).toThrow("Invalid app name")
36
+
37
+ expect(() => {
38
+ manager.appName = "app\\name"
39
+ }).toThrow("Invalid app name")
40
+
41
+ expect(() => {
42
+ manager.appName = "app<name"
43
+ }).toThrow("Invalid app name")
44
+
45
+ expect(() => {
46
+ manager.appName = "app>name"
47
+ }).toThrow("Invalid app name")
48
+
49
+ expect(() => {
50
+ manager.appName = 'app"name'
51
+ }).toThrow("Invalid app name")
52
+
53
+ expect(() => {
54
+ manager.appName = "app|name"
55
+ }).toThrow("Invalid app name")
56
+
57
+ expect(() => {
58
+ manager.appName = "app?name"
59
+ }).toThrow("Invalid app name")
60
+
61
+ expect(() => {
62
+ manager.appName = "app*name"
63
+ }).toThrow("Invalid app name")
64
+
65
+ expect(() => {
66
+ manager.appName = "CON"
67
+ }).toThrow("Invalid app name")
68
+
69
+ expect(() => {
70
+ manager.appName = "PRN"
71
+ }).toThrow("Invalid app name")
72
+
73
+ expect(() => {
74
+ manager.appName = "app."
75
+ }).toThrow("Invalid app name")
76
+
77
+ expect(() => {
78
+ manager.appName = "app "
79
+ }).toThrow("Invalid app name")
80
+
81
+ expect(() => {
82
+ manager.appName = "."
83
+ }).toThrow("Invalid app name")
84
+
85
+ expect(() => {
86
+ manager.appName = ".."
87
+ }).toThrow("Invalid app name")
88
+ })
89
+
90
+ test("DataPathsManager constructor uses valid default appName", () => {
91
+ // Should not throw when creating a new instance
92
+ expect(() => {
93
+ new DataPathsManager()
94
+ }).not.toThrow()
95
+
96
+ const manager = new DataPathsManager()
97
+ expect(manager.appName).toBe("opentui")
98
+ })
99
+
100
+ test("DataPathsManager emits paths:changed event when appName changes", async () => {
101
+ const manager = new DataPathsManager()
102
+ let eventFired = false
103
+ let eventPaths: any = null
104
+
105
+ manager.on("paths:changed", (paths) => {
106
+ eventFired = true
107
+ eventPaths = paths
108
+ })
109
+
110
+ const originalAppName = manager.appName
111
+ manager.appName = "test-app-event"
112
+
113
+ expect(eventFired).toBe(true)
114
+ expect(eventPaths).toBeDefined()
115
+ expect(eventPaths.globalDataPath).toContain("test-app-event")
116
+ expect(eventPaths.globalConfigPath).toContain("test-app-event")
117
+ expect(eventPaths.globalConfigFile).toContain("test-app-event")
118
+ expect(eventPaths.localConfigFile).toContain("test-app-event")
119
+ })
120
+
121
+ test("DataPathsManager does not emit event when appName is set to same value", () => {
122
+ const manager = new DataPathsManager()
123
+ let eventFired = false
124
+
125
+ manager.on("paths:changed", () => {
126
+ eventFired = true
127
+ })
128
+
129
+ // Set to the same value
130
+ manager.appName = manager.appName
131
+
132
+ expect(eventFired).toBe(false)
133
+ })
@@ -0,0 +1,109 @@
1
+ import os from "os"
2
+ import path from "path"
3
+ import { EventEmitter } from "events"
4
+ import { singleton } from "./singleton.js"
5
+ import { env, registerEnvVar } from "./env.js"
6
+ import { isValidDirectoryName } from "./validate-dir-name.js"
7
+
8
+ // Register environment variables for XDG directories
9
+ registerEnvVar({
10
+ name: "XDG_CONFIG_HOME",
11
+ description: "Base directory for user-specific configuration files",
12
+ type: "string",
13
+ default: "",
14
+ })
15
+
16
+ registerEnvVar({
17
+ name: "XDG_DATA_HOME",
18
+ description: "Base directory for user-specific data files",
19
+ type: "string",
20
+ default: "",
21
+ })
22
+
23
+ export interface DataPaths {
24
+ globalConfigPath: string
25
+ globalConfigFile: string
26
+ localConfigFile: string
27
+ globalDataPath: string
28
+ }
29
+
30
+ export interface DataPathsEvents {
31
+ "paths:changed": [paths: DataPaths]
32
+ }
33
+
34
+ export class DataPathsManager extends EventEmitter<DataPathsEvents> {
35
+ private _appName: string
36
+ private _globalConfigPath?: string
37
+ private _globalConfigFile?: string
38
+ private _localConfigFile?: string
39
+ private _globalDataPath?: string
40
+ constructor() {
41
+ super()
42
+ this._appName = "opentui"
43
+ }
44
+
45
+ get appName(): string {
46
+ return this._appName
47
+ }
48
+
49
+ set appName(value: string) {
50
+ if (!isValidDirectoryName(value)) {
51
+ throw new Error(`Invalid app name "${value}": must be a valid directory name`)
52
+ }
53
+ if (this._appName !== value) {
54
+ this._appName = value
55
+ this._globalConfigPath = undefined
56
+ this._globalConfigFile = undefined
57
+ this._localConfigFile = undefined
58
+ this._globalDataPath = undefined
59
+ this.emit("paths:changed", this.toObject())
60
+ }
61
+ }
62
+
63
+ get globalConfigPath(): string {
64
+ if (this._globalConfigPath === undefined) {
65
+ const homeDir = os.homedir()
66
+ const xdgConfigHome = env.XDG_CONFIG_HOME
67
+ const baseConfigDir = xdgConfigHome || path.join(homeDir, ".config")
68
+ this._globalConfigPath = path.join(baseConfigDir, this._appName)
69
+ }
70
+ return this._globalConfigPath
71
+ }
72
+
73
+ get globalConfigFile(): string {
74
+ if (this._globalConfigFile === undefined) {
75
+ this._globalConfigFile = path.join(this.globalConfigPath, "init.ts")
76
+ }
77
+ return this._globalConfigFile
78
+ }
79
+
80
+ get localConfigFile(): string {
81
+ if (this._localConfigFile === undefined) {
82
+ this._localConfigFile = path.join(process.cwd(), `.${this._appName}.ts`)
83
+ }
84
+ return this._localConfigFile
85
+ }
86
+
87
+ get globalDataPath(): string {
88
+ if (this._globalDataPath === undefined) {
89
+ const homeDir = os.homedir()
90
+ const xdgDataHome = env.XDG_DATA_HOME
91
+ const baseDataDir = xdgDataHome || path.join(homeDir, ".local/share")
92
+ this._globalDataPath = path.join(baseDataDir, this._appName)
93
+ }
94
+ return this._globalDataPath
95
+ }
96
+
97
+ toObject(): DataPaths {
98
+ return {
99
+ globalConfigPath: this.globalConfigPath,
100
+ globalConfigFile: this.globalConfigFile,
101
+ localConfigFile: this.localConfigFile,
102
+ globalDataPath: this.globalDataPath,
103
+ }
104
+ }
105
+ }
106
+
107
+ export function getDataPaths(): DataPathsManager {
108
+ return singleton("data-paths-opentui", () => new DataPathsManager())
109
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * A module-level map to store timeout IDs for all debounced functions
3
+ * Structure: Map<scopeId, Map<debounceId, timerId>>
4
+ */
5
+ const TIMERS_MAP = new Map<string | number, Map<string | number, ReturnType<typeof setTimeout>>>()
6
+
7
+ /**
8
+ * Debounce controller that manages debounce instances for a specific scope
9
+ */
10
+ export class DebounceController {
11
+ constructor(private scopeId: string | number) {
12
+ // Initialize the scope map if it doesn't exist
13
+ if (!TIMERS_MAP.has(this.scopeId)) {
14
+ TIMERS_MAP.set(this.scopeId, new Map())
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Debounces the provided function with the given ID
20
+ *
21
+ * @param id Unique identifier within this scope
22
+ * @param ms Milliseconds to wait before executing
23
+ * @param fn Function to execute
24
+ */
25
+ debounce<R>(id: string | number, ms: number, fn: () => Promise<R>): Promise<R> {
26
+ const scopeMap = TIMERS_MAP.get(this.scopeId)!
27
+
28
+ return new Promise((resolve, reject) => {
29
+ // Clear any existing timeout for this ID
30
+ if (scopeMap.has(id)) {
31
+ clearTimeout(scopeMap.get(id))
32
+ }
33
+
34
+ // Set a new timeout
35
+ const timerId = setTimeout(() => {
36
+ try {
37
+ resolve(fn())
38
+ } catch (error) {
39
+ reject(error)
40
+ }
41
+ scopeMap.delete(id)
42
+ }, ms)
43
+
44
+ // Store the new timeout ID
45
+ scopeMap.set(id, timerId)
46
+ })
47
+ }
48
+
49
+ /**
50
+ * Clear a specific debounce timer in this scope
51
+ *
52
+ * @param id The debounce ID to clear
53
+ */
54
+ clearDebounce(id: string | number): void {
55
+ const scopeMap = TIMERS_MAP.get(this.scopeId)
56
+ if (scopeMap && scopeMap.has(id)) {
57
+ clearTimeout(scopeMap.get(id))
58
+ scopeMap.delete(id)
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Clear all debounce timers in this scope
64
+ */
65
+ clear(): void {
66
+ const scopeMap = TIMERS_MAP.get(this.scopeId)
67
+ if (scopeMap) {
68
+ scopeMap.forEach((timerId) => clearTimeout(timerId))
69
+ scopeMap.clear()
70
+ }
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Creates a new debounce controller for a specific scope
76
+ *
77
+ * @param scopeId Unique identifier for this debounce scope
78
+ * @returns A DebounceController for the specified scope
79
+ */
80
+ export function createDebounce(scopeId: string | number): DebounceController {
81
+ return new DebounceController(scopeId)
82
+ }
83
+
84
+ /**
85
+ * Clears all debounce timers for a specific scope
86
+ *
87
+ * @param scopeId The scope identifier
88
+ */
89
+ export function clearDebounceScope(scopeId: string | number): void {
90
+ const scopeMap = TIMERS_MAP.get(scopeId)
91
+ if (scopeMap) {
92
+ scopeMap.forEach((timerId) => clearTimeout(timerId))
93
+ scopeMap.clear()
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Clears all active debounce timers across all scopes
99
+ */
100
+ export function clearAllDebounces(): void {
101
+ TIMERS_MAP.forEach((scopeMap) => {
102
+ scopeMap.forEach((timerId) => clearTimeout(timerId))
103
+ scopeMap.clear()
104
+ })
105
+ TIMERS_MAP.clear()
106
+ }
@@ -0,0 +1,98 @@
1
+ import { test, expect, describe } from "bun:test"
2
+ import { detectLinks } from "./detect-links.js"
3
+ import type { TextChunk } from "../text-buffer.js"
4
+ import type { SimpleHighlight } from "./tree-sitter/types.js"
5
+ import { RGBA } from "./RGBA.js"
6
+
7
+ function chunk(text: string): TextChunk {
8
+ return { __isChunk: true, text, fg: RGBA.fromInts(255, 255, 255, 255), attributes: 0 }
9
+ }
10
+
11
+ describe("detectLinks", () => {
12
+ test("should set link on markup.link.url chunks", () => {
13
+ const content = "[Click here](https://example.com)"
14
+ const highlights: SimpleHighlight[] = [
15
+ [0, 1, "markup.link"],
16
+ [1, 11, "markup.link.label"],
17
+ [11, 13, "markup.link"],
18
+ [13, 32, "markup.link.url"],
19
+ [32, 33, "markup.link"],
20
+ ]
21
+ const chunks = [chunk("["), chunk("Click here"), chunk("]("), chunk("https://example.com"), chunk(")")]
22
+
23
+ const result = detectLinks(chunks, { content, highlights })
24
+
25
+ expect(result.find((c) => c.text === "https://example.com")!.link).toEqual({ url: "https://example.com" })
26
+ expect(result.find((c) => c.text === "Click here")!.link).toEqual({ url: "https://example.com" })
27
+ })
28
+
29
+ test("should set link on string.special.url chunks", () => {
30
+ const content = "// see https://example.com for details"
31
+ const highlights: SimpleHighlight[] = [
32
+ [0, 38, "comment"],
33
+ [7, 26, "string.special.url"],
34
+ ]
35
+ const chunks = [chunk("// see "), chunk("https://example.com"), chunk(" for details")]
36
+
37
+ const result = detectLinks(chunks, { content, highlights })
38
+
39
+ expect(result.find((c) => c.text === "https://example.com")!.link).toEqual({ url: "https://example.com" })
40
+ })
41
+
42
+ test("should not set link on non-URL chunks", () => {
43
+ const content = "const x = 42"
44
+ const highlights: SimpleHighlight[] = [
45
+ [0, 5, "keyword"],
46
+ [6, 7, "variable"],
47
+ [10, 12, "number"],
48
+ ]
49
+ const chunks = [chunk("const"), chunk(" "), chunk("x"), chunk(" = "), chunk("42")]
50
+
51
+ const result = detectLinks(chunks, { content, highlights })
52
+
53
+ for (const c of result) {
54
+ expect(c.link).toBeUndefined()
55
+ }
56
+ })
57
+
58
+ test("should return chunks unchanged when no URL scopes exist", () => {
59
+ const content = "hello world"
60
+ const highlights: SimpleHighlight[] = [[0, 5, "keyword"]]
61
+ const chunks = [chunk("hello"), chunk(" world")]
62
+
63
+ const result = detectLinks(chunks, { content, highlights })
64
+
65
+ expect(result).toBe(chunks)
66
+ })
67
+
68
+ test("should detect links when chunks have concealed text", () => {
69
+ // Original content: [Click here](https://example.com)
70
+ // With concealment, `[` and `]` are concealed to empty strings,
71
+ // and `(` and `)` are concealed to empty strings.
72
+ // This means chunk text lengths don't match original byte offsets.
73
+ const content = "[Click here](https://example.com)"
74
+ const highlights: SimpleHighlight[] = [
75
+ [0, 1, "markup.link"], // [
76
+ [1, 11, "markup.link.label"], // Click here
77
+ [11, 13, "markup.link"], // ](
78
+ [13, 32, "markup.link.url"], // https://example.com
79
+ [32, 33, "markup.link"], // )
80
+ ]
81
+ // Simulate concealed chunks: `[` -> "", `](` -> " ", `)` -> ""
82
+ // The URL and label chunks remain unchanged.
83
+ const chunks = [
84
+ chunk(""), // concealed `[`
85
+ chunk("Click here"), // label, unchanged
86
+ chunk(" "), // concealed `](`
87
+ chunk("https://example.com"), // URL, unchanged
88
+ chunk(""), // concealed `)`
89
+ ]
90
+
91
+ const result = detectLinks(chunks, { content, highlights })
92
+
93
+ // The URL chunk should still get its link despite concealed offsets
94
+ expect(result.find((c) => c.text === "https://example.com")!.link).toEqual({ url: "https://example.com" })
95
+ // The label chunk should also get the link
96
+ expect(result.find((c) => c.text === "Click here")!.link).toEqual({ url: "https://example.com" })
97
+ })
98
+ })
@@ -0,0 +1,56 @@
1
+ import type { TextChunk } from "../text-buffer.js"
2
+ import type { SimpleHighlight } from "./tree-sitter/types.js"
3
+
4
+ const URL_SCOPES = ["markup.link.url", "string.special.url"]
5
+
6
+ export function detectLinks(
7
+ chunks: TextChunk[],
8
+ context: { content: string; highlights: SimpleHighlight[] },
9
+ ): TextChunk[] {
10
+ const content = context.content
11
+ const highlights = context.highlights
12
+
13
+ const ranges: Array<{ start: number; end: number; url: string }> = []
14
+
15
+ for (let i = 0; i < highlights.length; i++) {
16
+ const [start, end, group] = highlights[i]
17
+ if (!URL_SCOPES.includes(group)) continue
18
+
19
+ const url = content.slice(start, end)
20
+ ranges.push({ start, end, url })
21
+
22
+ for (let j = i - 1; j >= 0; j--) {
23
+ const [labelStart, labelEnd, prev] = highlights[j]
24
+ if (prev === "markup.link.label") {
25
+ ranges.push({ start: labelStart, end: labelEnd, url })
26
+ break
27
+ }
28
+ if (!prev.startsWith("markup.link")) break
29
+ }
30
+ }
31
+
32
+ if (ranges.length === 0) return chunks
33
+
34
+ // Use content.indexOf to find each chunk's position in the original content.
35
+ // This handles concealed text correctly because concealed chunks are either
36
+ // empty (length 0, skipped) or single-char replacements (length 1, skipped).
37
+ // Non-concealed chunks with length > 1 are exact substrings of content in order.
38
+ let contentPos = 0
39
+ for (const chunk of chunks) {
40
+ if (chunk.text.length <= 1) continue
41
+
42
+ const idx = content.indexOf(chunk.text, contentPos)
43
+ if (idx < 0) continue
44
+
45
+ for (const range of ranges) {
46
+ if (idx < range.end && idx + chunk.text.length > range.start) {
47
+ chunk.link = { url: range.url }
48
+ break
49
+ }
50
+ }
51
+
52
+ contentPos = idx + chunk.text.length
53
+ }
54
+
55
+ return chunks
56
+ }