@fairyhunter13/opentui-core 0.1.113 → 0.1.114

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 (591) hide show
  1. package/dev/keypress-debug-renderer.ts +148 -0
  2. package/dev/keypress-debug.ts +43 -0
  3. package/dev/print-env-vars.ts +32 -0
  4. package/dev/test-tmux-graphics-334.sh +68 -0
  5. package/dev/thai-debug-test.ts +68 -0
  6. package/docs/development.md +144 -0
  7. package/package.json +62 -53
  8. package/scripts/build.ts +400 -0
  9. package/scripts/publish.ts +60 -0
  10. package/src/3d/SpriteResourceManager.ts +286 -0
  11. package/src/3d/SpriteUtils.ts +70 -0
  12. package/src/3d/TextureUtils.ts +196 -0
  13. package/src/3d/ThreeRenderable.ts +197 -0
  14. package/src/3d/WGPURenderer.ts +294 -0
  15. package/src/3d/animation/ExplodingSpriteEffect.ts +513 -0
  16. package/src/3d/animation/PhysicsExplodingSpriteEffect.ts +429 -0
  17. package/src/3d/animation/SpriteAnimator.ts +633 -0
  18. package/src/3d/animation/SpriteParticleGenerator.ts +435 -0
  19. package/src/3d/canvas.ts +464 -0
  20. package/src/3d/index.ts +12 -0
  21. package/src/3d/physics/PlanckPhysicsAdapter.ts +72 -0
  22. package/src/3d/physics/RapierPhysicsAdapter.ts +66 -0
  23. package/src/3d/physics/physics-interface.ts +31 -0
  24. package/src/3d/shaders/supersampling.wgsl +201 -0
  25. package/src/3d.ts +3 -0
  26. package/src/NativeSpanFeed.ts +300 -0
  27. package/src/Renderable.ts +1704 -0
  28. package/src/__snapshots__/buffer.test.ts.snap +28 -0
  29. package/src/animation/Timeline.test.ts +2709 -0
  30. package/src/animation/Timeline.ts +598 -0
  31. package/src/ansi.ts +18 -0
  32. package/src/benchmark/attenuation-benchmark.ts +81 -0
  33. package/src/benchmark/colormatrix-benchmark.ts +128 -0
  34. package/src/benchmark/gain-benchmark.ts +80 -0
  35. package/src/benchmark/latest-all-bench-run.json +707 -0
  36. package/src/benchmark/latest-async-bench-run.json +336 -0
  37. package/src/benchmark/latest-default-bench-run.json +657 -0
  38. package/src/benchmark/latest-large-bench-run.json +707 -0
  39. package/src/benchmark/latest-quick-bench-run.json +207 -0
  40. package/src/benchmark/markdown-benchmark.ts +1796 -0
  41. package/src/benchmark/native-span-feed-async-benchmark.ts +355 -0
  42. package/src/benchmark/native-span-feed-benchmark.md +56 -0
  43. package/src/benchmark/native-span-feed-benchmark.ts +596 -0
  44. package/src/benchmark/native-span-feed-compare.ts +280 -0
  45. package/src/benchmark/renderer-benchmark.ts +754 -0
  46. package/src/benchmark/text-table-benchmark.ts +948 -0
  47. package/src/buffer.test.ts +291 -0
  48. package/src/buffer.ts +554 -0
  49. package/src/console.test.ts +612 -0
  50. package/src/console.ts +1254 -0
  51. package/src/edit-buffer.test.ts +1769 -0
  52. package/src/edit-buffer.ts +411 -0
  53. package/src/editor-view.test.ts +1032 -0
  54. package/src/editor-view.ts +284 -0
  55. package/src/examples/ascii-font-selection-demo.ts +245 -0
  56. package/src/examples/assets/Water_2_M_Normal.jpg +0 -0
  57. package/src/examples/assets/concrete.png +0 -0
  58. package/src/examples/assets/crate.png +0 -0
  59. package/src/examples/assets/crate_emissive.png +0 -0
  60. package/src/examples/assets/forrest_background.png +0 -0
  61. package/src/examples/assets/hast-example.json +1018 -0
  62. package/src/examples/assets/heart.png +0 -0
  63. package/src/examples/assets/main_char_heavy_attack.png +0 -0
  64. package/src/examples/assets/main_char_idle.png +0 -0
  65. package/src/examples/assets/main_char_jump_end.png +0 -0
  66. package/src/examples/assets/main_char_jump_landing.png +0 -0
  67. package/src/examples/assets/main_char_jump_start.png +0 -0
  68. package/src/examples/assets/main_char_run_loop.png +0 -0
  69. package/src/examples/assets/roughness_map.jpg +0 -0
  70. package/src/examples/build.ts +115 -0
  71. package/src/examples/code-demo.ts +924 -0
  72. package/src/examples/console-demo.ts +358 -0
  73. package/src/examples/core-plugin-slots-demo.ts +759 -0
  74. package/src/examples/diff-demo.ts +701 -0
  75. package/src/examples/draggable-three-demo.ts +259 -0
  76. package/src/examples/editor-demo.ts +322 -0
  77. package/src/examples/extmarks-demo.ts +196 -0
  78. package/src/examples/focus-restore-demo.ts +310 -0
  79. package/src/examples/fonts.ts +245 -0
  80. package/src/examples/fractal-shader-demo.ts +268 -0
  81. package/src/examples/framebuffer-demo.ts +674 -0
  82. package/src/examples/full-unicode-demo.ts +241 -0
  83. package/src/examples/golden-star-demo.ts +933 -0
  84. package/src/examples/grayscale-buffer-demo.ts +249 -0
  85. package/src/examples/hast-syntax-highlighting-demo.ts +129 -0
  86. package/src/examples/index.ts +926 -0
  87. package/src/examples/input-demo.ts +377 -0
  88. package/src/examples/input-select-layout-demo.ts +425 -0
  89. package/src/examples/install.sh +143 -0
  90. package/src/examples/keypress-debug-demo.ts +452 -0
  91. package/src/examples/lib/HexList.ts +122 -0
  92. package/src/examples/lib/PaletteGrid.ts +125 -0
  93. package/src/examples/lib/standalone-keys.ts +25 -0
  94. package/src/examples/lib/tab-controller.ts +243 -0
  95. package/src/examples/lights-phong-demo.ts +290 -0
  96. package/src/examples/link-demo.ts +220 -0
  97. package/src/examples/live-state-demo.ts +480 -0
  98. package/src/examples/markdown-demo.ts +725 -0
  99. package/src/examples/mouse-interaction-demo.ts +428 -0
  100. package/src/examples/nested-zindex-demo.ts +357 -0
  101. package/src/examples/opacity-example.ts +235 -0
  102. package/src/examples/opentui-demo.ts +1057 -0
  103. package/src/examples/physx-planck-2d-demo.ts +623 -0
  104. package/src/examples/physx-rapier-2d-demo.ts +655 -0
  105. package/src/examples/relative-positioning-demo.ts +323 -0
  106. package/src/examples/scroll-example.ts +214 -0
  107. package/src/examples/scrollbox-mouse-test.ts +112 -0
  108. package/src/examples/scrollbox-overlay-hit-test.ts +206 -0
  109. package/src/examples/select-demo.ts +237 -0
  110. package/src/examples/shader-cube-demo.ts +1015 -0
  111. package/src/examples/simple-layout-example.ts +591 -0
  112. package/src/examples/slider-demo.ts +617 -0
  113. package/src/examples/split-mode-demo.ts +453 -0
  114. package/src/examples/sprite-animation-demo.ts +443 -0
  115. package/src/examples/sprite-particle-generator-demo.ts +486 -0
  116. package/src/examples/static-sprite-demo.ts +193 -0
  117. package/src/examples/sticky-scroll-example.ts +308 -0
  118. package/src/examples/styled-text-demo.ts +282 -0
  119. package/src/examples/tab-select-demo.ts +219 -0
  120. package/src/examples/terminal-title.ts +29 -0
  121. package/src/examples/terminal.ts +305 -0
  122. package/src/examples/text-node-demo.ts +416 -0
  123. package/src/examples/text-selection-demo.ts +377 -0
  124. package/src/examples/text-table-demo.ts +503 -0
  125. package/src/examples/text-truncation-demo.ts +481 -0
  126. package/src/examples/text-wrap.ts +757 -0
  127. package/src/examples/texture-loading-demo.ts +259 -0
  128. package/src/examples/timeline-example.ts +670 -0
  129. package/src/examples/transparency-demo.ts +400 -0
  130. package/src/examples/vnode-composition-demo.ts +404 -0
  131. package/src/examples/wide-grapheme-overlay-demo.ts +280 -0
  132. package/src/index.ts +24 -0
  133. package/src/lib/KeyHandler.integration.test.ts +292 -0
  134. package/src/lib/KeyHandler.stopPropagation.test.ts +289 -0
  135. package/src/lib/KeyHandler.test.ts +662 -0
  136. package/src/lib/KeyHandler.ts +222 -0
  137. package/src/lib/RGBA.test.ts +984 -0
  138. package/src/lib/RGBA.ts +204 -0
  139. package/src/lib/ascii.font.ts +330 -0
  140. package/src/lib/border.test.ts +83 -0
  141. package/src/lib/border.ts +170 -0
  142. package/src/lib/bunfs.test.ts +27 -0
  143. package/src/lib/bunfs.ts +18 -0
  144. package/src/lib/clipboard.test.ts +41 -0
  145. package/src/lib/clipboard.ts +47 -0
  146. package/src/lib/clock.ts +35 -0
  147. package/src/lib/data-paths.test.ts +133 -0
  148. package/src/lib/data-paths.ts +109 -0
  149. package/src/lib/debounce.ts +106 -0
  150. package/src/lib/detect-links.test.ts +98 -0
  151. package/src/lib/detect-links.ts +56 -0
  152. package/src/lib/env.test.ts +228 -0
  153. package/src/lib/env.ts +209 -0
  154. package/src/lib/extmarks-history.ts +51 -0
  155. package/src/lib/extmarks-multiwidth.test.ts +322 -0
  156. package/src/lib/extmarks.test.ts +3457 -0
  157. package/src/lib/extmarks.ts +843 -0
  158. package/src/lib/fonts/block.json +405 -0
  159. package/src/lib/fonts/grid.json +265 -0
  160. package/src/lib/fonts/huge.json +741 -0
  161. package/src/lib/fonts/pallet.json +314 -0
  162. package/src/lib/fonts/shade.json +591 -0
  163. package/src/lib/fonts/slick.json +321 -0
  164. package/src/lib/fonts/tiny.json +69 -0
  165. package/src/lib/hast-styled-text.ts +59 -0
  166. package/src/lib/index.ts +21 -0
  167. package/src/lib/keymapping.test.ts +317 -0
  168. package/src/lib/keymapping.ts +115 -0
  169. package/src/lib/objects-in-viewport.test.ts +787 -0
  170. package/src/lib/objects-in-viewport.ts +153 -0
  171. package/src/lib/output.capture.ts +58 -0
  172. package/src/lib/parse.keypress-kitty.protocol.test.ts +340 -0
  173. package/src/lib/parse.keypress-kitty.test.ts +663 -0
  174. package/src/lib/parse.keypress-kitty.ts +439 -0
  175. package/src/lib/parse.keypress.test.ts +1849 -0
  176. package/src/lib/parse.keypress.ts +397 -0
  177. package/src/lib/parse.mouse.test.ts +552 -0
  178. package/src/lib/parse.mouse.ts +232 -0
  179. package/src/lib/paste.ts +16 -0
  180. package/src/lib/queue.ts +65 -0
  181. package/src/lib/renderable.validations.test.ts +87 -0
  182. package/src/lib/renderable.validations.ts +83 -0
  183. package/src/lib/scroll-acceleration.ts +98 -0
  184. package/src/lib/selection.ts +240 -0
  185. package/src/lib/singleton.ts +28 -0
  186. package/src/lib/stdin-parser.test.ts +2290 -0
  187. package/src/lib/stdin-parser.ts +1810 -0
  188. package/src/lib/styled-text.ts +178 -0
  189. package/src/lib/terminal-capability-detection.test.ts +202 -0
  190. package/src/lib/terminal-capability-detection.ts +79 -0
  191. package/src/lib/terminal-palette.test.ts +878 -0
  192. package/src/lib/terminal-palette.ts +383 -0
  193. package/src/lib/tree-sitter/assets/README.md +118 -0
  194. package/src/lib/tree-sitter/assets/update.ts +334 -0
  195. package/src/lib/tree-sitter/assets.d.ts +9 -0
  196. package/src/lib/tree-sitter/cache.test.ts +273 -0
  197. package/src/lib/tree-sitter/client.test.ts +1165 -0
  198. package/src/lib/tree-sitter/client.ts +607 -0
  199. package/src/lib/tree-sitter/default-parsers.ts +86 -0
  200. package/src/lib/tree-sitter/download-utils.ts +148 -0
  201. package/src/lib/tree-sitter/index.ts +28 -0
  202. package/src/lib/tree-sitter/parser.worker.ts +1042 -0
  203. package/src/lib/tree-sitter/parsers-config.ts +81 -0
  204. package/src/lib/tree-sitter/resolve-ft.test.ts +55 -0
  205. package/src/lib/tree-sitter/resolve-ft.ts +189 -0
  206. package/src/lib/tree-sitter/types.ts +82 -0
  207. package/src/lib/tree-sitter-styled-text.test.ts +1253 -0
  208. package/src/lib/tree-sitter-styled-text.ts +306 -0
  209. package/src/lib/validate-dir-name.ts +55 -0
  210. package/src/lib/yoga.options.test.ts +628 -0
  211. package/src/lib/yoga.options.ts +346 -0
  212. package/src/plugins/core-slot.ts +579 -0
  213. package/src/plugins/registry.ts +402 -0
  214. package/src/plugins/types.ts +46 -0
  215. package/src/post/effects.ts +930 -0
  216. package/src/post/filters.ts +489 -0
  217. package/src/post/matrices.ts +288 -0
  218. package/src/renderables/ASCIIFont.ts +219 -0
  219. package/src/renderables/Box.test.ts +205 -0
  220. package/src/renderables/Box.ts +326 -0
  221. package/src/renderables/Code.test.ts +2062 -0
  222. package/src/renderables/Code.ts +357 -0
  223. package/src/renderables/Diff.regression.test.ts +226 -0
  224. package/src/renderables/Diff.test.ts +3101 -0
  225. package/src/renderables/Diff.ts +1211 -0
  226. package/src/renderables/EditBufferRenderable.test.ts +288 -0
  227. package/src/renderables/EditBufferRenderable.ts +1166 -0
  228. package/src/renderables/FrameBuffer.ts +47 -0
  229. package/src/renderables/Input.test.ts +1228 -0
  230. package/src/renderables/Input.ts +247 -0
  231. package/src/renderables/LineNumberRenderable.ts +724 -0
  232. package/src/renderables/Markdown.ts +1393 -0
  233. package/src/renderables/ScrollBar.ts +422 -0
  234. package/src/renderables/ScrollBox.ts +883 -0
  235. package/src/renderables/Select.test.ts +1033 -0
  236. package/src/renderables/Select.ts +524 -0
  237. package/src/renderables/Slider.test.ts +456 -0
  238. package/src/renderables/Slider.ts +342 -0
  239. package/src/renderables/TabSelect.test.ts +197 -0
  240. package/src/renderables/TabSelect.ts +455 -0
  241. package/src/renderables/Text.selection-buffer.test.ts +123 -0
  242. package/src/renderables/Text.test.ts +2660 -0
  243. package/src/renderables/Text.ts +147 -0
  244. package/src/renderables/TextBufferRenderable.ts +518 -0
  245. package/src/renderables/TextNode.test.ts +1058 -0
  246. package/src/renderables/TextNode.ts +325 -0
  247. package/src/renderables/TextTable.test.ts +1421 -0
  248. package/src/renderables/TextTable.ts +1344 -0
  249. package/src/renderables/Textarea.ts +430 -0
  250. package/src/renderables/TimeToFirstDraw.ts +89 -0
  251. package/src/renderables/__snapshots__/Code.test.ts.snap +13 -0
  252. package/src/renderables/__snapshots__/Diff.test.ts.snap +785 -0
  253. package/src/renderables/__snapshots__/Text.test.ts.snap +421 -0
  254. package/src/renderables/__snapshots__/TextTable.test.ts.snap +215 -0
  255. package/src/renderables/__tests__/LineNumberRenderable.scrollbox-simple.test.ts +144 -0
  256. package/src/renderables/__tests__/LineNumberRenderable.scrollbox.test.ts +816 -0
  257. package/src/renderables/__tests__/LineNumberRenderable.test.ts +1865 -0
  258. package/src/renderables/__tests__/LineNumberRenderable.wrapping.test.ts +85 -0
  259. package/src/renderables/__tests__/Markdown.code-colors.test.ts +242 -0
  260. package/src/renderables/__tests__/Markdown.test.ts +2518 -0
  261. package/src/renderables/__tests__/MultiRenderable.selection.test.ts +87 -0
  262. package/src/renderables/__tests__/Textarea.buffer.test.ts +682 -0
  263. package/src/renderables/__tests__/Textarea.destroyed-events.test.ts +675 -0
  264. package/src/renderables/__tests__/Textarea.editing.test.ts +2041 -0
  265. package/src/renderables/__tests__/Textarea.error-handling.test.ts +35 -0
  266. package/src/renderables/__tests__/Textarea.events.test.ts +738 -0
  267. package/src/renderables/__tests__/Textarea.highlights.test.ts +590 -0
  268. package/src/renderables/__tests__/Textarea.keybinding.test.ts +3149 -0
  269. package/src/renderables/__tests__/Textarea.paste.test.ts +357 -0
  270. package/src/renderables/__tests__/Textarea.rendering.test.ts +1866 -0
  271. package/src/renderables/__tests__/Textarea.scroll.test.ts +733 -0
  272. package/src/renderables/__tests__/Textarea.selection.test.ts +1590 -0
  273. package/src/renderables/__tests__/Textarea.stress.test.ts +670 -0
  274. package/src/renderables/__tests__/Textarea.undo-redo.test.ts +383 -0
  275. package/src/renderables/__tests__/Textarea.visual-lines.test.ts +310 -0
  276. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.code.test.ts.snap +221 -0
  277. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox-simple.test.ts.snap +89 -0
  278. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox.test.ts.snap +457 -0
  279. package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.test.ts.snap +158 -0
  280. package/src/renderables/__tests__/__snapshots__/Textarea.rendering.test.ts.snap +387 -0
  281. package/src/renderables/__tests__/markdown-parser.test.ts +217 -0
  282. package/src/renderables/__tests__/renderable-test-utils.ts +60 -0
  283. package/src/renderables/composition/README.md +8 -0
  284. package/src/renderables/composition/VRenderable.ts +32 -0
  285. package/src/renderables/composition/constructs.ts +127 -0
  286. package/src/renderables/composition/vnode.ts +289 -0
  287. package/src/renderables/index.ts +23 -0
  288. package/src/renderables/markdown-parser.ts +66 -0
  289. package/src/renderer.ts +2681 -0
  290. package/src/runtime-plugin-support.ts +39 -0
  291. package/src/runtime-plugin.ts +615 -0
  292. package/src/syntax-style.test.ts +841 -0
  293. package/src/syntax-style.ts +257 -0
  294. package/src/testing/README.md +210 -0
  295. package/src/testing/capture-spans.test.ts +194 -0
  296. package/src/testing/integration.test.ts +276 -0
  297. package/src/testing/manual-clock.ts +117 -0
  298. package/src/testing/mock-keys.test.ts +1378 -0
  299. package/src/testing/mock-keys.ts +457 -0
  300. package/src/testing/mock-mouse.test.ts +218 -0
  301. package/src/testing/mock-mouse.ts +247 -0
  302. package/src/testing/mock-tree-sitter-client.ts +73 -0
  303. package/src/testing/spy.ts +13 -0
  304. package/src/testing/test-recorder.test.ts +415 -0
  305. package/src/testing/test-recorder.ts +145 -0
  306. package/src/testing/test-renderer.ts +132 -0
  307. package/src/testing.ts +7 -0
  308. package/src/tests/__snapshots__/absolute-positioning.snapshot.test.ts.snap +481 -0
  309. package/src/tests/__snapshots__/renderable.snapshot.test.ts.snap +19 -0
  310. package/src/tests/__snapshots__/scrollbox.test.ts.snap +29 -0
  311. package/src/tests/absolute-positioning.snapshot.test.ts +638 -0
  312. package/src/tests/allocator-stats.test.ts +38 -0
  313. package/src/tests/destroy-during-render.test.ts +200 -0
  314. package/src/tests/destroy-on-exit.fixture.ts +36 -0
  315. package/src/tests/destroy-on-exit.test.ts +41 -0
  316. package/src/tests/hover-cursor.test.ts +98 -0
  317. package/src/tests/native-span-feed-async.test.ts +173 -0
  318. package/src/tests/native-span-feed-close.test.ts +120 -0
  319. package/src/tests/native-span-feed-coverage.test.ts +227 -0
  320. package/src/tests/native-span-feed-edge-cases.test.ts +352 -0
  321. package/src/tests/native-span-feed-use-after-free.test.ts +45 -0
  322. package/src/tests/opacity.test.ts +123 -0
  323. package/src/tests/renderable.snapshot.test.ts +524 -0
  324. package/src/tests/renderable.test.ts +1281 -0
  325. package/src/tests/renderer.clock.test.ts +158 -0
  326. package/src/tests/renderer.console-startup.test.ts +185 -0
  327. package/src/tests/renderer.control.test.ts +425 -0
  328. package/src/tests/renderer.core-slot-binding.test.ts +952 -0
  329. package/src/tests/renderer.cursor.test.ts +26 -0
  330. package/src/tests/renderer.destroy-during-render.test.ts +147 -0
  331. package/src/tests/renderer.focus-restore.test.ts +257 -0
  332. package/src/tests/renderer.focus.test.ts +294 -0
  333. package/src/tests/renderer.idle.test.ts +219 -0
  334. package/src/tests/renderer.input.test.ts +2237 -0
  335. package/src/tests/renderer.kitty-flags.test.ts +195 -0
  336. package/src/tests/renderer.mouse.test.ts +1274 -0
  337. package/src/tests/renderer.palette.test.ts +629 -0
  338. package/src/tests/renderer.selection.test.ts +49 -0
  339. package/src/tests/renderer.slot-registry.test.ts +684 -0
  340. package/src/tests/renderer.useMouse.test.ts +47 -0
  341. package/src/tests/runtime-plugin-node-modules-cycle.fixture.ts +76 -0
  342. package/src/tests/runtime-plugin-node-modules-mjs.fixture.ts +43 -0
  343. package/src/tests/runtime-plugin-node-modules-no-bare-rewrite.fixture.ts +67 -0
  344. package/src/tests/runtime-plugin-node-modules-package-type-cache.fixture.ts +72 -0
  345. package/src/tests/runtime-plugin-node-modules-runtime-specifier.fixture.ts +44 -0
  346. package/src/tests/runtime-plugin-node-modules-scoped-package-bare-rewrite.fixture.ts +85 -0
  347. package/src/tests/runtime-plugin-path-alias.fixture.ts +43 -0
  348. package/src/tests/runtime-plugin-resolve-roots.fixture.ts +65 -0
  349. package/src/tests/runtime-plugin-support.fixture.ts +11 -0
  350. package/src/tests/runtime-plugin-support.test.ts +19 -0
  351. package/src/tests/runtime-plugin-windows-file-url.fixture.ts +30 -0
  352. package/src/tests/runtime-plugin.fixture.ts +40 -0
  353. package/src/tests/runtime-plugin.test.ts +354 -0
  354. package/src/tests/scrollbox-culling-bug.test.ts +114 -0
  355. package/src/tests/scrollbox-hitgrid-resize.test.ts +136 -0
  356. package/src/tests/scrollbox-hitgrid.test.ts +909 -0
  357. package/src/tests/scrollbox.test.ts +1530 -0
  358. package/src/tests/wrap-resize-perf.test.ts +276 -0
  359. package/src/tests/yoga-setters.test.ts +921 -0
  360. package/src/text-buffer-view.test.ts +705 -0
  361. package/src/text-buffer-view.ts +189 -0
  362. package/src/text-buffer.test.ts +347 -0
  363. package/src/text-buffer.ts +250 -0
  364. package/src/types.ts +161 -0
  365. package/src/utils.ts +88 -0
  366. package/src/zig/ansi.zig +268 -0
  367. package/src/zig/bench/README.md +50 -0
  368. package/src/zig/bench/buffer-draw-text-buffer_bench.zig +887 -0
  369. package/src/zig/bench/edit-buffer_bench.zig +476 -0
  370. package/src/zig/bench/native-span-feed_bench.zig +100 -0
  371. package/src/zig/bench/rope-markers_bench.zig +713 -0
  372. package/src/zig/bench/rope_bench.zig +514 -0
  373. package/src/zig/bench/styled-text_bench.zig +470 -0
  374. package/src/zig/bench/text-buffer-coords_bench.zig +362 -0
  375. package/src/zig/bench/text-buffer-view_bench.zig +459 -0
  376. package/src/zig/bench/text-chunk-graphemes_bench.zig +273 -0
  377. package/src/zig/bench/utf8_bench.zig +799 -0
  378. package/src/zig/bench-utils.zig +431 -0
  379. package/src/zig/bench.zig +217 -0
  380. package/src/zig/buffer-methods.zig +211 -0
  381. package/src/zig/buffer.zig +2281 -0
  382. package/src/zig/build.zig +289 -0
  383. package/src/zig/build.zig.zon +16 -0
  384. package/src/zig/edit-buffer.zig +825 -0
  385. package/src/zig/editor-view.zig +802 -0
  386. package/src/zig/event-bus.zig +13 -0
  387. package/src/zig/event-emitter.zig +65 -0
  388. package/src/zig/file-logger.zig +92 -0
  389. package/src/zig/grapheme.zig +599 -0
  390. package/src/zig/lib.zig +1854 -0
  391. package/src/zig/link.zig +333 -0
  392. package/src/zig/logger.zig +43 -0
  393. package/src/zig/mem-registry.zig +125 -0
  394. package/src/zig/native-span-feed-bench-lib.zig +7 -0
  395. package/src/zig/native-span-feed.zig +708 -0
  396. package/src/zig/renderer.zig +1393 -0
  397. package/src/zig/rope.zig +1220 -0
  398. package/src/zig/syntax-style.zig +161 -0
  399. package/src/zig/terminal.zig +987 -0
  400. package/src/zig/test.zig +72 -0
  401. package/src/zig/tests/README.md +18 -0
  402. package/src/zig/tests/buffer-methods_test.zig +1109 -0
  403. package/src/zig/tests/buffer_test.zig +2557 -0
  404. package/src/zig/tests/edit-buffer-history_test.zig +271 -0
  405. package/src/zig/tests/edit-buffer_test.zig +1689 -0
  406. package/src/zig/tests/editor-view_test.zig +3299 -0
  407. package/src/zig/tests/event-emitter_test.zig +249 -0
  408. package/src/zig/tests/grapheme_test.zig +1304 -0
  409. package/src/zig/tests/link_test.zig +190 -0
  410. package/src/zig/tests/mem-registry_test.zig +473 -0
  411. package/src/zig/tests/memory_leak_regression_test.zig +159 -0
  412. package/src/zig/tests/native-span-feed_test.zig +1264 -0
  413. package/src/zig/tests/renderer_test.zig +1017 -0
  414. package/src/zig/tests/rope-nested_test.zig +712 -0
  415. package/src/zig/tests/rope_fuzz_test.zig +238 -0
  416. package/src/zig/tests/rope_test.zig +2362 -0
  417. package/src/zig/tests/segment-merge.test.zig +148 -0
  418. package/src/zig/tests/syntax-style_test.zig +557 -0
  419. package/src/zig/tests/terminal_test.zig +754 -0
  420. package/src/zig/tests/text-buffer-drawing_test.zig +3237 -0
  421. package/src/zig/tests/text-buffer-highlights_test.zig +666 -0
  422. package/src/zig/tests/text-buffer-iterators_test.zig +776 -0
  423. package/src/zig/tests/text-buffer-segment_test.zig +320 -0
  424. package/src/zig/tests/text-buffer-selection_test.zig +1035 -0
  425. package/src/zig/tests/text-buffer-selection_viewport_test.zig +358 -0
  426. package/src/zig/tests/text-buffer-view_test.zig +3649 -0
  427. package/src/zig/tests/text-buffer_test.zig +2191 -0
  428. package/src/zig/tests/unicode-width-map.zon +3909 -0
  429. package/src/zig/tests/utf8_no_zwj_test.zig +260 -0
  430. package/src/zig/tests/utf8_test.zig +4057 -0
  431. package/src/zig/tests/utf8_wcwidth_cursor_test.zig +267 -0
  432. package/src/zig/tests/utf8_wcwidth_test.zig +357 -0
  433. package/src/zig/tests/word-wrap-editing_test.zig +498 -0
  434. package/src/zig/tests/wrap-cache-perf_test.zig +113 -0
  435. package/src/zig/text-buffer-iterators.zig +499 -0
  436. package/src/zig/text-buffer-segment.zig +404 -0
  437. package/src/zig/text-buffer-view.zig +1371 -0
  438. package/src/zig/text-buffer.zig +1180 -0
  439. package/src/zig/utf8.zig +1948 -0
  440. package/src/zig/utils.zig +9 -0
  441. package/src/zig-structs.ts +261 -0
  442. package/src/zig.ts +3884 -0
  443. package/tsconfig.build.json +24 -0
  444. package/tsconfig.json +27 -0
  445. package/3d/SpriteResourceManager.d.ts +0 -74
  446. package/3d/SpriteUtils.d.ts +0 -13
  447. package/3d/TextureUtils.d.ts +0 -24
  448. package/3d/ThreeRenderable.d.ts +0 -40
  449. package/3d/WGPURenderer.d.ts +0 -61
  450. package/3d/animation/ExplodingSpriteEffect.d.ts +0 -71
  451. package/3d/animation/PhysicsExplodingSpriteEffect.d.ts +0 -76
  452. package/3d/animation/SpriteAnimator.d.ts +0 -124
  453. package/3d/animation/SpriteParticleGenerator.d.ts +0 -62
  454. package/3d/canvas.d.ts +0 -44
  455. package/3d/index.d.ts +0 -12
  456. package/3d/physics/PlanckPhysicsAdapter.d.ts +0 -19
  457. package/3d/physics/RapierPhysicsAdapter.d.ts +0 -19
  458. package/3d/physics/physics-interface.d.ts +0 -27
  459. package/3d.d.ts +0 -2
  460. package/3d.js +0 -34041
  461. package/3d.js.map +0 -155
  462. package/LICENSE +0 -21
  463. package/NativeSpanFeed.d.ts +0 -41
  464. package/Renderable.d.ts +0 -334
  465. package/animation/Timeline.d.ts +0 -126
  466. package/ansi.d.ts +0 -13
  467. package/buffer.d.ts +0 -111
  468. package/console.d.ts +0 -144
  469. package/edit-buffer.d.ts +0 -98
  470. package/editor-view.d.ts +0 -73
  471. package/index-9vwc3fg6.js +0 -12260
  472. package/index-9vwc3fg6.js.map +0 -42
  473. package/index-dcj62y8t.js +0 -20614
  474. package/index-dcj62y8t.js.map +0 -67
  475. package/index-f7n39gpy.js +0 -411
  476. package/index-f7n39gpy.js.map +0 -10
  477. package/index.d.ts +0 -23
  478. package/index.js +0 -478
  479. package/index.js.map +0 -9
  480. package/lib/KeyHandler.d.ts +0 -61
  481. package/lib/RGBA.d.ts +0 -25
  482. package/lib/ascii.font.d.ts +0 -508
  483. package/lib/border.d.ts +0 -51
  484. package/lib/bunfs.d.ts +0 -7
  485. package/lib/clipboard.d.ts +0 -17
  486. package/lib/clock.d.ts +0 -15
  487. package/lib/data-paths.d.ts +0 -26
  488. package/lib/debounce.d.ts +0 -42
  489. package/lib/detect-links.d.ts +0 -6
  490. package/lib/env.d.ts +0 -42
  491. package/lib/extmarks-history.d.ts +0 -17
  492. package/lib/extmarks.d.ts +0 -89
  493. package/lib/hast-styled-text.d.ts +0 -17
  494. package/lib/index.d.ts +0 -21
  495. package/lib/keymapping.d.ts +0 -25
  496. package/lib/objects-in-viewport.d.ts +0 -24
  497. package/lib/output.capture.d.ts +0 -24
  498. package/lib/parse.keypress-kitty.d.ts +0 -2
  499. package/lib/parse.keypress.d.ts +0 -26
  500. package/lib/parse.mouse.d.ts +0 -30
  501. package/lib/paste.d.ts +0 -7
  502. package/lib/queue.d.ts +0 -15
  503. package/lib/renderable.validations.d.ts +0 -12
  504. package/lib/scroll-acceleration.d.ts +0 -43
  505. package/lib/selection.d.ts +0 -63
  506. package/lib/singleton.d.ts +0 -7
  507. package/lib/stdin-parser.d.ts +0 -87
  508. package/lib/styled-text.d.ts +0 -63
  509. package/lib/terminal-capability-detection.d.ts +0 -30
  510. package/lib/terminal-palette.d.ts +0 -50
  511. package/lib/tree-sitter/assets/update.d.ts +0 -11
  512. package/lib/tree-sitter/client.d.ts +0 -47
  513. package/lib/tree-sitter/default-parsers.d.ts +0 -2
  514. package/lib/tree-sitter/download-utils.d.ts +0 -21
  515. package/lib/tree-sitter/index.d.ts +0 -8
  516. package/lib/tree-sitter/parser.worker.d.ts +0 -1
  517. package/lib/tree-sitter/parsers-config.d.ts +0 -53
  518. package/lib/tree-sitter/resolve-ft.d.ts +0 -5
  519. package/lib/tree-sitter/types.d.ts +0 -82
  520. package/lib/tree-sitter-styled-text.d.ts +0 -14
  521. package/lib/validate-dir-name.d.ts +0 -1
  522. package/lib/yoga.options.d.ts +0 -32
  523. package/parser.worker.js +0 -899
  524. package/parser.worker.js.map +0 -12
  525. package/plugins/core-slot.d.ts +0 -72
  526. package/plugins/registry.d.ts +0 -42
  527. package/plugins/types.d.ts +0 -34
  528. package/post/effects.d.ts +0 -147
  529. package/post/filters.d.ts +0 -65
  530. package/post/matrices.d.ts +0 -20
  531. package/renderables/ASCIIFont.d.ts +0 -52
  532. package/renderables/Box.d.ts +0 -81
  533. package/renderables/Code.d.ts +0 -78
  534. package/renderables/Diff.d.ts +0 -142
  535. package/renderables/EditBufferRenderable.d.ts +0 -237
  536. package/renderables/FrameBuffer.d.ts +0 -16
  537. package/renderables/Input.d.ts +0 -67
  538. package/renderables/LineNumberRenderable.d.ts +0 -78
  539. package/renderables/Markdown.d.ts +0 -185
  540. package/renderables/ScrollBar.d.ts +0 -77
  541. package/renderables/ScrollBox.d.ts +0 -124
  542. package/renderables/Select.d.ts +0 -115
  543. package/renderables/Slider.d.ts +0 -47
  544. package/renderables/TabSelect.d.ts +0 -96
  545. package/renderables/Text.d.ts +0 -36
  546. package/renderables/TextBufferRenderable.d.ts +0 -105
  547. package/renderables/TextNode.d.ts +0 -91
  548. package/renderables/TextTable.d.ts +0 -140
  549. package/renderables/Textarea.d.ts +0 -63
  550. package/renderables/TimeToFirstDraw.d.ts +0 -24
  551. package/renderables/__tests__/renderable-test-utils.d.ts +0 -12
  552. package/renderables/composition/VRenderable.d.ts +0 -16
  553. package/renderables/composition/constructs.d.ts +0 -35
  554. package/renderables/composition/vnode.d.ts +0 -46
  555. package/renderables/index.d.ts +0 -23
  556. package/renderables/markdown-parser.d.ts +0 -10
  557. package/renderer.d.ts +0 -419
  558. package/runtime-plugin-support.d.ts +0 -3
  559. package/runtime-plugin-support.js +0 -29
  560. package/runtime-plugin-support.js.map +0 -10
  561. package/runtime-plugin.d.ts +0 -16
  562. package/runtime-plugin.js +0 -16
  563. package/runtime-plugin.js.map +0 -9
  564. package/syntax-style.d.ts +0 -54
  565. package/testing/manual-clock.d.ts +0 -17
  566. package/testing/mock-keys.d.ts +0 -81
  567. package/testing/mock-mouse.d.ts +0 -38
  568. package/testing/mock-tree-sitter-client.d.ts +0 -23
  569. package/testing/spy.d.ts +0 -7
  570. package/testing/test-recorder.d.ts +0 -61
  571. package/testing/test-renderer.d.ts +0 -23
  572. package/testing.d.ts +0 -6
  573. package/testing.js +0 -697
  574. package/testing.js.map +0 -15
  575. package/text-buffer-view.d.ts +0 -42
  576. package/text-buffer.d.ts +0 -67
  577. package/types.d.ts +0 -139
  578. package/utils.d.ts +0 -14
  579. package/zig-structs.d.ts +0 -155
  580. package/zig.d.ts +0 -353
  581. /package/{assets → src/lib/tree-sitter/assets}/javascript/highlights.scm +0 -0
  582. /package/{assets → src/lib/tree-sitter/assets}/javascript/tree-sitter-javascript.wasm +0 -0
  583. /package/{assets → src/lib/tree-sitter/assets}/markdown/highlights.scm +0 -0
  584. /package/{assets → src/lib/tree-sitter/assets}/markdown/injections.scm +0 -0
  585. /package/{assets → src/lib/tree-sitter/assets}/markdown/tree-sitter-markdown.wasm +0 -0
  586. /package/{assets → src/lib/tree-sitter/assets}/markdown_inline/highlights.scm +0 -0
  587. /package/{assets → src/lib/tree-sitter/assets}/markdown_inline/tree-sitter-markdown_inline.wasm +0 -0
  588. /package/{assets → src/lib/tree-sitter/assets}/typescript/highlights.scm +0 -0
  589. /package/{assets → src/lib/tree-sitter/assets}/typescript/tree-sitter-typescript.wasm +0 -0
  590. /package/{assets → src/lib/tree-sitter/assets}/zig/highlights.scm +0 -0
  591. /package/{assets → src/lib/tree-sitter/assets}/zig/tree-sitter-zig.wasm +0 -0
@@ -0,0 +1,1590 @@
1
+ import { describe, expect, it, beforeEach, afterEach } from "bun:test"
2
+ import { createTestRenderer, type TestRenderer, type MockMouse, type MockInput } from "../../testing/test-renderer.js"
3
+ import { createTextareaRenderable } from "./renderable-test-utils.js"
4
+ import { RGBA } from "../../lib/RGBA.js"
5
+ import { OptimizedBuffer } from "../../buffer.js"
6
+ import { TextRenderable } from "../Text.js"
7
+
8
+ let currentRenderer: TestRenderer
9
+ let renderOnce: () => Promise<void>
10
+ let currentMouse: MockMouse
11
+ let currentMockInput: MockInput
12
+
13
+ describe("Textarea - Selection Tests", () => {
14
+ beforeEach(async () => {
15
+ ;({
16
+ renderer: currentRenderer,
17
+ renderOnce,
18
+ mockMouse: currentMouse,
19
+ mockInput: currentMockInput,
20
+ } = await createTestRenderer({
21
+ width: 80,
22
+ height: 24,
23
+ }))
24
+ })
25
+
26
+ afterEach(() => {
27
+ currentRenderer.destroy()
28
+ })
29
+
30
+ describe("Selection Support", () => {
31
+ it("should support selection via mouse drag", async () => {
32
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
33
+ initialValue: "Hello World",
34
+ width: 40,
35
+ height: 10,
36
+ selectable: true,
37
+ })
38
+
39
+ expect(editor.hasSelection()).toBe(false)
40
+
41
+ await currentMouse.drag(editor.x, editor.y, editor.x + 5, editor.y)
42
+ await renderOnce()
43
+
44
+ expect(editor.hasSelection()).toBe(true)
45
+
46
+ const sel = editor.getSelection()
47
+ expect(sel).not.toBe(null)
48
+ expect(sel!.start).toBe(0)
49
+ expect(sel!.end).toBe(5)
50
+
51
+ expect(editor.getSelectedText()).toBe("Hello")
52
+ })
53
+
54
+ it("should return selected text from multi-line content", async () => {
55
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
56
+ initialValue: "AAAA\nBBBB\nCCCC",
57
+ width: 40,
58
+ height: 10,
59
+ selectable: true,
60
+ })
61
+
62
+ await currentMouse.drag(editor.x + 2, editor.y, editor.x + 2, editor.y + 2)
63
+ await renderOnce()
64
+
65
+ const selectedText = editor.getSelectedText()
66
+ expect(selectedText).toBe("AA\nBBBB\nCC")
67
+ })
68
+
69
+ it("should handle selection with viewport scrolling", async () => {
70
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
71
+ initialValue: Array.from({ length: 20 }, (_, i) => `Line ${i}`).join("\n"),
72
+ width: 40,
73
+ height: 5,
74
+ selectable: true,
75
+ })
76
+
77
+ editor.gotoLine(10)
78
+ await renderOnce()
79
+
80
+ const viewport = editor.editorView.getViewport()
81
+ expect(viewport.offsetY).toBeGreaterThan(0)
82
+
83
+ await currentMouse.drag(editor.x, editor.y, editor.x + 4, editor.y + 2)
84
+ await renderOnce()
85
+
86
+ expect(editor.hasSelection()).toBe(true)
87
+ const selectedText = editor.getSelectedText()
88
+ expect(selectedText.length).toBeGreaterThan(0)
89
+ expect(selectedText).not.toContain("Line 0")
90
+ expect(selectedText).not.toContain("Line 1")
91
+ expect(selectedText).toContain("Line")
92
+ })
93
+
94
+ it("should disable selection when selectable is false", async () => {
95
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
96
+ initialValue: "Hello World",
97
+ width: 40,
98
+ height: 10,
99
+ selectable: false,
100
+ })
101
+
102
+ const shouldHandle = editor.shouldStartSelection(editor.x, editor.y)
103
+ expect(shouldHandle).toBe(false)
104
+
105
+ await currentMouse.drag(editor.x, editor.y, editor.x + 5, editor.y)
106
+ await renderOnce()
107
+
108
+ expect(editor.hasSelection()).toBe(false)
109
+ expect(editor.getSelectedText()).toBe("")
110
+ })
111
+
112
+ it("should update selection when selectionBg/selectionFg changes", async () => {
113
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
114
+ initialValue: "Hello World",
115
+ width: 40,
116
+ height: 10,
117
+ selectable: true,
118
+ selectionBg: RGBA.fromValues(0, 0, 1, 1),
119
+ })
120
+
121
+ await currentMouse.drag(editor.x, editor.y, editor.x + 5, editor.y)
122
+ await renderOnce()
123
+
124
+ expect(editor.hasSelection()).toBe(true)
125
+
126
+ editor.selectionBg = RGBA.fromValues(1, 0, 0, 1)
127
+ editor.selectionFg = RGBA.fromValues(1, 1, 1, 1)
128
+
129
+ expect(editor.hasSelection()).toBe(true)
130
+ })
131
+
132
+ it("should clear selection", async () => {
133
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
134
+ initialValue: "Hello World",
135
+ width: 40,
136
+ height: 10,
137
+ selectable: true,
138
+ })
139
+
140
+ await currentMouse.drag(editor.x, editor.y, editor.x + 5, editor.y)
141
+ await renderOnce()
142
+
143
+ expect(editor.hasSelection()).toBe(true)
144
+
145
+ currentRenderer.clearSelection()
146
+ await renderOnce()
147
+
148
+ expect(editor.hasSelection()).toBe(false)
149
+ expect(editor.getSelectedText()).toBe("")
150
+ })
151
+
152
+ it("should handle selection with wrapping enabled", async () => {
153
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
154
+ initialValue: "ABCDEFGHIJKLMNOP",
155
+ width: 10,
156
+ height: 10,
157
+ wrapMode: "word",
158
+ selectable: true,
159
+ })
160
+
161
+ const vlineCount = editor.editorView.getVirtualLineCount()
162
+ expect(vlineCount).toBe(2)
163
+
164
+ await currentMouse.drag(editor.x + 2, editor.y, editor.x + 3, editor.y + 1)
165
+ await renderOnce()
166
+
167
+ const sel = editor.getSelection()
168
+ expect(sel).not.toBe(null)
169
+ expect(sel!.start).toBe(2)
170
+ expect(sel!.end).toBe(13)
171
+ })
172
+
173
+ it("should handle reverse selection (drag from end to start)", async () => {
174
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
175
+ initialValue: "Hello World",
176
+ width: 40,
177
+ height: 10,
178
+ selectable: true,
179
+ })
180
+
181
+ await currentMouse.drag(editor.x + 11, editor.y, editor.x + 6, editor.y)
182
+ await renderOnce()
183
+
184
+ const sel = editor.getSelection()
185
+ expect(sel).not.toBe(null)
186
+ expect(sel!.start).toBe(6)
187
+ expect(sel!.end).toBe(11)
188
+
189
+ expect(editor.getSelectedText()).toBe("World")
190
+ })
191
+
192
+ it("should render selection properly when drawing to buffer", async () => {
193
+ const buffer = OptimizedBuffer.create(80, 24, "wcwidth")
194
+
195
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
196
+ initialValue: "Hello World",
197
+ width: 40,
198
+ height: 10,
199
+ selectable: true,
200
+ selectionBg: RGBA.fromValues(0, 0, 1, 1),
201
+ selectionFg: RGBA.fromValues(1, 1, 1, 1),
202
+ })
203
+
204
+ await currentMouse.drag(editor.x, editor.y, editor.x + 5, editor.y)
205
+ await renderOnce()
206
+
207
+ expect(editor.hasSelection()).toBe(true)
208
+ expect(editor.getSelectedText()).toBe("Hello")
209
+
210
+ buffer.clear(RGBA.fromValues(0, 0, 0, 1))
211
+ buffer.drawEditorView(editor.editorView, editor.x, editor.y)
212
+
213
+ const sel = editor.getSelection()
214
+ expect(sel).not.toBe(null)
215
+ expect(sel!.start).toBe(0)
216
+ expect(sel!.end).toBe(5)
217
+
218
+ buffer.destroy()
219
+ })
220
+
221
+ it("should handle viewport-aware selection correctly", async () => {
222
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
223
+ initialValue: Array.from({ length: 15 }, (_, i) => `Line ${i}`).join("\n"),
224
+ width: 40,
225
+ height: 5,
226
+ selectable: true,
227
+ scrollMargin: 0,
228
+ scrollSpeed: 0,
229
+ })
230
+
231
+ editor.gotoLine(10)
232
+ await renderOnce()
233
+
234
+ const viewport = editor.editorView.getViewport()
235
+ expect(viewport.offsetY).toBeGreaterThan(0)
236
+
237
+ const expectedLineNumber = viewport.offsetY
238
+
239
+ await currentMouse.drag(editor.x, editor.y, editor.x + 6, editor.y)
240
+ await renderOnce()
241
+
242
+ expect(editor.hasSelection()).toBe(true)
243
+ const selectedText = editor.getSelectedText()
244
+
245
+ expect(selectedText).not.toContain("Line 0")
246
+ expect(selectedText).not.toContain("Line 1")
247
+ expect(selectedText).toContain(`Line ${expectedLineNumber}`)
248
+ })
249
+
250
+ it("should handle multi-line selection with viewport scrolling", async () => {
251
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
252
+ initialValue: Array.from({ length: 20 }, (_, i) => `AAAA${i}`).join("\n"),
253
+ width: 40,
254
+ height: 5,
255
+ selectable: true,
256
+ })
257
+
258
+ editor.gotoLine(8)
259
+ await renderOnce()
260
+
261
+ const viewport = editor.editorView.getViewport()
262
+ expect(viewport.offsetY).toBeGreaterThan(0)
263
+
264
+ await currentMouse.drag(editor.x, editor.y, editor.x + 4, editor.y + 2)
265
+ await renderOnce()
266
+
267
+ expect(editor.hasSelection()).toBe(true)
268
+ const selectedText = editor.getSelectedText()
269
+
270
+ const line1 = `AAAA${viewport.offsetY}`
271
+ const line2 = `AAAA${viewport.offsetY + 1}`
272
+ const line3 = `AAAA${viewport.offsetY + 2}`
273
+
274
+ expect(selectedText).toContain(line1)
275
+ expect(selectedText).toContain(line2)
276
+ expect(selectedText).toContain(line3.substring(0, 4))
277
+ })
278
+
279
+ it("should handle horizontal scrolled selection without wrapping", async () => {
280
+ const longLine = "A".repeat(100)
281
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
282
+ initialValue: longLine,
283
+ width: 20,
284
+ height: 5,
285
+ wrapMode: "none",
286
+ selectable: true,
287
+ })
288
+
289
+ for (let i = 0; i < 50; i++) {
290
+ editor.moveCursorRight()
291
+ }
292
+ await renderOnce()
293
+
294
+ const viewport = editor.editorView.getViewport()
295
+ expect(viewport.offsetX).toBeGreaterThan(0)
296
+
297
+ await currentMouse.drag(editor.x, editor.y, editor.x + 10, editor.y)
298
+ await renderOnce()
299
+
300
+ expect(editor.hasSelection()).toBe(true)
301
+ const selectedText = editor.getSelectedText()
302
+
303
+ expect(selectedText).toBe("A".repeat(10))
304
+
305
+ const sel = editor.getSelection()
306
+ expect(sel).not.toBe(null)
307
+ expect(sel!.start).toBeGreaterThanOrEqual(viewport.offsetX)
308
+ })
309
+
310
+ it("should render selection highlighting at correct screen position with viewport scroll", async () => {
311
+ const buffer = OptimizedBuffer.create(80, 24, "wcwidth")
312
+
313
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
314
+ initialValue: Array.from({ length: 15 }, (_, i) => `Line${i}`).join("\n"),
315
+ width: 20,
316
+ height: 5,
317
+ selectable: true,
318
+ selectionBg: RGBA.fromValues(1, 0, 0, 1),
319
+ })
320
+
321
+ editor.gotoLine(8)
322
+ await renderOnce()
323
+
324
+ const viewport = editor.editorView.getViewport()
325
+ expect(viewport.offsetY).toBeGreaterThan(0)
326
+
327
+ // Use manual drag steps instead of the drag helper to avoid timing issues
328
+ await currentMouse.pressDown(editor.x, editor.y)
329
+ await currentMouse.emitMouseEvent("drag", editor.x + 5, editor.y)
330
+ await currentMouse.release(editor.x + 5, editor.y)
331
+ await renderOnce()
332
+
333
+ buffer.clear(RGBA.fromValues(0, 0, 0, 1))
334
+ buffer.drawEditorView(editor.editorView, editor.x, editor.y)
335
+
336
+ const selectedText = editor.getSelectedText()
337
+ expect(selectedText).toBe(`Line${viewport.offsetY}`.substring(0, 5))
338
+
339
+ const { bg } = buffer.buffers
340
+ const bufferWidth = buffer.width
341
+
342
+ for (let cellX = editor.x; cellX < editor.x + 5; cellX++) {
343
+ const bufferIdx = editor.y * bufferWidth + cellX
344
+ const bgR = bg[bufferIdx * 4 + 0]
345
+ const bgG = bg[bufferIdx * 4 + 1]
346
+ const bgB = bg[bufferIdx * 4 + 2]
347
+
348
+ expect(Math.abs(bgR - 1.0)).toBeLessThan(0.01)
349
+ expect(Math.abs(bgG - 0.0)).toBeLessThan(0.01)
350
+ expect(Math.abs(bgB - 0.0)).toBeLessThan(0.01)
351
+ }
352
+
353
+ buffer.destroy()
354
+ })
355
+
356
+ it("should render selection correctly with empty lines between content", async () => {
357
+ const buffer = OptimizedBuffer.create(80, 24, "wcwidth")
358
+
359
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
360
+ initialValue: "AAAA\n\nBBBB\n\nCCCC",
361
+ width: 40,
362
+ height: 10,
363
+ selectable: true,
364
+ selectionBg: RGBA.fromValues(1, 0, 0, 1),
365
+ })
366
+
367
+ editor.focus()
368
+ editor.gotoLine(2)
369
+
370
+ for (let i = 0; i < 4; i++) {
371
+ currentMockInput.pressArrow("right", { shift: true })
372
+ }
373
+
374
+ expect(editor.getSelectedText()).toBe("BBBB")
375
+
376
+ buffer.clear(RGBA.fromValues(0, 0, 0, 1))
377
+ buffer.drawEditorView(editor.editorView, 0, 0)
378
+
379
+ const { bg } = buffer.buffers
380
+ const bufferWidth = buffer.width
381
+
382
+ for (let cellX = 0; cellX < 4; cellX++) {
383
+ const bufferIdx = 2 * bufferWidth + cellX
384
+ const bgR = bg[bufferIdx * 4 + 0]
385
+ const bgG = bg[bufferIdx * 4 + 1]
386
+ const bgB = bg[bufferIdx * 4 + 2]
387
+
388
+ expect(Math.abs(bgR - 1.0)).toBeLessThan(0.01)
389
+ expect(Math.abs(bgG - 0.0)).toBeLessThan(0.01)
390
+ expect(Math.abs(bgB - 0.0)).toBeLessThan(0.01)
391
+ }
392
+
393
+ buffer.destroy()
394
+ })
395
+
396
+ it("should handle shift+arrow selection with viewport scrolling", async () => {
397
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
398
+ initialValue: Array.from({ length: 20 }, (_, i) => `Line${i}`).join("\n"),
399
+ width: 40,
400
+ height: 5,
401
+ selectable: true,
402
+ })
403
+
404
+ editor.focus()
405
+
406
+ editor.gotoLine(15)
407
+ await renderOnce()
408
+
409
+ const viewport = editor.editorView.getViewport()
410
+ expect(viewport.offsetY).toBeGreaterThan(10)
411
+
412
+ for (let i = 0; i < 5; i++) {
413
+ currentMockInput.pressArrow("right", { shift: true })
414
+ }
415
+
416
+ expect(editor.hasSelection()).toBe(true)
417
+ const selectedText = editor.getSelectedText()
418
+
419
+ expect(selectedText).toBe("Line1")
420
+
421
+ const sel = editor.getSelection()
422
+ expect(sel).not.toBe(null)
423
+ expect(sel!.end - sel!.start).toBe(5)
424
+ })
425
+
426
+ it("should handle mouse drag selection with scrolled viewport using correct offset", async () => {
427
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
428
+ initialValue: Array.from({ length: 30 }, (_, i) => `AAAA${i}`).join("\n"),
429
+ width: 40,
430
+ height: 5,
431
+ selectable: true,
432
+ scrollSpeed: 0,
433
+ })
434
+
435
+ editor.gotoLine(20)
436
+ await renderOnce()
437
+
438
+ const viewport = editor.editorView.getViewport()
439
+ expect(viewport.offsetY).toBeGreaterThan(15)
440
+
441
+ await currentMouse.drag(editor.x, editor.y, editor.x + 4, editor.y)
442
+ await renderOnce()
443
+
444
+ expect(editor.hasSelection()).toBe(true)
445
+ const selectedText = editor.getSelectedText()
446
+
447
+ expect(selectedText).not.toContain("AAAA0")
448
+ expect(selectedText).not.toContain("AAAA1")
449
+
450
+ const firstVisibleLineIdx = viewport.offsetY
451
+ const expectedText = `AAAA${firstVisibleLineIdx}`.substring(0, 4)
452
+ expect(selectedText).toBe(expectedText)
453
+ })
454
+
455
+ it("should handle multi-line mouse drag with scrolled viewport", async () => {
456
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
457
+ initialValue: Array.from({ length: 30 }, (_, i) => `Line${i}`).join("\n"),
458
+ width: 40,
459
+ height: 5,
460
+ selectable: true,
461
+ })
462
+
463
+ editor.gotoLine(12)
464
+ await renderOnce()
465
+
466
+ const viewport = editor.editorView.getViewport()
467
+ expect(viewport.offsetY).toBeGreaterThan(7)
468
+
469
+ await currentMouse.drag(editor.x, editor.y, editor.x + 5, editor.y + 2)
470
+ await renderOnce()
471
+
472
+ expect(editor.hasSelection()).toBe(true)
473
+ const selectedText = editor.getSelectedText()
474
+
475
+ expect(selectedText.startsWith("Line0")).toBe(false)
476
+ expect(selectedText.startsWith("Line1")).toBe(false)
477
+ expect(selectedText.startsWith("Line2")).toBe(false)
478
+
479
+ const line1 = `Line${viewport.offsetY}`
480
+ const line2 = `Line${viewport.offsetY + 1}`
481
+ const line3 = `Line${viewport.offsetY + 2}`
482
+
483
+ expect(selectedText).toContain(line1)
484
+ expect(selectedText).toContain(line2)
485
+ expect(selectedText).toContain(line3.substring(0, 5))
486
+ })
487
+ })
488
+
489
+ describe("Shift+Arrow Key Selection", () => {
490
+ it("should start selection with shift+right", async () => {
491
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
492
+ initialValue: "Hello World",
493
+ width: 40,
494
+ height: 10,
495
+ selectable: true,
496
+ })
497
+
498
+ editor.focus()
499
+ expect(editor.hasSelection()).toBe(false)
500
+
501
+ currentMockInput.pressArrow("right", { shift: true })
502
+
503
+ expect(editor.hasSelection()).toBe(true)
504
+ expect(editor.getSelectedText()).toBe("H")
505
+ })
506
+
507
+ it("should extend selection with shift+right", async () => {
508
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
509
+ initialValue: "Hello World",
510
+ width: 40,
511
+ height: 10,
512
+ selectable: true,
513
+ })
514
+
515
+ editor.focus()
516
+
517
+ for (let i = 0; i < 5; i++) {
518
+ currentMockInput.pressArrow("right", { shift: true })
519
+ }
520
+
521
+ expect(editor.hasSelection()).toBe(true)
522
+ expect(editor.getSelectedText()).toBe("Hello")
523
+ })
524
+
525
+ it("should extend a mouse selection with shift+right", async () => {
526
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
527
+ initialValue: "Hello World",
528
+ width: 40,
529
+ height: 10,
530
+ selectable: true,
531
+ })
532
+
533
+ editor.focus()
534
+
535
+ await currentMouse.drag(editor.x, editor.y, editor.x + 5, editor.y)
536
+ await renderOnce()
537
+
538
+ expect(editor.hasSelection()).toBe(true)
539
+ expect(editor.getSelectedText()).toBe("Hello")
540
+
541
+ currentMockInput.pressArrow("right", { shift: true })
542
+ await renderOnce()
543
+
544
+ expect(editor.getSelectedText()).toBe("Hello ")
545
+ })
546
+
547
+ it("should handle shift+left selection", async () => {
548
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
549
+ initialValue: "Hello World",
550
+ width: 40,
551
+ height: 10,
552
+ selectable: true,
553
+ })
554
+
555
+ editor.focus()
556
+ const cursor = editor.logicalCursor
557
+ editor.editBuffer.setCursorToLineCol(cursor.row, 9999)
558
+
559
+ for (let i = 0; i < 5; i++) {
560
+ currentMockInput.pressArrow("left", { shift: true })
561
+ }
562
+
563
+ expect(editor.hasSelection()).toBe(true)
564
+ expect(editor.getSelectedText()).toBe("World")
565
+ })
566
+
567
+ it("should select with shift+down", async () => {
568
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
569
+ initialValue: "Line 1\nLine 2\nLine 3",
570
+ width: 40,
571
+ height: 10,
572
+ selectable: true,
573
+ })
574
+
575
+ editor.focus()
576
+
577
+ currentMockInput.pressArrow("down", { shift: true })
578
+
579
+ expect(editor.hasSelection()).toBe(true)
580
+ const selectedText = editor.getSelectedText()
581
+ expect(selectedText).toBe("Line 1")
582
+ })
583
+
584
+ it("should select with shift+up", async () => {
585
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
586
+ initialValue: "Line 1\nLine 2\nLine 3",
587
+ width: 40,
588
+ height: 10,
589
+ selectable: true,
590
+ })
591
+
592
+ editor.focus()
593
+ editor.gotoLine(2)
594
+
595
+ currentMockInput.pressArrow("up", { shift: true })
596
+
597
+ expect(editor.hasSelection()).toBe(true)
598
+ const selectedText = editor.getSelectedText()
599
+ expect(selectedText.includes("Line 2")).toBe(true)
600
+ })
601
+
602
+ it("should select to line start with shift+home", async () => {
603
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
604
+ initialValue: "Hello World",
605
+ width: 40,
606
+ height: 10,
607
+ selectable: true,
608
+ })
609
+
610
+ editor.focus()
611
+ for (let i = 0; i < 6; i++) {
612
+ editor.moveCursorRight()
613
+ }
614
+
615
+ currentMockInput.pressKey("HOME", { shift: true })
616
+
617
+ expect(editor.hasSelection()).toBe(true)
618
+ expect(editor.getSelectedText()).toBe("Hello W")
619
+ })
620
+
621
+ it("should select to line end with shift+end", async () => {
622
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
623
+ initialValue: "Hello World",
624
+ width: 40,
625
+ height: 10,
626
+ selectable: true,
627
+ })
628
+
629
+ editor.focus()
630
+
631
+ currentMockInput.pressKey("END", { shift: true })
632
+
633
+ expect(editor.hasSelection()).toBe(true)
634
+ expect(editor.getSelectedText()).toBe("Hello World")
635
+ })
636
+
637
+ it("should clear selection when moving without shift", async () => {
638
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
639
+ initialValue: "Hello World",
640
+ width: 40,
641
+ height: 10,
642
+ selectable: true,
643
+ })
644
+
645
+ editor.focus()
646
+
647
+ for (let i = 0; i < 5; i++) {
648
+ currentMockInput.pressArrow("right", { shift: true })
649
+ }
650
+
651
+ expect(editor.hasSelection()).toBe(true)
652
+
653
+ currentMockInput.pressArrow("right")
654
+
655
+ expect(editor.hasSelection()).toBe(false)
656
+ })
657
+
658
+ it("should delete selected text with backspace", async () => {
659
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
660
+ initialValue: "Hello World",
661
+ width: 40,
662
+ height: 10,
663
+ selectable: true,
664
+ })
665
+
666
+ editor.focus()
667
+
668
+ for (let i = 0; i < 5; i++) {
669
+ currentMockInput.pressArrow("right", { shift: true })
670
+ }
671
+
672
+ expect(editor.getSelectedText()).toBe("Hello")
673
+ expect(editor.plainText).toBe("Hello World")
674
+
675
+ currentMockInput.pressBackspace()
676
+
677
+ expect(editor.hasSelection()).toBe(false)
678
+ expect(editor.plainText).toBe(" World")
679
+ expect(editor.logicalCursor.col).toBe(0)
680
+ })
681
+
682
+ it("should delete selected text with delete key", async () => {
683
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
684
+ initialValue: "Hello World!",
685
+ width: 40,
686
+ height: 10,
687
+ selectable: true,
688
+ })
689
+
690
+ editor.focus()
691
+
692
+ const cursor = editor.logicalCursor
693
+ editor.editBuffer.setCursorToLineCol(cursor.row, 9999)
694
+ for (let i = 0; i < 6; i++) {
695
+ currentMockInput.pressArrow("left", { shift: true })
696
+ }
697
+
698
+ expect(editor.getSelectedText()).toBe("World!")
699
+ expect(editor.plainText).toBe("Hello World!")
700
+
701
+ currentMockInput.pressKey("DELETE")
702
+
703
+ expect(editor.hasSelection()).toBe(false)
704
+ expect(editor.plainText).toBe("Hello ")
705
+ expect(editor.logicalCursor.col).toBe(6)
706
+ })
707
+
708
+ it("should delete multi-line selection with backspace", async () => {
709
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
710
+ initialValue: "Line 1\nLine 2\nLine 3",
711
+ width: 40,
712
+ height: 10,
713
+ selectable: true,
714
+ })
715
+
716
+ editor.focus()
717
+
718
+ for (let i = 0; i < 10; i++) {
719
+ currentMockInput.pressArrow("right", { shift: true })
720
+ }
721
+
722
+ const selectedText = editor.getSelectedText()
723
+ expect(editor.plainText).toBe("Line 1\nLine 2\nLine 3")
724
+
725
+ currentMockInput.pressBackspace()
726
+
727
+ expect(editor.hasSelection()).toBe(false)
728
+ expect(editor.plainText).toBe("e 2\nLine 3")
729
+ expect(editor.logicalCursor.col).toBe(0)
730
+ expect(editor.logicalCursor.row).toBe(0)
731
+ })
732
+
733
+ it("should delete entire line when selected with delete", async () => {
734
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
735
+ initialValue: "Line 1\nLine 2\nLine 3",
736
+ width: 40,
737
+ height: 10,
738
+ selectable: true,
739
+ })
740
+
741
+ editor.focus()
742
+ editor.gotoLine(1)
743
+
744
+ currentMockInput.pressArrow("down", { shift: true })
745
+
746
+ const selectedText = editor.getSelectedText()
747
+ expect(selectedText).toBe("Line 2")
748
+
749
+ currentMockInput.pressKey("DELETE")
750
+
751
+ expect(editor.hasSelection()).toBe(false)
752
+ expect(editor.plainText).toBe("Line 1\nLine 3")
753
+ expect(editor.logicalCursor.row).toBe(1)
754
+ })
755
+
756
+ it("should replace selected text when typing", async () => {
757
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
758
+ initialValue: "Hello World",
759
+ width: 40,
760
+ height: 10,
761
+ selectable: true,
762
+ })
763
+
764
+ editor.focus()
765
+
766
+ for (let i = 0; i < 5; i++) {
767
+ currentMockInput.pressArrow("right", { shift: true })
768
+ }
769
+
770
+ expect(editor.getSelectedText()).toBe("Hello")
771
+
772
+ currentMockInput.pressKey("H")
773
+ currentMockInput.pressKey("i")
774
+
775
+ expect(editor.hasSelection()).toBe(false)
776
+ expect(editor.plainText).toBe("Hi World")
777
+ })
778
+
779
+ it("should delete selected text via native deleteSelectedText API", async () => {
780
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
781
+ initialValue: "Hello World",
782
+ width: 40,
783
+ height: 10,
784
+ selectable: true,
785
+ })
786
+
787
+ editor.focus()
788
+
789
+ await currentMouse.drag(editor.x, editor.y, editor.x + 5, editor.y)
790
+ await renderOnce()
791
+
792
+ expect(editor.hasSelection()).toBe(true)
793
+ expect(editor.getSelectedText()).toBe("Hello")
794
+
795
+ editor.editorView.deleteSelectedText()
796
+ currentRenderer.clearSelection()
797
+ await renderOnce()
798
+
799
+ expect(editor.plainText).toBe(" World")
800
+ expect(editor.logicalCursor.row).toBe(0)
801
+ expect(editor.logicalCursor.col).toBe(0)
802
+ expect(editor.editorView.hasSelection()).toBe(false)
803
+ })
804
+ it("should maintain correct selection start when scrolling down with shift+down", async () => {
805
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
806
+ initialValue: Array.from({ length: 20 }, (_, i) => `Line ${i}`).join("\n"),
807
+ width: 20,
808
+ height: 5,
809
+ selectable: true,
810
+ })
811
+
812
+ editor.focus()
813
+
814
+ for (let i = 0; i < 8; i++) {
815
+ currentMockInput.pressArrow("down", { shift: true })
816
+ await renderOnce()
817
+ }
818
+
819
+ const viewport = editor.editorView.getViewport()
820
+ expect(viewport.offsetY).toBeGreaterThan(0)
821
+
822
+ const sel = editor.getSelection()
823
+ expect(sel).not.toBe(null)
824
+ expect(sel!.start).toBe(0)
825
+ })
826
+
827
+ it("should not start selection in textarea when clicking in text renderable below after scrolling", async () => {
828
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
829
+ initialValue: Array.from({ length: 20 }, (_, i) => `Textarea Line ${i}`).join("\n"),
830
+ width: 40,
831
+ height: 5,
832
+ selectable: true,
833
+ top: 0,
834
+ })
835
+
836
+ const textBelow = new TextRenderable(currentRenderer, {
837
+ id: "text-below",
838
+ content: "This is text below the textarea",
839
+ selectable: true,
840
+ top: 5,
841
+ left: 0,
842
+ width: 40,
843
+ height: 1,
844
+ })
845
+ currentRenderer.root.add(textBelow)
846
+
847
+ editor.focus()
848
+
849
+ editor.gotoBufferEnd()
850
+ await renderOnce()
851
+
852
+ const viewport = editor.editorView.getViewport()
853
+ expect(viewport.offsetY).toBeGreaterThan(10)
854
+
855
+ await currentMouse.drag(textBelow.x, textBelow.y, textBelow.x + 10, textBelow.y)
856
+ await renderOnce()
857
+
858
+ expect(editor.hasSelection()).toBe(false)
859
+ expect(editor.getSelectedText()).toBe("")
860
+
861
+ expect(textBelow.hasSelection()).toBe(true)
862
+ expect(textBelow.getSelectedText()).toBe("This is te")
863
+
864
+ textBelow.destroy()
865
+ })
866
+
867
+ it("should maintain selection in both renderables when dragging from text-below up into textarea", async () => {
868
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
869
+ initialValue: Array.from({ length: 20 }, (_, i) => `Textarea Line ${i}`).join("\n"),
870
+ width: 40,
871
+ height: 5,
872
+ selectable: true,
873
+ top: 0,
874
+ })
875
+
876
+ const textBelow = new TextRenderable(currentRenderer, {
877
+ id: "text-below",
878
+ content: "This is text below the textarea",
879
+ selectable: true,
880
+ top: 5,
881
+ left: 0,
882
+ width: 40,
883
+ height: 1,
884
+ })
885
+ currentRenderer.root.add(textBelow)
886
+
887
+ editor.focus()
888
+
889
+ editor.gotoBufferEnd()
890
+ await renderOnce()
891
+
892
+ const viewport = editor.editorView.getViewport()
893
+ expect(viewport.offsetY).toBeGreaterThan(10)
894
+
895
+ const startX = textBelow.x + 5
896
+ const startY = textBelow.y
897
+ const endX = editor.x + 15
898
+ const endY = editor.y + 3
899
+
900
+ await currentMouse.drag(startX, startY, endX, endY)
901
+ await renderOnce()
902
+
903
+ expect(textBelow.hasSelection()).toBe(true)
904
+ const textBelowSelection = textBelow.getSelectedText()
905
+ expect(textBelowSelection.length).toBeGreaterThan(0)
906
+
907
+ expect(editor.hasSelection()).toBe(true)
908
+ const textareaSelection = editor.getSelectedText()
909
+ expect(textareaSelection.length).toBeGreaterThan(0)
910
+
911
+ textBelow.destroy()
912
+ })
913
+
914
+ it("should handle cross-renderable selection from bottom-left text to top-right text", async () => {
915
+ const { BoxRenderable } = await import("../Box.js")
916
+
917
+ const bottomText = new TextRenderable(currentRenderer, {
918
+ id: "bottom-instructions",
919
+ content: "Click and drag to select text across any elements",
920
+ left: 5,
921
+ top: 20,
922
+ width: 50,
923
+ height: 1,
924
+ selectable: true,
925
+ })
926
+ currentRenderer.root.add(bottomText)
927
+
928
+ const rightBox = new BoxRenderable(currentRenderer, {
929
+ id: "right-box",
930
+ left: 50,
931
+ top: 5,
932
+ width: 30,
933
+ height: 10,
934
+ padding: 1,
935
+ flexDirection: "column",
936
+ })
937
+ currentRenderer.root.add(rightBox)
938
+
939
+ const codeText1 = new TextRenderable(currentRenderer, {
940
+ id: "code-line-1",
941
+ content: "function handleSelection() {",
942
+ selectable: true,
943
+ })
944
+ rightBox.add(codeText1)
945
+
946
+ const codeText2 = new TextRenderable(currentRenderer, {
947
+ id: "code-line-2",
948
+ content: " const selected = getText()",
949
+ selectable: true,
950
+ })
951
+ rightBox.add(codeText2)
952
+
953
+ const codeText3 = new TextRenderable(currentRenderer, {
954
+ id: "code-line-3",
955
+ content: " console.log(selected)",
956
+ selectable: true,
957
+ })
958
+ rightBox.add(codeText3)
959
+
960
+ const codeText4 = new TextRenderable(currentRenderer, {
961
+ id: "code-line-4",
962
+ content: "}",
963
+ selectable: true,
964
+ })
965
+ rightBox.add(codeText4)
966
+
967
+ await renderOnce()
968
+
969
+ const startX = bottomText.x + 10
970
+ const startY = bottomText.y
971
+ const endX = codeText2.x + 15
972
+ const endY = codeText2.y
973
+
974
+ await currentMouse.drag(startX, startY, endX, endY)
975
+ await renderOnce()
976
+
977
+ expect(bottomText.hasSelection()).toBe(true)
978
+ const bottomSelected = bottomText.getSelectedText()
979
+ expect(bottomSelected).toBe("Click and d")
980
+
981
+ expect(codeText1.hasSelection()).toBe(false)
982
+
983
+ expect(codeText2.hasSelection()).toBe(true)
984
+ const codeText2Selected = codeText2.getSelectedText()
985
+ const codeText2Content = " const selected = getText()"
986
+ expect(codeText2Selected).toBe(codeText2Content.substring(0, 15))
987
+
988
+ bottomText.destroy()
989
+ rightBox.destroy()
990
+ })
991
+ })
992
+
993
+ describe("Selection After Resize", () => {
994
+ it("should maintain selection correctly after resize - same text selected and rendered properly", async () => {
995
+ const buffer = OptimizedBuffer.create(80, 24, "wcwidth")
996
+
997
+ const { textarea: editor, root } = await createTextareaRenderable(currentRenderer, renderOnce, {
998
+ initialValue: Array.from({ length: 30 }, (_, i) => `Line ${i.toString().padStart(2, "0")}`).join("\n"),
999
+ width: 40,
1000
+ height: 10,
1001
+ selectable: true,
1002
+ selectionBg: RGBA.fromValues(0, 1, 0, 1),
1003
+ selectionFg: RGBA.fromValues(0, 0, 0, 1),
1004
+ })
1005
+
1006
+ editor.gotoLine(5)
1007
+ await renderOnce()
1008
+
1009
+ await currentMouse.drag(editor.x + 5, editor.y + 2, editor.x + 10, editor.y + 4)
1010
+ await renderOnce()
1011
+
1012
+ const selectedTextBefore = editor.getSelectedText()
1013
+ const selectionBefore = editor.getSelection()
1014
+
1015
+ expect(editor.hasSelection()).toBe(true)
1016
+ expect(selectedTextBefore).toBeTruthy()
1017
+
1018
+ buffer.clear(RGBA.fromValues(0, 0, 0, 1))
1019
+ buffer.drawEditorView(editor.editorView, editor.x, editor.y)
1020
+
1021
+ const { bg: bgBefore } = buffer.buffers
1022
+ const bufferWidth = buffer.width
1023
+
1024
+ const selectedCellsBefore: Array<{ x: number; y: number }> = []
1025
+ for (let y = 0; y < editor.height; y++) {
1026
+ for (let x = 0; x < editor.width; x++) {
1027
+ const bufferIdx = (editor.y + y) * bufferWidth + (editor.x + x)
1028
+ const bgG = bgBefore[bufferIdx * 4 + 1]
1029
+ if (Math.abs(bgG - 1.0) < 0.01) {
1030
+ selectedCellsBefore.push({ x, y })
1031
+ }
1032
+ }
1033
+ }
1034
+
1035
+ expect(selectedCellsBefore.length).toBeGreaterThan(0)
1036
+
1037
+ editor.width = 50
1038
+ editor.height = 15
1039
+ root.yogaNode.calculateLayout(80, 24)
1040
+ await renderOnce()
1041
+
1042
+ const selectedTextAfter = editor.getSelectedText()
1043
+ const selectionAfter = editor.getSelection()
1044
+
1045
+ expect(editor.hasSelection()).toBe(true)
1046
+ expect(selectedTextAfter).toBe(selectedTextBefore)
1047
+ expect(selectionAfter?.start).toBe(selectionBefore?.start)
1048
+ expect(selectionAfter?.end).toBe(selectionBefore?.end)
1049
+
1050
+ buffer.clear(RGBA.fromValues(0, 0, 0, 1))
1051
+ buffer.drawEditorView(editor.editorView, editor.x, editor.y)
1052
+
1053
+ const { bg: bgAfter } = buffer.buffers
1054
+
1055
+ const selectedCellsAfter: Array<{ x: number; y: number }> = []
1056
+ for (let y = 0; y < editor.height; y++) {
1057
+ for (let x = 0; x < editor.width; x++) {
1058
+ const bufferIdx = (editor.y + y) * bufferWidth + (editor.x + x)
1059
+ const bgG = bgAfter[bufferIdx * 4 + 1]
1060
+ if (Math.abs(bgG - 1.0) < 0.01) {
1061
+ selectedCellsAfter.push({ x, y })
1062
+ }
1063
+ }
1064
+ }
1065
+
1066
+ expect(selectedCellsAfter.length).toBeGreaterThan(0)
1067
+ expect(selectedCellsAfter.length).toBe(selectedCellsBefore.length)
1068
+
1069
+ buffer.destroy()
1070
+ editor.destroy()
1071
+ })
1072
+
1073
+ it("should maintain exact same text selected after wrap width changes", async () => {
1074
+ const buffer = OptimizedBuffer.create(80, 24, "wcwidth")
1075
+
1076
+ const { textarea: editor, root } = await createTextareaRenderable(currentRenderer, renderOnce, {
1077
+ initialValue: "AAAAA BBBBB CCCCC DDDDD EEEEE FFFFF GGGGG HHHHH",
1078
+ width: 50,
1079
+ height: 10,
1080
+ wrapMode: "word",
1081
+ selectable: true,
1082
+ selectionBg: RGBA.fromValues(1, 0, 1, 1),
1083
+ selectionFg: RGBA.fromValues(1, 1, 1, 1),
1084
+ })
1085
+
1086
+ await renderOnce()
1087
+
1088
+ await currentMouse.drag(editor.x + 6, editor.y, editor.x + 17, editor.y)
1089
+ await renderOnce()
1090
+
1091
+ const selectedTextBefore = editor.getSelectedText()
1092
+ const selectionBefore = editor.getSelection()
1093
+
1094
+ expect(editor.hasSelection()).toBe(true)
1095
+ expect(selectedTextBefore).toBe("BBBBB CCCCC")
1096
+
1097
+ editor.width = 15
1098
+ editor.height = 15
1099
+ root.yogaNode.calculateLayout(80, 24)
1100
+ await renderOnce()
1101
+
1102
+ const selectedTextAfterNarrow = editor.getSelectedText()
1103
+ const selectionAfterNarrow = editor.getSelection()
1104
+
1105
+ expect(editor.hasSelection()).toBe(true)
1106
+ expect(selectedTextAfterNarrow).toBe("BBBBB CCCCC")
1107
+ expect(selectionAfterNarrow?.start).toBe(selectionBefore?.start)
1108
+ expect(selectionAfterNarrow?.end).toBe(selectionBefore?.end)
1109
+
1110
+ buffer.clear(RGBA.fromValues(0, 0, 0, 1))
1111
+ buffer.drawEditorView(editor.editorView, editor.x, editor.y)
1112
+
1113
+ const { bg: bgNarrow } = buffer.buffers
1114
+ const bufferWidth = buffer.width
1115
+
1116
+ let selectedCellsNarrow = 0
1117
+ for (let y = 0; y < editor.height; y++) {
1118
+ for (let x = 0; x < editor.width; x++) {
1119
+ const bufferIdx = (editor.y + y) * bufferWidth + (editor.x + x)
1120
+ const bgR = bgNarrow[bufferIdx * 4 + 0]
1121
+ const bgB = bgNarrow[bufferIdx * 4 + 2]
1122
+ if (Math.abs(bgR - 1.0) < 0.01 && Math.abs(bgB - 1.0) < 0.01) {
1123
+ selectedCellsNarrow++
1124
+ }
1125
+ }
1126
+ }
1127
+
1128
+ expect(selectedCellsNarrow).toBe(11)
1129
+
1130
+ editor.width = 50
1131
+ editor.height = 10
1132
+ root.yogaNode.calculateLayout(80, 24)
1133
+ await renderOnce()
1134
+
1135
+ const selectedTextAfterWide = editor.getSelectedText()
1136
+ const selectionAfterWide = editor.getSelection()
1137
+
1138
+ expect(editor.hasSelection()).toBe(true)
1139
+ expect(selectedTextAfterWide).toBe("BBBBB CCCCC")
1140
+ expect(selectionAfterWide?.start).toBe(selectionBefore?.start)
1141
+ expect(selectionAfterWide?.end).toBe(selectionBefore?.end)
1142
+
1143
+ buffer.clear(RGBA.fromValues(0, 0, 0, 1))
1144
+ buffer.drawEditorView(editor.editorView, editor.x, editor.y)
1145
+
1146
+ const { bg: bgWide } = buffer.buffers
1147
+
1148
+ let selectedCellsWide = 0
1149
+ for (let y = 0; y < editor.height; y++) {
1150
+ for (let x = 0; x < editor.width; x++) {
1151
+ const bufferIdx = (editor.y + y) * bufferWidth + (editor.x + x)
1152
+ const bgR = bgWide[bufferIdx * 4 + 0]
1153
+ const bgB = bgWide[bufferIdx * 4 + 2]
1154
+ if (Math.abs(bgR - 1.0) < 0.01 && Math.abs(bgB - 1.0) < 0.01) {
1155
+ selectedCellsWide++
1156
+ }
1157
+ }
1158
+ }
1159
+
1160
+ expect(selectedCellsWide).toBe(11)
1161
+
1162
+ buffer.destroy()
1163
+ editor.destroy()
1164
+ })
1165
+
1166
+ it("should handle resize during active mouse selection drag", async () => {
1167
+ const buffer = OptimizedBuffer.create(80, 24, "wcwidth")
1168
+
1169
+ const { textarea: editor, root } = await createTextareaRenderable(currentRenderer, renderOnce, {
1170
+ initialValue: Array.from({ length: 50 }, (_, i) => `Line ${i}`).join("\n"),
1171
+ width: 40,
1172
+ height: 10,
1173
+ selectable: true,
1174
+ selectionBg: RGBA.fromValues(0, 1, 1, 1),
1175
+ })
1176
+
1177
+ await renderOnce()
1178
+
1179
+ await currentMouse.pressDown(editor.x + 2, editor.y + 1)
1180
+ await currentMouse.moveTo(editor.x + 8, editor.y + 3)
1181
+ await renderOnce()
1182
+
1183
+ expect(editor.hasSelection()).toBe(true)
1184
+ const selectedBeforeResize = editor.getSelectedText()
1185
+
1186
+ editor.width = 30
1187
+ editor.height = 8
1188
+ root.yogaNode.calculateLayout(80, 24)
1189
+ await renderOnce()
1190
+
1191
+ await currentMouse.moveTo(editor.x + 10, editor.y + 2)
1192
+ await renderOnce()
1193
+
1194
+ expect(editor.hasSelection()).toBe(true)
1195
+
1196
+ await currentMouse.release(editor.x + 10, editor.y + 2)
1197
+ await renderOnce()
1198
+
1199
+ buffer.clear(RGBA.fromValues(0, 0, 0, 1))
1200
+ buffer.drawEditorView(editor.editorView, editor.x, editor.y)
1201
+
1202
+ const { bg: bgAfterResize } = buffer.buffers
1203
+ const bufferWidth = buffer.width
1204
+
1205
+ let selectedCellsAfterResize = 0
1206
+ for (let y = 0; y < editor.height; y++) {
1207
+ for (let x = 0; x < editor.width; x++) {
1208
+ const bufferIdx = (editor.y + y) * bufferWidth + (editor.x + x)
1209
+ const bgG = bgAfterResize[bufferIdx * 4 + 1]
1210
+ const bgB = bgAfterResize[bufferIdx * 4 + 2]
1211
+ if (Math.abs(bgG - 1.0) < 0.01 && Math.abs(bgB - 1.0) < 0.01) {
1212
+ selectedCellsAfterResize++
1213
+ }
1214
+ }
1215
+ }
1216
+
1217
+ expect(selectedCellsAfterResize).toBeGreaterThan(0)
1218
+
1219
+ buffer.destroy()
1220
+ editor.destroy()
1221
+ })
1222
+
1223
+ it("should maintain selection correctly when renderable position changes during resize", async () => {
1224
+ const buffer = OptimizedBuffer.create(80, 24, "wcwidth")
1225
+
1226
+ const { textarea: editor, root } = await createTextareaRenderable(currentRenderer, renderOnce, {
1227
+ initialValue: Array.from({ length: 20 }, (_, i) => `Line ${i.toString().padStart(2, "0")}`).join("\n"),
1228
+ left: 10,
1229
+ top: 5,
1230
+ width: 40,
1231
+ height: 10,
1232
+ selectable: true,
1233
+ selectionBg: RGBA.fromValues(1, 1, 0, 1),
1234
+ selectionFg: RGBA.fromValues(0, 0, 0, 1),
1235
+ })
1236
+
1237
+ await renderOnce()
1238
+
1239
+ const initialX = editor.x
1240
+ const initialY = editor.y
1241
+
1242
+ await currentMouse.drag(editor.x + 5, editor.y + 2, editor.x + 10, editor.y + 4)
1243
+ await renderOnce()
1244
+
1245
+ const selectedTextBefore = editor.getSelectedText()
1246
+ const selectionBefore = editor.getSelection()
1247
+
1248
+ expect(editor.hasSelection()).toBe(true)
1249
+ expect(selectedTextBefore).toBeTruthy()
1250
+
1251
+ buffer.clear(RGBA.fromValues(0, 0, 0, 1))
1252
+ buffer.drawEditorView(editor.editorView, editor.x, editor.y)
1253
+
1254
+ const { bg: bgBefore } = buffer.buffers
1255
+ const bufferWidth = buffer.width
1256
+
1257
+ const selectedCellsBeforeCount = countSelectedCells(bgBefore, bufferWidth, editor, 1, 1, 0)
1258
+ expect(selectedCellsBeforeCount).toBeGreaterThan(0)
1259
+
1260
+ editor.left = 20
1261
+ editor.top = 10
1262
+ root.yogaNode.calculateLayout(80, 24)
1263
+ await renderOnce()
1264
+
1265
+ const newX = editor.x
1266
+ const newY = editor.y
1267
+
1268
+ expect(newX).not.toBe(initialX)
1269
+ expect(newY).not.toBe(initialY)
1270
+
1271
+ const selectedTextAfter = editor.getSelectedText()
1272
+ const selectionAfter = editor.getSelection()
1273
+
1274
+ expect(editor.hasSelection()).toBe(true)
1275
+ expect(selectedTextAfter).toBe(selectedTextBefore)
1276
+ expect(selectionAfter?.start).toBe(selectionBefore?.start)
1277
+ expect(selectionAfter?.end).toBe(selectionBefore?.end)
1278
+
1279
+ buffer.clear(RGBA.fromValues(0, 0, 0, 1))
1280
+ buffer.drawEditorView(editor.editorView, editor.x, editor.y)
1281
+
1282
+ const { bg: bgAfter } = buffer.buffers
1283
+ const selectedCellsAfterCount = countSelectedCells(bgAfter, bufferWidth, editor, 1, 1, 0)
1284
+
1285
+ expect(selectedCellsAfterCount).toBe(selectedCellsBeforeCount)
1286
+ expect(selectedCellsAfterCount).toBeGreaterThan(0)
1287
+
1288
+ buffer.destroy()
1289
+ editor.destroy()
1290
+ })
1291
+
1292
+ it("should keep cursor within textarea bounds after resize causes wrapping with scrolled selection", async () => {
1293
+ const { textarea: editor, root } = await createTextareaRenderable(currentRenderer, renderOnce, {
1294
+ initialValue: Array.from(
1295
+ { length: 50 },
1296
+ (_, i) =>
1297
+ `This is a long line ${i.toString().padStart(2, "0")} with enough text to cause wrapping when narrow`,
1298
+ ).join("\n"),
1299
+ width: 60,
1300
+ height: 10,
1301
+ top: 0,
1302
+ wrapMode: "word",
1303
+ selectable: true,
1304
+ showCursor: true,
1305
+ })
1306
+
1307
+ const textBelow = new TextRenderable(currentRenderer, {
1308
+ id: "text-below",
1309
+ content: "Element below textarea",
1310
+ top: 10,
1311
+ left: 0,
1312
+ })
1313
+ currentRenderer.root.add(textBelow)
1314
+
1315
+ await renderOnce()
1316
+
1317
+ editor.focus()
1318
+ editor.gotoLine(15)
1319
+ await renderOnce()
1320
+
1321
+ await currentMouse.drag(editor.x + 5, editor.y + 3, editor.x + 10, editor.y + 9)
1322
+ await renderOnce()
1323
+
1324
+ const viewportAfterSelection = editor.editorView.getViewport()
1325
+
1326
+ expect(editor.hasSelection()).toBe(true)
1327
+ expect(viewportAfterSelection.offsetY).toBeGreaterThan(0)
1328
+
1329
+ editor.width = 8
1330
+ root.yogaNode.calculateLayout(80, 24)
1331
+ await renderOnce()
1332
+
1333
+ const viewportAfterResize = editor.editorView.getViewport()
1334
+ const cursorAfterResize = editor.visualCursor
1335
+
1336
+ expect(cursorAfterResize.visualRow).toBeGreaterThanOrEqual(0)
1337
+ expect(cursorAfterResize.visualRow).toBeLessThan(editor.height)
1338
+ expect(cursorAfterResize.visualCol).toBeGreaterThanOrEqual(0)
1339
+ expect(cursorAfterResize.visualCol).toBeLessThan(editor.width)
1340
+
1341
+ textBelow.destroy()
1342
+ editor.destroy()
1343
+ })
1344
+ })
1345
+
1346
+ describe("Selection Preserved on Viewport Scroll", () => {
1347
+ it("should preserve selection when scrolling viewport", async () => {
1348
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
1349
+ initialValue: Array.from({ length: 50 }, (_, i) => `Line ${i}`).join("\n"),
1350
+ width: 40,
1351
+ height: 10,
1352
+ selectable: true,
1353
+ })
1354
+
1355
+ editor.focus()
1356
+ await renderOnce()
1357
+
1358
+ // Select all text using keyboard (Cmd+Shift+Down)
1359
+ currentMockInput.pressKey("ARROW_DOWN", { shift: true, super: true })
1360
+ await renderOnce()
1361
+
1362
+ const selectionBefore = editor.getSelection()
1363
+ const selectedTextBefore = editor.getSelectedText()
1364
+
1365
+ expect(selectionBefore).not.toBeNull()
1366
+ expect(selectedTextBefore).toContain("Line 0")
1367
+ expect(selectedTextBefore).toContain("Line 49")
1368
+
1369
+ // Start renderer to simulate real app with continuous render loop
1370
+ currentRenderer.start()
1371
+
1372
+ // Scroll up with mouse wheel
1373
+ await currentMouse.scroll(editor.x, editor.y + 1, "up")
1374
+ await Bun.sleep(100)
1375
+
1376
+ const selectionAfter = editor.getSelection()
1377
+ const selectedTextAfter = editor.getSelectedText()
1378
+
1379
+ currentRenderer.pause()
1380
+
1381
+ // Selection should not change when scrolling viewport
1382
+ expect(selectionAfter).not.toBeNull()
1383
+ expect(selectionAfter!.start).toBe(selectionBefore!.start)
1384
+ expect(selectionAfter!.end).toBe(selectionBefore!.end)
1385
+ expect(selectedTextAfter).toBe(selectedTextBefore)
1386
+
1387
+ editor.destroy()
1388
+ })
1389
+ })
1390
+
1391
+ describe("Keyboard Selection with Viewport Scrolling", () => {
1392
+ it("should select to buffer home after shift+end then shift+home when scrolled", async () => {
1393
+ const lines = Array.from({ length: 30 }, (_, i) => `Line ${i.toString().padStart(2, "0")}`)
1394
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
1395
+ initialValue: lines.join("\n"),
1396
+ width: 40,
1397
+ height: 6,
1398
+ selectable: true,
1399
+ })
1400
+
1401
+ editor.focus()
1402
+ await renderOnce()
1403
+
1404
+ for (let i = 0; i < 3; i++) {
1405
+ await currentMouse.scroll(editor.x + 2, editor.y + 2, "down")
1406
+ }
1407
+ await renderOnce()
1408
+
1409
+ const viewportAfterScroll = editor.editorView.getViewport()
1410
+ expect(viewportAfterScroll.offsetY).toBeGreaterThan(0)
1411
+ expect(editor.logicalCursor.row).toBeGreaterThan(0)
1412
+
1413
+ currentMockInput.pressKey("END", { shift: true })
1414
+ await renderOnce()
1415
+
1416
+ expect(editor.hasSelection()).toBe(true)
1417
+
1418
+ currentMockInput.pressKey("HOME", { shift: true })
1419
+ await renderOnce()
1420
+
1421
+ const selection = editor.getSelection()
1422
+ expect(selection).not.toBeNull()
1423
+ expect(selection!.start).toBe(0)
1424
+
1425
+ const selectedText = editor.getSelectedText()
1426
+ expect(selectedText.startsWith("Line 00")).toBe(true)
1427
+ expect(selectedText).not.toContain("Line 29")
1428
+ })
1429
+
1430
+ it("should allow shift+end after shift+home from a mid-buffer cursor", async () => {
1431
+ const lines = Array.from({ length: 30 }, (_, i) => `Line ${i.toString().padStart(2, "0")}`)
1432
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
1433
+ initialValue: lines.join("\n"),
1434
+ width: 40,
1435
+ height: 6,
1436
+ selectable: true,
1437
+ })
1438
+
1439
+ editor.focus()
1440
+ editor.gotoLine(10)
1441
+ await renderOnce()
1442
+
1443
+ currentMockInput.pressKey("END", { shift: true })
1444
+ await renderOnce()
1445
+
1446
+ expect(editor.hasSelection()).toBe(true)
1447
+
1448
+ currentMockInput.pressKey("HOME", { shift: true })
1449
+ await renderOnce()
1450
+
1451
+ currentMockInput.pressKey("END", { shift: true })
1452
+ await renderOnce()
1453
+
1454
+ expect(editor.hasSelection()).toBe(true)
1455
+ expect(editor.getSelectedText()).toContain("Line 29")
1456
+ })
1457
+
1458
+ it("should select to buffer home with shift+super+up in scrollable textarea", async () => {
1459
+ // Create textarea with content taller than visible area
1460
+ const lines = Array.from({ length: 50 }, (_, i) => `Line ${i.toString().padStart(2, "0")}`)
1461
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
1462
+ initialValue: lines.join("\n"),
1463
+ width: 40,
1464
+ height: 10,
1465
+ selectable: true,
1466
+ })
1467
+
1468
+ // Move cursor to middle of content (line 25)
1469
+ editor.focus()
1470
+ editor.gotoLine(25)
1471
+ await renderOnce()
1472
+
1473
+ // Verify viewport has scrolled
1474
+ const viewportBefore = editor.editorView.getViewport()
1475
+ expect(viewportBefore.offsetY).toBeGreaterThan(0)
1476
+
1477
+ // Select to buffer home (shift+super+up)
1478
+ currentMockInput.pressKey("ARROW_UP", { shift: true, super: true })
1479
+ await renderOnce()
1480
+
1481
+ // Should have selection
1482
+ expect(editor.hasSelection()).toBe(true)
1483
+
1484
+ // Selection should include content from line 0 to line 25
1485
+ const selectedText = editor.getSelectedText()
1486
+ expect(selectedText).toContain("Line 00")
1487
+ expect(selectedText).toContain("Line 24")
1488
+ expect(selectedText.split("\n").length).toBeGreaterThanOrEqual(25)
1489
+
1490
+ const viewportAfter = editor.editorView.getViewport()
1491
+ expect(viewportAfter.offsetY).toBe(0)
1492
+ })
1493
+
1494
+ it("should select to buffer end with shift+super+down in scrollable textarea", async () => {
1495
+ // Create textarea with content taller than visible area
1496
+ const lines = Array.from({ length: 50 }, (_, i) => `Line ${i.toString().padStart(2, "0")}`)
1497
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
1498
+ initialValue: lines.join("\n"),
1499
+ width: 40,
1500
+ height: 10,
1501
+ selectable: true,
1502
+ })
1503
+
1504
+ // Move cursor to line 20
1505
+ editor.focus()
1506
+ editor.gotoLine(20)
1507
+ await renderOnce()
1508
+
1509
+ const viewportBefore = editor.editorView.getViewport()
1510
+ expect(viewportBefore.offsetY).toBeGreaterThan(0)
1511
+
1512
+ // Select to buffer end (shift+super+down)
1513
+ currentMockInput.pressKey("ARROW_DOWN", { shift: true, super: true })
1514
+ await renderOnce()
1515
+
1516
+ // Should have selection
1517
+ expect(editor.hasSelection()).toBe(true)
1518
+
1519
+ // Selection should include content from line 20 to line 49
1520
+ const selectedText = editor.getSelectedText()
1521
+ expect(selectedText).toContain("Line 20")
1522
+ expect(selectedText).toContain("Line 49")
1523
+ expect(selectedText.split("\n").length).toBeGreaterThanOrEqual(29)
1524
+
1525
+ const viewportAfter = editor.editorView.getViewport()
1526
+ const totalLines = editor.editorView.getTotalVirtualLineCount()
1527
+ const maxOffsetY = Math.max(0, totalLines - viewportBefore.height)
1528
+ expect(viewportAfter.offsetY).toBe(maxOffsetY)
1529
+ })
1530
+
1531
+ it("should handle selection across viewport boundaries correctly", async () => {
1532
+ // Create textarea with content taller than visible area
1533
+ const lines = Array.from({ length: 30 }, (_, i) => `Line ${i.toString().padStart(2, "0")}`)
1534
+ const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
1535
+ initialValue: lines.join("\n"),
1536
+ width: 40,
1537
+ height: 5, // Small viewport
1538
+ selectable: true,
1539
+ })
1540
+
1541
+ // Move cursor to middle (line 15)
1542
+ editor.focus()
1543
+ editor.gotoLine(15)
1544
+ // Move to column 5
1545
+ for (let i = 0; i < 5; i++) {
1546
+ editor.moveCursorRight()
1547
+ }
1548
+ await renderOnce()
1549
+
1550
+ const cursorBefore = editor.editorView.getVisualCursor()
1551
+ expect(cursorBefore.logicalRow).toBe(15)
1552
+ expect(cursorBefore.logicalCol).toBe(5)
1553
+
1554
+ // Select to buffer home
1555
+ currentMockInput.pressKey("ARROW_UP", { shift: true, super: true })
1556
+ await renderOnce()
1557
+
1558
+ expect(editor.hasSelection()).toBe(true)
1559
+ const selectedText = editor.getSelectedText()
1560
+
1561
+ // Should select from (15, 5) to (0, 0)
1562
+ // First line should be complete, last line should be partial
1563
+ expect(selectedText.startsWith("Line 00")).toBe(true)
1564
+ expect(selectedText).toContain("Line 14")
1565
+ })
1566
+ })
1567
+ })
1568
+
1569
+ function countSelectedCells(
1570
+ bg: Float32Array,
1571
+ bufferWidth: number,
1572
+ editor: { x: number; y: number; height: number; width: number },
1573
+ r: number,
1574
+ g: number,
1575
+ b: number,
1576
+ ): number {
1577
+ let count = 0
1578
+ for (let y = 0; y < editor.height; y++) {
1579
+ for (let x = 0; x < editor.width; x++) {
1580
+ const bufferIdx = (editor.y + y) * bufferWidth + (editor.x + x)
1581
+ const bgR = bg[bufferIdx * 4 + 0]
1582
+ const bgG = bg[bufferIdx * 4 + 1]
1583
+ const bgB = bg[bufferIdx * 4 + 2]
1584
+ if (Math.abs(bgR - r) < 0.01 && Math.abs(bgG - g) < 0.01 && Math.abs(bgB - b) < 0.01) {
1585
+ count++
1586
+ }
1587
+ }
1588
+ }
1589
+ return count
1590
+ }