@react-x11/components 0.9.0 → 0.11.0

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 (579) hide show
  1. package/README.md +10 -4
  2. package/dist/charts/node.d.ts +11 -1
  3. package/dist/charts/node.d.ts.map +1 -1
  4. package/dist/charts/node.js +44 -14
  5. package/dist/charts/node.js.map +1 -1
  6. package/dist/charts/render.d.ts +12 -3
  7. package/dist/charts/render.d.ts.map +1 -1
  8. package/dist/charts/render.js +94 -25
  9. package/dist/charts/render.js.map +1 -1
  10. package/dist/code/index.d.ts.map +1 -1
  11. package/dist/code/index.js +27 -24
  12. package/dist/code/index.js.map +1 -1
  13. package/dist/code-editor/completion.d.ts.map +1 -1
  14. package/dist/code-editor/completion.js +3 -2
  15. package/dist/code-editor/completion.js.map +1 -1
  16. package/dist/code-editor/index.d.ts.map +1 -1
  17. package/dist/code-editor/index.js +19 -0
  18. package/dist/code-editor/index.js.map +1 -1
  19. package/dist/code-editor/node.d.ts +177 -7
  20. package/dist/code-editor/node.d.ts.map +1 -1
  21. package/dist/code-editor/node.js +1329 -164
  22. package/dist/code-editor/node.js.map +1 -1
  23. package/dist/code-language/index.d.ts +1 -1
  24. package/dist/code-language/index.d.ts.map +1 -1
  25. package/dist/code-language/index.js +1 -1
  26. package/dist/code-language/index.js.map +1 -1
  27. package/dist/code-language/languages/glsl.d.ts.map +1 -1
  28. package/dist/code-language/languages/glsl.js +10 -2
  29. package/dist/code-language/languages/glsl.js.map +1 -1
  30. package/dist/code-language/languages/javascript.d.ts.map +1 -1
  31. package/dist/code-language/languages/javascript.js +18 -3
  32. package/dist/code-language/languages/javascript.js.map +1 -1
  33. package/dist/code-language/languages/json.d.ts.map +1 -1
  34. package/dist/code-language/languages/json.js +10 -2
  35. package/dist/code-language/languages/json.js.map +1 -1
  36. package/dist/code-language/languages/shell.d.ts.map +1 -1
  37. package/dist/code-language/languages/shell.js +10 -2
  38. package/dist/code-language/languages/shell.js.map +1 -1
  39. package/dist/code-language/languages/sql.d.ts.map +1 -1
  40. package/dist/code-language/languages/sql.js +20 -2
  41. package/dist/code-language/languages/sql.js.map +1 -1
  42. package/dist/code-language/runs.d.ts +31 -0
  43. package/dist/code-language/runs.d.ts.map +1 -1
  44. package/dist/code-language/runs.js +105 -0
  45. package/dist/code-language/runs.js.map +1 -1
  46. package/dist/code-language/stream.d.ts +22 -0
  47. package/dist/code-language/stream.d.ts.map +1 -1
  48. package/dist/code-language/stream.js +259 -9
  49. package/dist/code-language/stream.js.map +1 -1
  50. package/dist/code-language/types.d.ts +5 -4
  51. package/dist/code-language/types.d.ts.map +1 -1
  52. package/dist/codeblock/index.d.ts +26 -2
  53. package/dist/codeblock/index.d.ts.map +1 -1
  54. package/dist/codeblock/index.js +57 -4
  55. package/dist/codeblock/index.js.map +1 -1
  56. package/dist/flow/draw.d.ts +50 -0
  57. package/dist/flow/draw.d.ts.map +1 -1
  58. package/dist/flow/draw.js +132 -33
  59. package/dist/flow/draw.js.map +1 -1
  60. package/dist/flow/gl/color.d.ts +13 -0
  61. package/dist/flow/gl/color.d.ts.map +1 -0
  62. package/dist/flow/gl/color.js +82 -0
  63. package/dist/flow/gl/color.js.map +1 -0
  64. package/dist/flow/gl/index.d.ts +60 -0
  65. package/dist/flow/gl/index.d.ts.map +1 -0
  66. package/dist/flow/gl/index.js +283 -0
  67. package/dist/flow/gl/index.js.map +1 -0
  68. package/dist/flow/gl/pack.d.ts +133 -0
  69. package/dist/flow/gl/pack.d.ts.map +1 -0
  70. package/dist/flow/gl/pack.js +408 -0
  71. package/dist/flow/gl/pack.js.map +1 -0
  72. package/dist/flow/gl/renderer.d.ts +95 -0
  73. package/dist/flow/gl/renderer.d.ts.map +1 -0
  74. package/dist/flow/gl/renderer.js +459 -0
  75. package/dist/flow/gl/renderer.js.map +1 -0
  76. package/dist/flow/gl/shaders.d.ts +79 -0
  77. package/dist/flow/gl/shaders.d.ts.map +1 -0
  78. package/dist/flow/gl/shaders.js +346 -0
  79. package/dist/flow/gl/shaders.js.map +1 -0
  80. package/dist/flow/gl/text.d.ts +318 -0
  81. package/dist/flow/gl/text.d.ts.map +1 -0
  82. package/dist/flow/gl/text.js +1103 -0
  83. package/dist/flow/gl/text.js.map +1 -0
  84. package/dist/flow/index.d.ts +1 -1
  85. package/dist/flow/index.d.ts.map +1 -1
  86. package/dist/flow/index.js +619 -64
  87. package/dist/flow/index.js.map +1 -1
  88. package/dist/flow/node.d.ts +621 -72
  89. package/dist/flow/node.d.ts.map +1 -1
  90. package/dist/flow/node.js +2253 -1018
  91. package/dist/flow/node.js.map +1 -1
  92. package/dist/flow/paint.d.ts +35 -0
  93. package/dist/flow/paint.d.ts.map +1 -0
  94. package/dist/flow/paint.js +393 -0
  95. package/dist/flow/paint.js.map +1 -0
  96. package/dist/flow/paths.d.ts +14 -0
  97. package/dist/flow/paths.d.ts.map +1 -1
  98. package/dist/flow/paths.js +45 -19
  99. package/dist/flow/paths.js.map +1 -1
  100. package/dist/flow/scene.d.ts +584 -0
  101. package/dist/flow/scene.d.ts.map +1 -0
  102. package/dist/flow/scene.js +1453 -0
  103. package/dist/flow/scene.js.map +1 -0
  104. package/dist/flow/types.d.ts +111 -1
  105. package/dist/flow/types.d.ts.map +1 -1
  106. package/dist/html/controls.d.ts +28 -0
  107. package/dist/html/controls.d.ts.map +1 -1
  108. package/dist/html/controls.js +52 -2
  109. package/dist/html/controls.js.map +1 -1
  110. package/dist/html/css/calc.d.ts +14 -0
  111. package/dist/html/css/calc.d.ts.map +1 -0
  112. package/dist/html/css/calc.js +344 -0
  113. package/dist/html/css/calc.js.map +1 -0
  114. package/dist/html/css/cascade.d.ts +262 -6
  115. package/dist/html/css/cascade.d.ts.map +1 -1
  116. package/dist/html/css/cascade.js +1373 -67
  117. package/dist/html/css/cascade.js.map +1 -1
  118. package/dist/html/css/color.d.ts +23 -0
  119. package/dist/html/css/color.d.ts.map +1 -0
  120. package/dist/html/css/color.js +740 -0
  121. package/dist/html/css/color.js.map +1 -0
  122. package/dist/html/css/content.d.ts +92 -0
  123. package/dist/html/css/content.d.ts.map +1 -0
  124. package/dist/html/css/content.js +388 -0
  125. package/dist/html/css/content.js.map +1 -0
  126. package/dist/html/css/counter-styles.d.ts +76 -0
  127. package/dist/html/css/counter-styles.d.ts.map +1 -0
  128. package/dist/html/css/counter-styles.js +893 -0
  129. package/dist/html/css/counter-styles.js.map +1 -0
  130. package/dist/html/css/decode.d.ts +12 -0
  131. package/dist/html/css/decode.d.ts.map +1 -0
  132. package/dist/html/css/decode.js +116 -0
  133. package/dist/html/css/decode.js.map +1 -0
  134. package/dist/html/css/parse.d.ts +110 -10
  135. package/dist/html/css/parse.d.ts.map +1 -1
  136. package/dist/html/css/parse.js +1473 -180
  137. package/dist/html/css/parse.js.map +1 -1
  138. package/dist/html/css/style.d.ts +501 -15
  139. package/dist/html/css/style.d.ts.map +1 -1
  140. package/dist/html/css/style.js +3729 -206
  141. package/dist/html/css/style.js.map +1 -1
  142. package/dist/html/css/ua.d.ts.map +1 -1
  143. package/dist/html/css/ua.js +65 -16
  144. package/dist/html/css/ua.js.map +1 -1
  145. package/dist/html/css/values.d.ts +37 -3
  146. package/dist/html/css/values.d.ts.map +1 -1
  147. package/dist/html/css/values.js +122 -48
  148. package/dist/html/css/values.js.map +1 -1
  149. package/dist/html/css/vars.d.ts +52 -0
  150. package/dist/html/css/vars.d.ts.map +1 -0
  151. package/dist/html/css/vars.js +358 -0
  152. package/dist/html/css/vars.js.map +1 -0
  153. package/dist/html/dom.d.ts +30 -4
  154. package/dist/html/dom.d.ts.map +1 -1
  155. package/dist/html/dom.js +278 -17
  156. package/dist/html/dom.js.map +1 -1
  157. package/dist/html/fonts.d.ts +106 -0
  158. package/dist/html/fonts.d.ts.map +1 -0
  159. package/dist/html/fonts.js +565 -0
  160. package/dist/html/fonts.js.map +1 -0
  161. package/dist/html/gif.d.ts +15 -0
  162. package/dist/html/gif.d.ts.map +1 -0
  163. package/dist/html/gif.js +229 -0
  164. package/dist/html/gif.js.map +1 -0
  165. package/dist/html/index.d.ts +50 -11
  166. package/dist/html/index.d.ts.map +1 -1
  167. package/dist/html/index.js +115 -9
  168. package/dist/html/index.js.map +1 -1
  169. package/dist/html/layout/block.d.ts +226 -3
  170. package/dist/html/layout/block.d.ts.map +1 -1
  171. package/dist/html/layout/block.js +3077 -219
  172. package/dist/html/layout/block.js.map +1 -1
  173. package/dist/html/layout/boxes.d.ts +367 -17
  174. package/dist/html/layout/boxes.d.ts.map +1 -1
  175. package/dist/html/layout/boxes.js +1742 -182
  176. package/dist/html/layout/boxes.js.map +1 -1
  177. package/dist/html/layout/cache.d.ts +58 -0
  178. package/dist/html/layout/cache.d.ts.map +1 -0
  179. package/dist/html/layout/cache.js +224 -0
  180. package/dist/html/layout/cache.js.map +1 -0
  181. package/dist/html/layout/collapse.d.ts +45 -0
  182. package/dist/html/layout/collapse.d.ts.map +1 -0
  183. package/dist/html/layout/collapse.js +246 -0
  184. package/dist/html/layout/collapse.js.map +1 -0
  185. package/dist/html/layout/css-grid.d.ts +7 -0
  186. package/dist/html/layout/css-grid.d.ts.map +1 -0
  187. package/dist/html/layout/css-grid.js +900 -0
  188. package/dist/html/layout/css-grid.js.map +1 -0
  189. package/dist/html/layout/flex.d.ts.map +1 -1
  190. package/dist/html/layout/flex.js +1025 -89
  191. package/dist/html/layout/flex.js.map +1 -1
  192. package/dist/html/layout/floats.d.ts +45 -6
  193. package/dist/html/layout/floats.d.ts.map +1 -1
  194. package/dist/html/layout/floats.js +113 -14
  195. package/dist/html/layout/floats.js.map +1 -1
  196. package/dist/html/layout/grid-lines.d.ts +39 -0
  197. package/dist/html/layout/grid-lines.d.ts.map +1 -0
  198. package/dist/html/layout/grid-lines.js +119 -0
  199. package/dist/html/layout/grid-lines.js.map +1 -0
  200. package/dist/html/layout/grid.d.ts +53 -0
  201. package/dist/html/layout/grid.d.ts.map +1 -0
  202. package/dist/html/layout/grid.js +153 -0
  203. package/dist/html/layout/grid.js.map +1 -0
  204. package/dist/html/layout/inline.d.ts +154 -1
  205. package/dist/html/layout/inline.d.ts.map +1 -1
  206. package/dist/html/layout/inline.js +3598 -175
  207. package/dist/html/layout/inline.js.map +1 -1
  208. package/dist/html/layout/shaping.d.ts +6 -0
  209. package/dist/html/layout/shaping.d.ts.map +1 -0
  210. package/dist/html/layout/shaping.js +100 -0
  211. package/dist/html/layout/shaping.js.map +1 -0
  212. package/dist/html/layout/table.d.ts +3 -0
  213. package/dist/html/layout/table.d.ts.map +1 -1
  214. package/dist/html/layout/table.js +681 -175
  215. package/dist/html/layout/table.js.map +1 -1
  216. package/dist/html/layout/tracks.d.ts +44 -0
  217. package/dist/html/layout/tracks.d.ts.map +1 -0
  218. package/dist/html/layout/tracks.js +349 -0
  219. package/dist/html/layout/tracks.js.map +1 -0
  220. package/dist/html/node.d.ts +169 -4
  221. package/dist/html/node.d.ts.map +1 -1
  222. package/dist/html/node.js +1429 -137
  223. package/dist/html/node.js.map +1 -1
  224. package/dist/html/paint.d.ts +92 -3
  225. package/dist/html/paint.d.ts.map +1 -1
  226. package/dist/html/paint.js +3635 -107
  227. package/dist/html/paint.js.map +1 -1
  228. package/dist/html/resources.d.ts +60 -14
  229. package/dist/html/resources.d.ts.map +1 -1
  230. package/dist/html/resources.js +122 -36
  231. package/dist/html/resources.js.map +1 -1
  232. package/dist/html/surfaces.d.ts +23 -0
  233. package/dist/html/surfaces.d.ts.map +1 -0
  234. package/dist/html/surfaces.js +68 -0
  235. package/dist/html/surfaces.js.map +1 -0
  236. package/dist/html/svg.d.ts +80 -0
  237. package/dist/html/svg.d.ts.map +1 -0
  238. package/dist/html/svg.js +442 -0
  239. package/dist/html/svg.js.map +1 -0
  240. package/dist/html/url.d.ts +23 -0
  241. package/dist/html/url.d.ts.map +1 -0
  242. package/dist/html/url.js +80 -0
  243. package/dist/html/url.js.map +1 -0
  244. package/dist/index.d.ts +1 -1
  245. package/dist/index.d.ts.map +1 -1
  246. package/dist/index.js.map +1 -1
  247. package/dist/internal/codelines.d.ts +42 -0
  248. package/dist/internal/codelines.d.ts.map +1 -0
  249. package/dist/internal/codelines.js +170 -0
  250. package/dist/internal/codelines.js.map +1 -0
  251. package/dist/internal/collate.d.ts +3 -0
  252. package/dist/internal/collate.d.ts.map +1 -0
  253. package/dist/internal/collate.js +11 -0
  254. package/dist/internal/collate.js.map +1 -0
  255. package/dist/internal/markdown/ast.d.ts +11 -0
  256. package/dist/internal/markdown/ast.d.ts.map +1 -1
  257. package/dist/internal/markdown/parse.d.ts.map +1 -1
  258. package/dist/internal/markdown/parse.js +109 -8
  259. package/dist/internal/markdown/parse.js.map +1 -1
  260. package/dist/internal/prewarm.d.ts +18 -0
  261. package/dist/internal/prewarm.d.ts.map +1 -0
  262. package/dist/internal/prewarm.js +42 -0
  263. package/dist/internal/prewarm.js.map +1 -0
  264. package/dist/internal/sdf.d.ts +20 -0
  265. package/dist/internal/sdf.d.ts.map +1 -0
  266. package/dist/internal/sdf.js +216 -0
  267. package/dist/internal/sdf.js.map +1 -0
  268. package/dist/internal/splice.d.ts +12 -0
  269. package/dist/internal/splice.d.ts.map +1 -0
  270. package/dist/internal/splice.js +23 -0
  271. package/dist/internal/splice.js.map +1 -0
  272. package/dist/internal/window.d.ts +12 -0
  273. package/dist/internal/window.d.ts.map +1 -1
  274. package/dist/internal/window.js +23 -6
  275. package/dist/internal/window.js.map +1 -1
  276. package/dist/maps/anchors.d.ts +6 -1
  277. package/dist/maps/anchors.d.ts.map +1 -1
  278. package/dist/maps/anchors.js +260 -1
  279. package/dist/maps/anchors.js.map +1 -1
  280. package/dist/maps/controller.d.ts +57 -0
  281. package/dist/maps/controller.d.ts.map +1 -1
  282. package/dist/maps/controller.js +97 -18
  283. package/dist/maps/controller.js.map +1 -1
  284. package/dist/maps/gl/buckets.d.ts +5 -1
  285. package/dist/maps/gl/buckets.d.ts.map +1 -1
  286. package/dist/maps/gl/buckets.js +5 -2
  287. package/dist/maps/gl/buckets.js.map +1 -1
  288. package/dist/maps/gl/build-worker.d.ts +2 -0
  289. package/dist/maps/gl/build-worker.d.ts.map +1 -1
  290. package/dist/maps/gl/build-worker.js +1 -1
  291. package/dist/maps/gl/build-worker.js.map +1 -1
  292. package/dist/maps/gl/cover.d.ts +23 -4
  293. package/dist/maps/gl/cover.d.ts.map +1 -1
  294. package/dist/maps/gl/cover.js +61 -48
  295. package/dist/maps/gl/cover.js.map +1 -1
  296. package/dist/maps/gl/placement.d.ts +21 -8
  297. package/dist/maps/gl/placement.d.ts.map +1 -1
  298. package/dist/maps/gl/placement.js +133 -58
  299. package/dist/maps/gl/placement.js.map +1 -1
  300. package/dist/maps/gl/renderer.d.ts +36 -4
  301. package/dist/maps/gl/renderer.d.ts.map +1 -1
  302. package/dist/maps/gl/renderer.js +81 -24
  303. package/dist/maps/gl/renderer.js.map +1 -1
  304. package/dist/maps/gl/shaders.d.ts +18 -17
  305. package/dist/maps/gl/shaders.d.ts.map +1 -1
  306. package/dist/maps/gl/shaders.js +34 -32
  307. package/dist/maps/gl/shaders.js.map +1 -1
  308. package/dist/maps/gl/store.d.ts +10 -0
  309. package/dist/maps/gl/store.d.ts.map +1 -1
  310. package/dist/maps/gl/store.js +47 -4
  311. package/dist/maps/gl/store.js.map +1 -1
  312. package/dist/maps/gl/text.d.ts +153 -24
  313. package/dist/maps/gl/text.d.ts.map +1 -1
  314. package/dist/maps/gl/text.js +379 -63
  315. package/dist/maps/gl/text.js.map +1 -1
  316. package/dist/maps/gl/view.d.ts +293 -2
  317. package/dist/maps/gl/view.d.ts.map +1 -1
  318. package/dist/maps/gl/view.js +317 -82
  319. package/dist/maps/gl/view.js.map +1 -1
  320. package/dist/maps/icons.d.ts +32 -0
  321. package/dist/maps/icons.d.ts.map +1 -0
  322. package/dist/maps/icons.js +151 -0
  323. package/dist/maps/icons.js.map +1 -0
  324. package/dist/maps/index.d.ts +2 -0
  325. package/dist/maps/index.d.ts.map +1 -1
  326. package/dist/maps/index.js +2 -0
  327. package/dist/maps/index.js.map +1 -1
  328. package/dist/maps/labels.d.ts +56 -4
  329. package/dist/maps/labels.d.ts.map +1 -1
  330. package/dist/maps/labels.js +213 -46
  331. package/dist/maps/labels.js.map +1 -1
  332. package/dist/maps/node.d.ts +17 -0
  333. package/dist/maps/node.d.ts.map +1 -1
  334. package/dist/maps/node.js +64 -30
  335. package/dist/maps/node.js.map +1 -1
  336. package/dist/maps/proj.d.ts +48 -0
  337. package/dist/maps/proj.d.ts.map +1 -1
  338. package/dist/maps/proj.js +73 -0
  339. package/dist/maps/proj.js.map +1 -1
  340. package/dist/maps/sources.d.ts +5 -3
  341. package/dist/maps/sources.d.ts.map +1 -1
  342. package/dist/maps/sources.js +5 -3
  343. package/dist/maps/sources.js.map +1 -1
  344. package/dist/maps/style.d.ts +35 -0
  345. package/dist/maps/style.d.ts.map +1 -1
  346. package/dist/maps/style.js.map +1 -1
  347. package/dist/maps/styles.d.ts +12 -0
  348. package/dist/maps/styles.d.ts.map +1 -1
  349. package/dist/maps/styles.js +120 -6
  350. package/dist/maps/styles.js.map +1 -1
  351. package/dist/maps/tiles.d.ts +31 -11
  352. package/dist/maps/tiles.d.ts.map +1 -1
  353. package/dist/maps/tiles.js +61 -27
  354. package/dist/maps/tiles.js.map +1 -1
  355. package/dist/maps/types.d.ts +28 -4
  356. package/dist/maps/types.d.ts.map +1 -1
  357. package/dist/markdown/index.d.ts.map +1 -1
  358. package/dist/markdown/index.js +209 -68
  359. package/dist/markdown/index.js.map +1 -1
  360. package/dist/markdown/spans.d.ts.map +1 -1
  361. package/dist/markdown/spans.js +6 -0
  362. package/dist/markdown/spans.js.map +1 -1
  363. package/dist/qml/containers.d.ts.map +1 -1
  364. package/dist/qml/containers.js +6 -1
  365. package/dist/qml/containers.js.map +1 -1
  366. package/dist/qml/interaction.d.ts.map +1 -1
  367. package/dist/qml/interaction.js +38 -5
  368. package/dist/qml/interaction.js.map +1 -1
  369. package/dist/qml/layouts.d.ts.map +1 -1
  370. package/dist/qml/layouts.js +21 -6
  371. package/dist/qml/layouts.js.map +1 -1
  372. package/dist/qml/qtquick.d.ts.map +1 -1
  373. package/dist/qml/qtquick.js +9 -2
  374. package/dist/qml/qtquick.js.map +1 -1
  375. package/dist/qml/react.d.ts.map +1 -1
  376. package/dist/qml/react.js +9 -7
  377. package/dist/qml/react.js.map +1 -1
  378. package/dist/reorder/clock.d.ts +7 -0
  379. package/dist/reorder/clock.d.ts.map +1 -0
  380. package/dist/reorder/clock.js +26 -0
  381. package/dist/reorder/clock.js.map +1 -0
  382. package/dist/reorder/index.d.ts +25 -3
  383. package/dist/reorder/index.d.ts.map +1 -1
  384. package/dist/reorder/index.js +64 -19
  385. package/dist/reorder/index.js.map +1 -1
  386. package/dist/rich-text-editor/index.d.ts +0 -11
  387. package/dist/rich-text-editor/index.d.ts.map +1 -1
  388. package/dist/rich-text-editor/index.js +38 -9
  389. package/dist/rich-text-editor/index.js.map +1 -1
  390. package/dist/rich-text-editor/keymap.d.ts.map +1 -1
  391. package/dist/rich-text-editor/keymap.js +2 -1
  392. package/dist/rich-text-editor/keymap.js.map +1 -1
  393. package/dist/rich-text-editor/keys.d.ts +17 -6
  394. package/dist/rich-text-editor/keys.d.ts.map +1 -1
  395. package/dist/rich-text-editor/keys.js +135 -26
  396. package/dist/rich-text-editor/keys.js.map +1 -1
  397. package/dist/rich-text-editor/markdown.d.ts.map +1 -1
  398. package/dist/rich-text-editor/markdown.js +30 -1
  399. package/dist/rich-text-editor/markdown.js.map +1 -1
  400. package/dist/rich-text-editor/marks.d.ts +9 -0
  401. package/dist/rich-text-editor/marks.d.ts.map +1 -0
  402. package/dist/rich-text-editor/marks.js +83 -0
  403. package/dist/rich-text-editor/marks.js.map +1 -0
  404. package/dist/rich-text-editor/render.d.ts.map +1 -1
  405. package/dist/rich-text-editor/render.js +28 -10
  406. package/dist/rich-text-editor/render.js.map +1 -1
  407. package/dist/rich-text-editor/replace.d.ts +13 -0
  408. package/dist/rich-text-editor/replace.d.ts.map +1 -0
  409. package/dist/rich-text-editor/replace.js +64 -0
  410. package/dist/rich-text-editor/replace.js.map +1 -0
  411. package/dist/rich-text-editor/toolbar.d.ts.map +1 -1
  412. package/dist/rich-text-editor/toolbar.js +1 -1
  413. package/dist/rich-text-editor/toolbar.js.map +1 -1
  414. package/dist/rich-text-editor/virtual.d.ts.map +1 -1
  415. package/dist/rich-text-editor/virtual.js +17 -0
  416. package/dist/rich-text-editor/virtual.js.map +1 -1
  417. package/dist/richtext/node.d.ts +46 -2
  418. package/dist/richtext/node.d.ts.map +1 -1
  419. package/dist/richtext/node.js +187 -4
  420. package/dist/richtext/node.js.map +1 -1
  421. package/dist/richtext/runs.d.ts +13 -8
  422. package/dist/richtext/runs.d.ts.map +1 -1
  423. package/dist/richtext/runs.js +18 -8
  424. package/dist/richtext/runs.js.map +1 -1
  425. package/dist/table/index.d.ts.map +1 -1
  426. package/dist/table/index.js +23 -0
  427. package/dist/table/index.js.map +1 -1
  428. package/dist/table/rows.d.ts +3 -2
  429. package/dist/table/rows.d.ts.map +1 -1
  430. package/dist/table/rows.js +20 -6
  431. package/dist/table/rows.js.map +1 -1
  432. package/dist/terminal/index.d.ts.map +1 -1
  433. package/dist/terminal/index.js +4 -0
  434. package/dist/terminal/index.js.map +1 -1
  435. package/dist/terminal/vt/pty.d.ts +13 -2
  436. package/dist/terminal/vt/pty.d.ts.map +1 -1
  437. package/dist/terminal/vt/pty.js +18 -3
  438. package/dist/terminal/vt/pty.js.map +1 -1
  439. package/dist/terminal-output/index.d.ts.map +1 -1
  440. package/dist/terminal-output/index.js +103 -23
  441. package/dist/terminal-output/index.js.map +1 -1
  442. package/dist/terminal-output/runs.d.ts +4 -1
  443. package/dist/terminal-output/runs.d.ts.map +1 -1
  444. package/dist/terminal-output/runs.js +5 -1
  445. package/dist/terminal-output/runs.js.map +1 -1
  446. package/dist/three/canvas.d.ts.map +1 -1
  447. package/dist/three/canvas.js +85 -7
  448. package/dist/three/canvas.js.map +1 -1
  449. package/dist/three/events.d.ts +22 -4
  450. package/dist/three/events.d.ts.map +1 -1
  451. package/dist/three/events.js +34 -4
  452. package/dist/three/events.js.map +1 -1
  453. package/dist/tree/index.d.ts.map +1 -1
  454. package/dist/tree/index.js +17 -0
  455. package/dist/tree/index.js.map +1 -1
  456. package/package.json +42 -33
  457. package/src/charts/node.ts +62 -26
  458. package/src/charts/render.ts +120 -30
  459. package/src/code/index.ts +36 -32
  460. package/src/code-editor/completion.ts +3 -2
  461. package/src/code-editor/index.ts +19 -0
  462. package/src/code-editor/node.ts +1574 -184
  463. package/src/code-language/index.ts +1 -1
  464. package/src/code-language/languages/glsl.ts +12 -2
  465. package/src/code-language/languages/javascript.ts +21 -4
  466. package/src/code-language/languages/json.ts +12 -2
  467. package/src/code-language/languages/shell.ts +12 -2
  468. package/src/code-language/languages/sql.ts +22 -2
  469. package/src/code-language/runs.ts +111 -1
  470. package/src/code-language/stream.ts +257 -17
  471. package/src/code-language/types.ts +5 -4
  472. package/src/codeblock/index.ts +68 -4
  473. package/src/flow/draw.ts +167 -41
  474. package/src/flow/gl/color.ts +85 -0
  475. package/src/flow/gl/index.ts +342 -0
  476. package/src/flow/gl/pack.ts +585 -0
  477. package/src/flow/gl/renderer.ts +679 -0
  478. package/src/flow/gl/shaders.ts +371 -0
  479. package/src/flow/gl/text.ts +1295 -0
  480. package/src/flow/index.ts +794 -67
  481. package/src/flow/node.ts +2496 -1256
  482. package/src/flow/paint.ts +511 -0
  483. package/src/flow/paths.ts +80 -15
  484. package/src/flow/scene.ts +2199 -0
  485. package/src/flow/types.ts +113 -1
  486. package/src/html/controls.ts +73 -2
  487. package/src/html/css/calc.ts +385 -0
  488. package/src/html/css/cascade.ts +1481 -62
  489. package/src/html/css/color.ts +755 -0
  490. package/src/html/css/content.ts +417 -0
  491. package/src/html/css/counter-styles.ts +973 -0
  492. package/src/html/css/decode.ts +130 -0
  493. package/src/html/css/parse.ts +1514 -160
  494. package/src/html/css/style.ts +4131 -223
  495. package/src/html/css/ua.ts +66 -17
  496. package/src/html/css/values.ts +150 -50
  497. package/src/html/css/vars.ts +348 -0
  498. package/src/html/dom.ts +284 -17
  499. package/src/html/fonts.ts +641 -0
  500. package/src/html/gif.ts +233 -0
  501. package/src/html/index.ts +180 -20
  502. package/src/html/layout/block.ts +3560 -208
  503. package/src/html/layout/boxes.ts +2141 -190
  504. package/src/html/layout/cache.ts +289 -0
  505. package/src/html/layout/collapse.ts +317 -0
  506. package/src/html/layout/css-grid.ts +1063 -0
  507. package/src/html/layout/flex.ts +1115 -106
  508. package/src/html/layout/floats.ts +128 -14
  509. package/src/html/layout/grid-lines.ts +141 -0
  510. package/src/html/layout/grid.ts +181 -0
  511. package/src/html/layout/inline.ts +4187 -192
  512. package/src/html/layout/shaping.ts +140 -0
  513. package/src/html/layout/table.ts +696 -186
  514. package/src/html/layout/tracks.ts +437 -0
  515. package/src/html/node.ts +1585 -156
  516. package/src/html/paint.ts +4582 -267
  517. package/src/html/resources.ts +161 -50
  518. package/src/html/surfaces.ts +81 -0
  519. package/src/html/svg.ts +504 -0
  520. package/src/html/url.ts +86 -0
  521. package/src/index.ts +2 -0
  522. package/src/internal/codelines.ts +200 -0
  523. package/src/internal/collate.ts +12 -0
  524. package/src/internal/markdown/ast.ts +11 -0
  525. package/src/internal/markdown/parse.ts +154 -11
  526. package/src/internal/prewarm.ts +45 -0
  527. package/src/internal/sdf.ts +227 -0
  528. package/src/internal/splice.ts +25 -0
  529. package/src/internal/window.ts +36 -5
  530. package/src/maps/anchors.ts +307 -1
  531. package/src/maps/controller.ts +116 -31
  532. package/src/maps/gl/buckets.ts +5 -1
  533. package/src/maps/gl/build-worker.ts +13 -2
  534. package/src/maps/gl/cover.ts +79 -44
  535. package/src/maps/gl/placement.ts +221 -69
  536. package/src/maps/gl/renderer.ts +104 -20
  537. package/src/maps/gl/shaders.ts +35 -32
  538. package/src/maps/gl/store.ts +66 -17
  539. package/src/maps/gl/text.ts +444 -71
  540. package/src/maps/gl/view.ts +361 -96
  541. package/src/maps/icons.ts +183 -0
  542. package/src/maps/index.ts +4 -0
  543. package/src/maps/labels.ts +269 -49
  544. package/src/maps/node.ts +82 -49
  545. package/src/maps/proj.ts +87 -0
  546. package/src/maps/sources.ts +5 -3
  547. package/src/maps/style.ts +32 -0
  548. package/src/maps/styles.ts +215 -4
  549. package/src/maps/tiles.ts +64 -31
  550. package/src/maps/types.ts +28 -4
  551. package/src/markdown/index.ts +293 -92
  552. package/src/markdown/spans.ts +5 -0
  553. package/src/qml/containers.tsx +6 -1
  554. package/src/qml/interaction.tsx +42 -5
  555. package/src/qml/layouts.tsx +21 -6
  556. package/src/qml/qtquick.tsx +9 -2
  557. package/src/qml/react.tsx +7 -5
  558. package/src/reorder/clock.ts +27 -0
  559. package/src/reorder/index.ts +100 -27
  560. package/src/rich-text-editor/index.ts +37 -12
  561. package/src/rich-text-editor/keymap.ts +1 -1
  562. package/src/rich-text-editor/keys.ts +135 -26
  563. package/src/rich-text-editor/markdown.ts +36 -1
  564. package/src/rich-text-editor/marks.ts +101 -0
  565. package/src/rich-text-editor/render.ts +49 -12
  566. package/src/rich-text-editor/replace.ts +85 -0
  567. package/src/rich-text-editor/toolbar.ts +1 -1
  568. package/src/rich-text-editor/virtual.ts +25 -0
  569. package/src/richtext/node.ts +235 -14
  570. package/src/richtext/runs.ts +23 -12
  571. package/src/table/index.ts +29 -0
  572. package/src/table/rows.ts +18 -8
  573. package/src/terminal/index.ts +4 -0
  574. package/src/terminal/vt/pty.ts +16 -2
  575. package/src/terminal-output/index.ts +129 -31
  576. package/src/terminal-output/runs.ts +16 -2
  577. package/src/three/canvas.ts +102 -8
  578. package/src/three/events.ts +49 -9
  579. package/src/tree/index.ts +19 -0
@@ -58,6 +58,7 @@ import {
58
58
  } from 'react-x11/keysyms';
59
59
 
60
60
  import { startInterval, stopInterval } from '../code-language/timers.js';
61
+ import { spliceAll } from '../internal/splice.js';
61
62
  import type { TimerId } from '../code-language/timers.js';
62
63
  import {
63
64
  clampPos,
@@ -125,6 +126,10 @@ export interface CodeEditorHandle {
125
126
  readonly language: Language | null;
126
127
  readonly canUndo: boolean;
127
128
  readonly canRedo: boolean;
129
+ /** The `diagnostics` prop as it stands in the text now: moved with every
130
+ * edit since the prop was set, the way the code they point at moved, and
131
+ * without the ones whose code an edit took away. */
132
+ readonly diagnostics: readonly Diagnostic[];
128
133
  selectedText(): string;
129
134
  replaceRange(from: Position, to: Position, text: string): void;
130
135
  insertText(text: string): void;
@@ -235,8 +240,19 @@ interface ClipboardLike {
235
240
  read(options?: { selection?: string }): Promise<unknown>;
236
241
  }
237
242
 
238
- interface HistoryEntry {
239
- value: string;
243
+ /**
244
+ * One undoable change: the lines `[from, from + before.length)` of the text
245
+ * before it became `after`, the selection it found, which an undo restores,
246
+ * and the one it left, which a redo does. A change and not a copy of the
247
+ * text: a history of copies held two hundred of them, which for a 2.5 MB
248
+ * file is half a gigabyte, and put every undo through a full reset.
249
+ */
250
+ interface HistoryStep {
251
+ from: number;
252
+ before: readonly string[];
253
+ after: readonly string[];
254
+ caretBefore: Position;
255
+ anchorBefore: Position;
240
256
  caret: Position;
241
257
  anchor: Position;
242
258
  }
@@ -260,6 +276,53 @@ const SEVERITY_COLORS: Record<string, string> = {
260
276
  hint: '#8b8f98',
261
277
  };
262
278
 
279
+ /** Two token style maps that colour alike — the same object, or an
280
+ * inline one written again with the same entries, which a render hands
281
+ * over every time and which laid every line out again. */
282
+ function sameTokenStyles(
283
+ a: TokenStyles | undefined,
284
+ b: TokenStyles | undefined,
285
+ ): boolean {
286
+ if (a === b) return true;
287
+ if (!a || !b) return false;
288
+ const ka = Object.keys(a);
289
+ if (ka.length !== Object.keys(b).length) return false;
290
+ for (const k of ka) {
291
+ const x = (a as Record<string, Record<string, unknown> | undefined>)[k];
292
+ const y = (b as Record<string, Record<string, unknown> | undefined>)[k];
293
+ if (x === y) continue;
294
+ if (!x || !y) return false;
295
+ const fx = Object.keys(x);
296
+ if (fx.length !== Object.keys(y).length) return false;
297
+ for (const f of fx) if (x[f] !== y[f]) return false;
298
+ }
299
+ return true;
300
+ }
301
+
302
+ /** Two diagnostics lists that say the same thing, in the same order. */
303
+ function sameDiagnostics(
304
+ a: readonly Diagnostic[] | undefined,
305
+ b: readonly Diagnostic[] | undefined,
306
+ ): boolean {
307
+ if (a === b) return true;
308
+ if ((a?.length ?? 0) !== (b?.length ?? 0)) return false;
309
+ if (!a || !b) return true; // both empty
310
+ for (let i = 0; i < a.length; i++) {
311
+ const x = a[i];
312
+ const y = b[i];
313
+ if (
314
+ !posEqual(x.from, y.from) ||
315
+ !posEqual(x.to, y.to) ||
316
+ x.severity !== y.severity ||
317
+ x.message !== y.message ||
318
+ x.source !== y.source
319
+ ) {
320
+ return false;
321
+ }
322
+ }
323
+ return true;
324
+ }
325
+
263
326
  function cpToUtf16(text: string, cp: number): number {
264
327
  let i = 0;
265
328
  for (let n = 0; n < cp && i < text.length; n++) {
@@ -311,7 +374,28 @@ export interface EditorMouseEvent {
311
374
  preventDefault?(): void;
312
375
  }
313
376
 
377
+ type Rect = { x: number; y: number; width: number; height: number };
378
+
379
+ /** What `_repaintSince` compares a change against: the rows, offsets,
380
+ * gutter and thumbs the view had before it. */
381
+ interface ViewBefore {
382
+ caretLine: number;
383
+ selection: [number, number] | null;
384
+ brackets: [number, number] | null;
385
+ scrollX: number;
386
+ scrollY: number;
387
+ gutter: number;
388
+ empty: boolean;
389
+ /** What the thumbs are sized by: `_maxScrollX()` and `_maxScrollY()`. */
390
+ maxX: number;
391
+ maxY: number;
392
+ }
393
+
314
394
  interface LineCacheEntry {
395
+ /** The line it was built from — the same string until the line is
396
+ * edited, so a lookup that finds it needs nothing else recomputed. */
397
+ raw: string;
398
+ tabSize: number;
315
399
  display: string;
316
400
  tokens: readonly Token[];
317
401
  styleKey: string;
@@ -319,11 +403,309 @@ interface LineCacheEntry {
319
403
  map: TabMap;
320
404
  }
321
405
 
406
+ /** A line's tokens with no language. One array, so that a cached line
407
+ * entry, which is checked against its tokens by identity, is found again —
408
+ * a fresh `[]` per lookup made every paint of a plain-text editor lay out
409
+ * every line in view. */
410
+ const NO_TOKENS: readonly Token[] = Object.freeze([]);
411
+
412
+ /** Line entries past which the ones farthest from the view are let go. An
413
+ * entry holds a text layout, native memory on every backend, and a file
414
+ * scrolled end to end had one for every line in it. */
415
+ const LINE_CACHE_MAX = 2048;
416
+
417
+ /** Display code units past which a line is laid out in pieces
418
+ * (`ChunkedLayout`). */
419
+ const CHUNKED_PAST = 2048;
420
+ /** A piece is at least this long, and is cut here at the latest. */
421
+ const CHUNK_MIN = 256;
422
+ const CHUNK_MAX = 2048;
423
+
424
+ /**
425
+ * A line too long to lay out whole, laid out in pieces placed end to end.
426
+ *
427
+ * A minified file or a log line of a hundred thousand characters is one
428
+ * text layout otherwise, and everything the editor asks of a layout is
429
+ * linear in its length or worse: putting the caret at the end of a
430
+ * 100,000-character line took 4.3 s in CoreText's caret lookup, every
431
+ * keystroke shaped the whole line again, and on X11 a line past 32,767
432
+ * device pixels threw out of the paint. A piece answers in a layout of its own size, only the
433
+ * pieces a keystroke changed are shaped again (`_chunkLayouts`, keyed by
434
+ * what a piece holds — which is why the cuts are made by content,
435
+ * `chunkBreaks`), and only the pieces in view are drawn (`drawSpan`).
436
+ *
437
+ * What it gives up is shaping across the seams: a kerning pair or a
438
+ * ligature that straddles one is set as two. Code is set in a monospace
439
+ * face, and a seam is one place in a few hundred characters.
440
+ */
441
+ /** A layout's first baseline, from its top — where its text sits, whatever
442
+ * fallback face in it is taller than the base. Both engines give their
443
+ * lines one (ntk's `lines[].baseline`, the CoreText bridge's too); null
444
+ * for one that does not. */
445
+ function baselineOf(layout: LayoutLike): number | null {
446
+ const lines = (layout as { lines?: ReadonlyArray<{ baseline?: unknown }> })
447
+ .lines;
448
+ const b = lines?.[0]?.baseline;
449
+ return typeof b === 'number' && Number.isFinite(b) ? b : null;
450
+ }
451
+
452
+ /** One piece of a `ChunkedLayout`: where it starts in the line's display
453
+ * text, in code points and in UTF-16 units, where it starts across, its
454
+ * text, its layout, and the runs its spans styled it in — length, colour,
455
+ * weight and style, four entries a run — which is what a rebuild compares
456
+ * to keep it (`_chunkedLayout`).
457
+ *
458
+ * `layout` is null until something asks about the piece or one past it,
459
+ * when it is made from `spans`. `advance` is how far the next piece
460
+ * starts after this one: a guess until then, and after it the caret's
461
+ * place at the piece's end (`advanceOf`). */
462
+ interface Chunk {
463
+ cp: number;
464
+ u16: number;
465
+ x: number;
466
+ text: string;
467
+ layout: LayoutLike | null;
468
+ spans: Array<Record<string, unknown>> | null;
469
+ advance: number;
470
+ runs: readonly unknown[];
471
+ }
472
+
473
+ /**
474
+ * How far a laid-out piece takes the line across: where the caret stands at
475
+ * its end. Not its layout's width, which leaves out the white space a piece
476
+ * ends on, as a layout's width does. A piece cut after a space set the next
477
+ * one a column to its left, so the space vanished from the screen and every
478
+ * caret after it was a column out: seven columns by the end of a line of
479
+ * 78,000 characters, from the eight pieces that ended on one. The larger of
480
+ * the two, since the caret after a right-to-left piece is at its left.
481
+ */
482
+ function advanceOf(layout: LayoutLike, text: string): number {
483
+ const end = layout.caretPosition(utf16ToCp(text, text.length)).x;
484
+ return Math.max(layout.width, end);
485
+ }
486
+
487
+ class ChunkedLayout implements LayoutLike {
488
+ /**
489
+ * The pieces before this one are laid out, and every piece up to it sits
490
+ * where it is drawn. Past it, a piece sits where the guesses before it
491
+ * put it.
492
+ *
493
+ * Laying out a line of a million characters shaped every piece of it
494
+ * before the first paint: 1.4 s to open a minified file, of which the
495
+ * view needed one piece. So a piece is laid out when anything asks about
496
+ * it or a piece after it: a draw, a caret, a click. Everything left of
497
+ * what is asked about is laid out too, so whatever is drawn is where one
498
+ * layout of the whole line would put it. Only the width of the part
499
+ * nobody has looked at is a guess, and the editor's scroll extent follows
500
+ * it as it firms up (`grew`).
501
+ */
502
+ private _laid = 0;
503
+
504
+ constructor(
505
+ /** The pieces, in order, and never empty. */
506
+ readonly chunks: readonly Chunk[],
507
+ /** A piece's layout, from its spans. */
508
+ private readonly _layOut: (
509
+ spans: Array<Record<string, unknown>>,
510
+ ) => LayoutLike,
511
+ /** Told the line's width when laying out more of it changed it. */
512
+ private readonly _grew: (width: number) => void = () => {},
513
+ ) {
514
+ while (this._laid < chunks.length && chunks[this._laid].layout) {
515
+ this._laid += 1;
516
+ }
517
+ // the first piece at once: the line's height, and its baseline
518
+ this._layOutThrough(0);
519
+ }
520
+
521
+ get width(): number {
522
+ // the last piece's ink, as one layout of the whole line measures it
523
+ const last = this.chunks[this.chunks.length - 1];
524
+ return last.x + (last.layout?.width ?? last.advance);
525
+ }
526
+
527
+ get height(): number {
528
+ let height = 0;
529
+ for (let k = 0; k < this._laid; k++) {
530
+ height = Math.max(height, this.chunks[k].layout!.height);
531
+ }
532
+ return height;
533
+ }
534
+
535
+ /** Lay out the pieces through `k`, and move every piece after them to
536
+ * where the laid-out ones now end. */
537
+ private _layOutThrough(k: number): void {
538
+ const chunks = this.chunks;
539
+ const from = this._laid;
540
+ const through = Math.min(k, chunks.length - 1);
541
+ if (through < from) return;
542
+ const before = this.width;
543
+ for (let j = from; j <= through; j++) {
544
+ const piece = chunks[j];
545
+ if (!piece.layout) {
546
+ piece.layout = this._layOut(piece.spans!);
547
+ piece.spans = null;
548
+ piece.advance = advanceOf(piece.layout, piece.text);
549
+ }
550
+ }
551
+ this._laid = through + 1;
552
+ // the first piece that was not laid out sits where the ones before it
553
+ // end, and every piece after it where the one before it does
554
+ let x = chunks[from].x;
555
+ for (let j = from; j < chunks.length; j++) {
556
+ const piece = chunks[j];
557
+ piece.x = x;
558
+ x += piece.advance;
559
+ }
560
+ const width = this.width;
561
+ if (width !== before) this._grew(width);
562
+ }
563
+
564
+ /** The last piece whose `key` is at or before `at`, laid out. */
565
+ private _pieceAt(at: number, key: 'cp' | 'u16' | 'x') {
566
+ const chunks = this.chunks;
567
+ if (key === 'x') {
568
+ // a piece's place across is known only as far as the pieces are laid
569
+ // out: lay them out until the one `at` falls in is among them
570
+ while (this._laid < chunks.length && chunks[this._laid].x <= at) {
571
+ this._layOutThrough(this._laid);
572
+ }
573
+ }
574
+ let lo = 0;
575
+ let hi = chunks.length - 1;
576
+ while (lo < hi) {
577
+ const mid = (lo + hi + 1) >> 1;
578
+ if (chunks[mid][key] <= at) lo = mid;
579
+ else hi = mid - 1;
580
+ }
581
+ this._layOutThrough(lo);
582
+ return chunks[lo] as Chunk & { layout: LayoutLike };
583
+ }
584
+
585
+ caretPosition(cp: number): { x: number; y: number; height: number } {
586
+ const piece = this._pieceAt(cp, 'cp');
587
+ const at = piece.layout.caretPosition(cp - piece.cp);
588
+ return { x: piece.x + at.x, y: at.y, height: at.height };
589
+ }
590
+
591
+ indexAt(x: number, y: number): number {
592
+ const piece = this._pieceAt(x, 'x');
593
+ return piece.cp + piece.layout.indexAt(x - piece.x, y);
594
+ }
595
+
596
+ /** `caretPosition`'s x for a UTF-16 offset into the display text, with
597
+ * the code point counted inside the one piece rather than the line. */
598
+ caretXAt(u16: number): number {
599
+ const piece = this._pieceAt(u16, 'u16');
600
+ const cp = utf16ToCp(piece.text, u16 - piece.u16);
601
+ return piece.x + piece.layout.caretPosition(cp).x;
602
+ }
603
+
604
+ /** `indexAt`, answered as a UTF-16 offset into the display text. */
605
+ indexAtUtf16(x: number, y: number): number {
606
+ const piece = this._pieceAt(x, 'x');
607
+ const cp = piece.layout.indexAt(x - piece.x, y);
608
+ return piece.u16 + cpToUtf16(piece.text, cp);
609
+ }
610
+
611
+ draw(ctx: unknown, x: number, y: number): void {
612
+ this.drawSpan(ctx, x, y, -Infinity, Infinity);
613
+ }
614
+
615
+ /**
616
+ * The pieces that reach into `[from, to)` across the line, each with its
617
+ * first baseline at `baseline` — a piece holding an emoji is laid out
618
+ * taller than the rest, and set by its top it sat on a baseline of its
619
+ * own. `y` places a piece whose engine gives no baseline. The pieces up to
620
+ * `to` are laid out on the way, so each is drawn where it belongs.
621
+ */
622
+ drawSpan(
623
+ ctx: unknown,
624
+ x: number,
625
+ y: number,
626
+ from: number,
627
+ to: number,
628
+ baseline: number | null = null,
629
+ ) {
630
+ const chunks = this.chunks;
631
+ for (let k = 0; k < chunks.length; k++) {
632
+ // where an unlaid piece sits is exact here: the ones before it are
633
+ // laid out by now
634
+ if (chunks[k].x >= to) break;
635
+ this._layOutThrough(k);
636
+ const piece = chunks[k] as Chunk & { layout: LayoutLike };
637
+ if (piece.x + piece.layout.width <= from) continue;
638
+ const b = baseline == null ? null : baselineOf(piece.layout);
639
+ piece.layout.draw(ctx, x + piece.x, b == null ? y : baseline! - b);
640
+ }
641
+ }
642
+ }
643
+
644
+ /** Where a long line is cut into pieces: after a delimiter that the few
645
+ * characters before it pick out, between `CHUNK_MIN` and `CHUNK_MAX` apart.
646
+ * Chosen by what the text says there rather than by how far the line has
647
+ * come, so a character typed into a piece moves the cuts near it and none
648
+ * of the rest — the pieces after it are the same text as before, and their
649
+ * layouts are found again rather than shaped again. */
650
+ function chunkBreaks(display: string, from = 0): number[] {
651
+ const breaks = [from];
652
+ let last = from;
653
+ for (let i = from + 1; i < display.length; i++) {
654
+ const since = i - last;
655
+ if (since < CHUNK_MIN) continue;
656
+ const code = display.charCodeAt(i - 1);
657
+ // never between the halves of a surrogate pair
658
+ if (code >= 0xd800 && code <= 0xdbff) continue;
659
+ const delimiter =
660
+ code === 32 || code === 44 || code === 59 || code === 123 || code === 125;
661
+ const picked =
662
+ delimiter &&
663
+ ((display.charCodeAt(i - 2) * 31 + display.charCodeAt(i - 3) * 7) &
664
+ 15) ===
665
+ 0;
666
+ if (picked || since >= CHUNK_MAX) {
667
+ breaks.push(i);
668
+ last = i;
669
+ }
670
+ }
671
+ return breaks;
672
+ }
673
+
674
+ /** A line's spans cut at `breaks`. */
675
+ function chunkSpans(
676
+ spans: Array<Record<string, unknown>>,
677
+ breaks: readonly number[],
678
+ ): Array<Array<Record<string, unknown>>> {
679
+ const pieces: Array<Array<Record<string, unknown>>> = [];
680
+ let piece: Array<Record<string, unknown>> = [];
681
+ let at = 0;
682
+ let next = 1;
683
+ for (const span of spans) {
684
+ let text = span.text as string;
685
+ while (text.length > 0) {
686
+ const cut = next < breaks.length ? breaks[next] : Infinity;
687
+ const take = Math.min(text.length, cut - at);
688
+ piece.push({ ...span, text: text.slice(0, take) });
689
+ text = text.slice(take);
690
+ at += take;
691
+ if (at === cut) {
692
+ pieces.push(piece);
693
+ piece = [];
694
+ next += 1;
695
+ }
696
+ }
697
+ }
698
+ if (piece.length > 0) pieces.push(piece);
699
+ return pieces;
700
+ }
701
+
322
702
  export class CodeEditorNode extends Node implements CodeEditorHandle {
323
703
  private _lines: string[];
324
704
  /** What `_lines` was last built from — how a controlled `value` that the
325
- * parent refused to update snaps back on the next read (see `_syncProps`). */
326
- private _synced: string;
705
+ * parent refused to update snaps back on the next read (see `_syncProps`).
706
+ * Null where no `value` prop reads it: the text is joined only for a
707
+ * reader. */
708
+ private _synced: string | null;
327
709
  private _caret: Position = { line: 0, ch: 0 };
328
710
  private _anchor: Position = { line: 0, ch: 0 };
329
711
  private _scrollX = 0;
@@ -331,15 +713,29 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
331
713
  private _focused = false;
332
714
  private _caretOn = false;
333
715
  private _blinkTimer: TimerId = null;
716
+ /** Whether the last paint drew a horizontal thumb — see `paint`. */
717
+ private _thumbAcross = false;
334
718
  private _goalX: number | null = null;
335
719
  private _drag: 'char' | 'word' | 'line' | null = null;
336
720
  private _dragOrigin: Position = { line: 0, ch: 0 };
337
721
  /** Escape was the last key, so the next Tab leaves instead of indenting —
338
722
  * the editor's way of not being a keyboard trap (see `defaultKeyDown`). */
339
723
  private _tabEscapes = false;
340
- private _history: HistoryEntry[];
724
+ /** The steps, oldest first; `_historyIndex` of them are applied. */
725
+ private _history: HistoryStep[] = [];
341
726
  private _historyIndex = 0;
342
727
  private _undoRun: string | null = null;
728
+ /** The lines the last change replaced, in the text it left: what
729
+ * `_repaintSince` claims of an edit. */
730
+ /** The `diagnostics` prop, moved through the edits since it was set —
731
+ * see `_mapDiagnostics` — and the prop array it was taken from. */
732
+ private _diagnostics: Diagnostic[] = [];
733
+ private _diagnosticsFrom: readonly Diagnostic[] | undefined = undefined;
734
+ private _lastEdit: {
735
+ fromLine: number;
736
+ removed: number;
737
+ inserted: number;
738
+ } | null = null;
343
739
  private _pendingValue: string | null = null;
344
740
  private _keyNative: unknown = null;
345
741
  private _tok: Tokenizer | null = null;
@@ -348,6 +744,12 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
348
744
  private _metricsKey = '';
349
745
  private _lineH = 0;
350
746
  private _charW = 0;
747
+ /** The base face's first baseline and the bottom of its glyphs, from the
748
+ * top of a row: every row's text sits on the one, and a squiggle hugs
749
+ * the other, whatever a fallback face in the row would make of its own
750
+ * layout. Null baseline: an engine that does not say (see `paint`). */
751
+ private _baseline: number | null = null;
752
+ private _textBottom = 0;
351
753
  private _widest = 0;
352
754
 
353
755
  constructor(props: Record<string, unknown>, app: unknown) {
@@ -360,9 +762,6 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
360
762
  : '';
361
763
  this._lines = initial.split('\n');
362
764
  this._synced = initial;
363
- this._history = [
364
- { value: initial, caret: this._caret, anchor: this._anchor },
365
- ];
366
765
  // Without this nothing focuses the editor and no key ever reaches it —
367
766
  // an app's `focusable`/`tabIndex` prop still overrides either way, which
368
767
  // is how `<CodeEditor disabled>` stays out of the tab order.
@@ -408,9 +807,10 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
408
807
  set value(next: string) {
409
808
  const text = next == null ? '' : String(next);
410
809
  if (text === this._lines.join('\n')) return;
810
+ const view = this._viewBefore();
411
811
  this._setText(text);
412
- this._noteExternal();
413
- this._repaint();
812
+ this._repaintSince(view, this._lastEdit);
813
+ this._lastEdit = null;
414
814
  }
415
815
 
416
816
  get name(): string | undefined {
@@ -442,17 +842,29 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
442
842
  const text = String(v);
443
843
  if (text === this._synced) return;
444
844
  this._setText(text);
445
- this._noteExternal();
446
845
  }
447
846
 
847
+ /**
848
+ * A text from outside — a controlled `value`, the `value` setter — as an
849
+ * undoable step of its own, and as the edit it really is: the lines it
850
+ * shares with the text before it at either end are neither tokenized nor
851
+ * laid out again. A formatter's output, or a parent echoing a value back
852
+ * with one line changed, used to reset the whole editor.
853
+ */
448
854
  private _setText(text: string): void {
449
- this._lines = text.split('\n');
855
+ const next = text.split('\n');
856
+ const caret = clampPos(next, this._caret);
857
+ const anchor = clampPos(next, this._anchor);
450
858
  this._synced = text;
451
- this._caret = clampPos(this._lines, this._caret);
452
- this._anchor = clampPos(this._lines, this._anchor);
453
- this._lineCache.clear();
454
- this._widest = 0;
455
- this._tokenizer()?.setLines(this._lines);
859
+ this._edit(0, this._lines.length, next, null, caret, anchor);
860
+ this._caret = caret;
861
+ this._anchor = anchor;
862
+ // the widest line may have gone: what is known of the lines left
863
+ let widest = 0;
864
+ for (const entry of this._lineCache.values()) {
865
+ widest = Math.max(widest, entry.layout?.width ?? 0);
866
+ }
867
+ this._widest = widest;
456
868
  }
457
869
 
458
870
  // --- tokenizer -----------------------------------------------------------
@@ -466,7 +878,7 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
466
878
  ? lang.createTokenizer({
467
879
  invalidate: (fromLine) => {
468
880
  for (const key of this._lineCache.keys()) {
469
- if (key >= fromLine) this._lineCache.delete(key);
881
+ if (key >= fromLine) this._dropLine(key);
470
882
  }
471
883
  this._repaint();
472
884
  },
@@ -474,6 +886,7 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
474
886
  : null;
475
887
  this._tok?.setLines(this._lines);
476
888
  this._lineCache.clear();
889
+ this._chunkedStash = null;
477
890
  }
478
891
  return this._tok;
479
892
  }
@@ -548,6 +961,13 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
548
961
  const probe = fonts.layout('Mg', s);
549
962
  this._lineH = probe.height || Number(s.size) * 1.4;
550
963
  this._charW = fonts.layout('0', s).width || Number(s.size) * 0.6;
964
+ this._baseline = baselineOf(probe);
965
+ const line = (probe as { lines?: ReadonlyArray<{ descent?: unknown }> })
966
+ .lines?.[0];
967
+ this._textBottom =
968
+ this._baseline != null && typeof line?.descent === 'number'
969
+ ? this._baseline + line.descent
970
+ : this._lineH;
551
971
  }
552
972
 
553
973
  private _lineHeight(): number {
@@ -633,18 +1053,22 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
633
1053
 
634
1054
  private _lineEntry(line: number): LineCacheEntry {
635
1055
  const raw = this._lines[line];
636
- const map = tabMap(raw, this._tabSize());
637
- const tokens = this._tokenizer()?.lineTokens(line) ?? [];
1056
+ const tabSize = this._tabSize();
1057
+ const tokens = this._tokenizer()?.lineTokens(line) ?? NO_TOKENS;
638
1058
  const styleKey = this._metricsKey;
639
1059
  const cached = this._lineCache.get(line);
1060
+ // the line's own string, not its display: building the tab map is
1061
+ // linear in the line, and a long one is asked for many times a frame
640
1062
  if (
641
1063
  cached &&
642
- cached.display === map.display &&
1064
+ cached.raw === raw &&
1065
+ cached.tabSize === tabSize &&
643
1066
  cached.tokens === tokens &&
644
1067
  cached.styleKey === styleKey
645
1068
  ) {
646
1069
  return cached;
647
1070
  }
1071
+ const map = tabMap(raw, tabSize);
648
1072
  const fonts = this._fonts();
649
1073
  const base = this._textStyle();
650
1074
  let layout: LayoutLike | null = null;
@@ -676,9 +1100,30 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
676
1100
  if (cursor < map.display.length) {
677
1101
  spans.push({ text: map.display.slice(cursor) });
678
1102
  }
679
- layout = fonts.layout(spans.length > 0 ? spans : [{ text: '' }], base);
1103
+ if (map.display.length > CHUNKED_PAST) {
1104
+ const stash = this._chunkedStash;
1105
+ const previous =
1106
+ cached?.layout instanceof ChunkedLayout &&
1107
+ cached.styleKey === styleKey
1108
+ ? cached.layout
1109
+ : stash?.styleKey === styleKey
1110
+ ? stash.layout
1111
+ : null;
1112
+ layout = this._chunkedLayout(
1113
+ fonts,
1114
+ base,
1115
+ styleKey,
1116
+ map.display,
1117
+ spans,
1118
+ previous,
1119
+ );
1120
+ } else {
1121
+ layout = fonts.layout(spans.length > 0 ? spans : [{ text: '' }], base);
1122
+ }
680
1123
  }
681
1124
  const entry: LineCacheEntry = {
1125
+ raw,
1126
+ tabSize,
682
1127
  display: map.display,
683
1128
  tokens,
684
1129
  styleKey,
@@ -690,11 +1135,194 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
690
1135
  return entry;
691
1136
  }
692
1137
 
1138
+ /**
1139
+ * A long line in pieces (`ChunkedLayout`), each piece's layout kept by
1140
+ * what it holds, so an edit shapes only the pieces whose text or colours
1141
+ * it changed. Bounded like the line cache it serves.
1142
+ */
1143
+ private _chunkLayouts = new Map<string, LayoutLike>();
1144
+
1145
+ /** The last long line an edit took out of the line cache, which the next
1146
+ * long line laid out starts from (`_chunkedLayout`). Checked piece by
1147
+ * piece against what it is asked for, so one from another line, or from
1148
+ * before an edit that changed everything, is simply not used. */
1149
+ private _chunkedStash: { styleKey: string; layout: ChunkedLayout } | null =
1150
+ null;
1151
+
1152
+ /** Take a line's entry out of the cache, keeping a long one's pieces for
1153
+ * the rebuild the edit that dropped it is about to ask for. */
1154
+ private _dropLine(line: number): void {
1155
+ const entry = this._lineCache.get(line);
1156
+ if (entry?.layout instanceof ChunkedLayout) {
1157
+ this._chunkedStash = { styleKey: entry.styleKey, layout: entry.layout };
1158
+ }
1159
+ this._lineCache.delete(line);
1160
+ }
1161
+
1162
+ private _chunkedLayout(
1163
+ fonts: FontsLike,
1164
+ base: Record<string, unknown>,
1165
+ styleKey: string,
1166
+ display: string,
1167
+ spans: Array<Record<string, unknown>>,
1168
+ previous: ChunkedLayout | null,
1169
+ ): LayoutLike {
1170
+ const chunks: Chunk[] = [];
1171
+ // The pieces an edit left alone, from the start of the line: the same
1172
+ // text at the same place, in the same runs. Their layouts, and where
1173
+ // they sit, are what they were, and the break after each is a break of
1174
+ // the new line too, since a break depends on the text before it alone —
1175
+ // all but the old line's last piece, which ended where the old line did.
1176
+ // A keystroke at the end of a line of a million characters built a key
1177
+ // from every piece's text and looked each one up: most of its 30 ms.
1178
+ let from = 0;
1179
+ let si = 0;
1180
+ let spanStart = 0;
1181
+ if (previous) {
1182
+ const old = previous.chunks;
1183
+ for (let k = 0; k < old.length - 1; k++) {
1184
+ const chunk = old[k];
1185
+ // a piece nobody laid out is no saving to keep, and its place across
1186
+ // was a guess
1187
+ if (!chunk.layout) break;
1188
+ if (chunk.u16 !== from || !display.startsWith(chunk.text, from)) break;
1189
+ const end = from + chunk.text.length;
1190
+ let i = si;
1191
+ let start = spanStart;
1192
+ let at = from;
1193
+ let r = 0;
1194
+ let same = true;
1195
+ while (at < end) {
1196
+ const span = spans[i];
1197
+ const spanEnd = start + (span.text as string).length;
1198
+ const take = Math.min(spanEnd, end) - at;
1199
+ const runs = chunk.runs;
1200
+ if (
1201
+ runs[r] !== take ||
1202
+ runs[r + 1] !== (span.color ?? '') ||
1203
+ runs[r + 2] !== (span.weight ?? '') ||
1204
+ runs[r + 3] !== (span.style ?? '')
1205
+ ) {
1206
+ same = false;
1207
+ break;
1208
+ }
1209
+ r += 4;
1210
+ at += take;
1211
+ if (at === spanEnd) {
1212
+ start = spanEnd;
1213
+ i += 1;
1214
+ }
1215
+ }
1216
+ if (!same || r !== chunk.runs.length) break;
1217
+ chunks.push(chunk);
1218
+ from = end;
1219
+ si = i;
1220
+ spanStart = start;
1221
+ }
1222
+ }
1223
+ const last = chunks[chunks.length - 1];
1224
+ let cp = last ? last.cp + utf16ToCp(last.text, last.text.length) : 0;
1225
+ let x = last ? last.x + last.advance : 0;
1226
+ // the spans from where the kept pieces end, the first one cut there
1227
+ const rest = spans.slice(si);
1228
+ if (rest.length > 0 && from > spanStart) {
1229
+ rest[0] = {
1230
+ ...rest[0],
1231
+ text: (rest[0].text as string).slice(from - spanStart),
1232
+ };
1233
+ }
1234
+ // The rest are laid out when something asks about them (ChunkedLayout),
1235
+ // and guessed across until then: a column a code point, which is what
1236
+ // a monospace face makes of ASCII to the pixel.
1237
+ this._metrics();
1238
+ const column = this._charW;
1239
+ const breaks = chunkBreaks(display, from).map((b) => b - from);
1240
+ let u16 = from;
1241
+ for (const piece of chunkSpans(rest, breaks)) {
1242
+ const text = piece.map((sp) => sp.text as string).join('');
1243
+ const runs: unknown[] = [];
1244
+ for (const sp of piece) {
1245
+ runs.push(
1246
+ (sp.text as string).length,
1247
+ sp.color ?? '',
1248
+ sp.weight ?? '',
1249
+ sp.style ?? '',
1250
+ );
1251
+ }
1252
+ const points = utf16ToCp(text, text.length);
1253
+ const guess = points * column;
1254
+ chunks.push({
1255
+ cp,
1256
+ u16,
1257
+ x,
1258
+ text,
1259
+ layout: null,
1260
+ spans: piece,
1261
+ advance: guess,
1262
+ runs,
1263
+ });
1264
+ cp += points;
1265
+ u16 += text.length;
1266
+ x += guess;
1267
+ }
1268
+ if (chunks.length === 0) {
1269
+ // an empty display is never chunked; kept here all the same so the
1270
+ // layout is never empty
1271
+ const layout = fonts.layout([{ text: '' }], base);
1272
+ chunks.push({
1273
+ cp: 0,
1274
+ u16: 0,
1275
+ x: 0,
1276
+ text: '',
1277
+ layout,
1278
+ spans: null,
1279
+ advance: layout.width,
1280
+ runs: [],
1281
+ });
1282
+ }
1283
+ return new ChunkedLayout(
1284
+ chunks,
1285
+ (piece) => this._pieceLayout(fonts, base, styleKey, piece),
1286
+ (width) => {
1287
+ if (width > this._widest) this._widest = width;
1288
+ },
1289
+ );
1290
+ }
1291
+
1292
+ /** A long line's piece, laid out — or found, where the same text in the
1293
+ * same colours was laid out before (`_chunkLayouts`). */
1294
+ private _pieceLayout(
1295
+ fonts: FontsLike,
1296
+ base: Record<string, unknown>,
1297
+ styleKey: string,
1298
+ piece: Array<Record<string, unknown>>,
1299
+ ): LayoutLike {
1300
+ const key =
1301
+ styleKey +
1302
+ '|' +
1303
+ piece
1304
+ .map(
1305
+ (sp) =>
1306
+ `${sp.color ?? ''},${sp.weight ?? ''},${sp.style ?? ''},${sp.text}`,
1307
+ )
1308
+ .join('\u0000');
1309
+ let layout = this._chunkLayouts.get(key);
1310
+ if (!layout) {
1311
+ if (this._chunkLayouts.size > 4096) this._chunkLayouts.clear();
1312
+ layout = fonts.layout(piece, base);
1313
+ this._chunkLayouts.set(key, layout);
1314
+ }
1315
+ return layout;
1316
+ }
1317
+
693
1318
  /** Caret x (device pixels from the text origin) for a position. */
694
1319
  private _caretX(pos: Position): number {
695
1320
  const entry = this._lineEntry(pos.line);
696
1321
  if (!entry.layout) return 0;
697
1322
  const disp = entry.map.toDisplay(pos.ch);
1323
+ if (entry.layout instanceof ChunkedLayout) {
1324
+ return entry.layout.caretXAt(disp);
1325
+ }
698
1326
  return entry.layout.caretPosition(utf16ToCp(entry.display, disp)).x;
699
1327
  }
700
1328
 
@@ -712,8 +1340,11 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
712
1340
  );
713
1341
  const entry = this._lineEntry(line);
714
1342
  if (!entry.layout) return { line, ch: 0 };
715
- const cp = entry.layout.indexAt(x - textX + this._scrollX, 0);
716
- const disp = cpToUtf16(entry.display, cp);
1343
+ const at = x - textX + this._scrollX;
1344
+ const disp =
1345
+ entry.layout instanceof ChunkedLayout
1346
+ ? entry.layout.indexAtUtf16(at, 0)
1347
+ : cpToUtf16(entry.display, entry.layout.indexAt(at, 0));
717
1348
  return clampPos(this._lines, { line, ch: entry.map.toRaw(disp) });
718
1349
  }
719
1350
 
@@ -774,25 +1405,32 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
774
1405
 
775
1406
  // --- scrolling -----------------------------------------------------------
776
1407
 
1408
+ // Both limits, and every offset a caret is revealed at, are whole device
1409
+ // pixels: a line is a measured height, and a shift that is not whole is
1410
+ // not a copy (`_blitScroll`).
777
1411
  private _maxScrollY(): number {
778
1412
  const content = this._contentRect();
779
1413
  return Math.max(
780
1414
  0,
781
- this._lines.length * this._lineHeight() - content.height,
1415
+ Math.ceil(this._lines.length * this._lineHeight() - content.height),
782
1416
  );
783
1417
  }
784
1418
 
785
1419
  private _maxScrollX(): number {
786
1420
  const content = this._contentRect();
787
1421
  const textW = content.width - this._gutterWidth();
788
- return Math.max(0, this._widest + CARET_MARGIN * this._scale - textW);
1422
+ return Math.max(
1423
+ 0,
1424
+ Math.ceil(this._widest + CARET_MARGIN * this._scale - textW),
1425
+ );
789
1426
  }
790
1427
 
791
- /** Wheel notches (deltaX/deltaY arrive as ±1 per notch) → three lines a
792
- * notch, in the device pixels the offsets are kept in. */
1428
+ /** The wheel, in the unit core hands every scroller: logical pixels, not
1429
+ * notches — a notch is 48 of them (react-x11's `WHEEL_NOTCH_PX`) and a
1430
+ * touchpad reports what it measured — so a notch moves the text as far
1431
+ * as it moves a `<box>` pane beside the editor. */
793
1432
  handleWheel(ev: { deltaX: number; deltaY: number }): boolean {
794
- const step = this._lineHeight() * 3;
795
- return this._scrollByDevice(ev.deltaX * step, ev.deltaY * step);
1433
+ return this.scrollBy(ev.deltaX, ev.deltaY);
796
1434
  }
797
1435
 
798
1436
  /** Logical pixels, like core's own `scrollBy`: application code writes
@@ -808,9 +1446,87 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
808
1446
  const nx = Math.max(0, Math.min(this._scrollX + dx, this._maxScrollX()));
809
1447
  const ny = Math.max(0, Math.min(this._scrollY + dy, this._maxScrollY()));
810
1448
  if (nx === this._scrollX && ny === this._scrollY) return false;
1449
+ const shiftX = this._scrollX - nx;
1450
+ const shiftY = this._scrollY - ny;
811
1451
  this._scrollX = nx;
812
1452
  this._scrollY = ny;
813
- this.root?.invalidate(false, this.abs, 'scroll');
1453
+ if (!this._blitScroll(shiftX, shiftY)) {
1454
+ this.root?.invalidate(false, this.abs, 'scroll');
1455
+ }
1456
+ return true;
1457
+ }
1458
+
1459
+ /**
1460
+ * The rect a scroll of the view shifts (core's `scrollContents`): the
1461
+ * content box less the scroll thumbs' strips, which stay put while the
1462
+ * text moves under them — and sideways, less the gutter as well, which
1463
+ * stays put while the text moves beside it.
1464
+ */
1465
+ private _scrollRegion(sideways: boolean): Rect {
1466
+ const box = this._contentRect();
1467
+ const bar = 4 * this._scale; // a thumb's inset, see `paint`
1468
+ const across = this._maxScrollX() > 0 ? bar : 0;
1469
+ const gutter = sideways ? this._gutterWidth() : 0;
1470
+ return {
1471
+ x: box.x + gutter,
1472
+ y: box.y,
1473
+ width: box.width - gutter - bar,
1474
+ height: box.height - across,
1475
+ };
1476
+ }
1477
+
1478
+ /**
1479
+ * A scroll along one axis as a blit: inside `_scrollRegion` the new frame
1480
+ * is the old one shifted. The thumbs' strips are carved out of the region
1481
+ * and claimed beside it, edge to edge, which leaves the frame a blit; the
1482
+ * band the shift exposes is all `paint` draws (`paintDamage()`).
1483
+ * `pinned` are rows that change as well as move — an edit's, the
1484
+ * caret's, when revealing it scrolled — which the frame repaints after
1485
+ * the copy instead of refusing it, so the caller claims inside them.
1486
+ *
1487
+ * False where the promise would not hold: both ways at once, since the
1488
+ * gutter moves with the text down and not across; off the device grid,
1489
+ * where a shift is not a copy; and under a background image, which does
1490
+ * not translate. Core checks the rest — the border ring, a rounded
1491
+ * corner, anything drawn over the box — and repaints the region itself
1492
+ * when one of them fails.
1493
+ */
1494
+ private _blitScroll(
1495
+ shiftX: number,
1496
+ shiftY: number,
1497
+ pinned: Rect[] | null = null,
1498
+ ): boolean {
1499
+ if ((shiftX !== 0) === (shiftY !== 0)) return false;
1500
+ if (!Number.isInteger(shiftX) || !Number.isInteger(shiftY)) return false;
1501
+ if ((this.style as Record<string, unknown>).backgroundImage) return false;
1502
+ const box = this._contentRect();
1503
+ const bar = 4 * this._scale;
1504
+ const rect = this._scrollRegion(shiftX !== 0);
1505
+ const whole = [rect.x, rect.y, rect.width, rect.height, bar];
1506
+ if (!whole.every(Number.isInteger)) return false;
1507
+ if (rect.width <= 0 || rect.height <= 0) return false;
1508
+ if (Math.abs(shiftX) >= rect.width || Math.abs(shiftY) >= rect.height) {
1509
+ return false;
1510
+ }
1511
+ this.scrollContents(rect, shiftX, shiftY, null, pinned);
1512
+ this.invalidate(
1513
+ false,
1514
+ { x: box.x + box.width - bar, y: box.y, width: bar, height: box.height },
1515
+ 'scroll',
1516
+ );
1517
+ const across = box.height - rect.height;
1518
+ if (across > 0) {
1519
+ this.invalidate(
1520
+ false,
1521
+ {
1522
+ x: box.x,
1523
+ y: rect.y + rect.height,
1524
+ width: box.width - bar,
1525
+ height: across,
1526
+ },
1527
+ 'scroll',
1528
+ );
1529
+ }
814
1530
  return true;
815
1531
  }
816
1532
 
@@ -818,19 +1534,19 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
818
1534
  this._metrics();
819
1535
  const content = this._contentRect();
820
1536
  const lineTop = this._caret.line * this._lineH;
821
- if (lineTop < this._scrollY) this._scrollY = lineTop;
1537
+ if (lineTop < this._scrollY) this._scrollY = Math.floor(lineTop);
822
1538
  const lineBottom = lineTop + this._lineH;
823
1539
  if (lineBottom > this._scrollY + content.height) {
824
- this._scrollY = lineBottom - content.height;
1540
+ this._scrollY = Math.ceil(lineBottom - content.height);
825
1541
  }
826
1542
  const textW = content.width - this._gutterWidth();
827
1543
  const x = this._caretX(this._caret);
828
1544
  const margin = CARET_MARGIN * this._scale;
829
1545
  if (x < this._scrollX + margin) {
830
- this._scrollX = Math.max(0, x - margin);
1546
+ this._scrollX = Math.max(0, Math.floor(x - margin));
831
1547
  }
832
1548
  if (x > this._scrollX + textW - margin) {
833
- this._scrollX = x - textW + margin;
1549
+ this._scrollX = Math.ceil(x - textW + margin);
834
1550
  }
835
1551
  this._scrollY = Math.max(0, Math.min(this._scrollY, this._maxScrollY()));
836
1552
  this._scrollX = Math.max(0, this._scrollX);
@@ -857,15 +1573,17 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
857
1573
  return ev;
858
1574
  }
859
1575
 
1576
+ /** `value` is a thunk where joining the text is the caller's cost: the
1577
+ * text is joined for a handler that is there to read it, and only then. */
860
1578
  private _fire(
861
1579
  prop: 'onChange' | 'onSubmit' | 'onSelectionChange',
862
- value: string,
1580
+ value: string | (() => string),
863
1581
  ): void {
864
1582
  const handler = this.props[prop] as
865
1583
  ((ev: CodeEditorEvent) => void) | undefined;
866
1584
  if (!handler) return;
867
1585
  const previous = this._pendingValue;
868
- this._pendingValue = value;
1586
+ this._pendingValue = typeof value === 'function' ? value() : value;
869
1587
  try {
870
1588
  handler(
871
1589
  this._makeEvent(
@@ -886,23 +1604,347 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
886
1604
  this.root?.invalidate(false, this.abs, 'text');
887
1605
  }
888
1606
 
889
- // --- history -------------------------------------------------------------
1607
+ /** What an edit or a caret move can change on screen, read before it. */
1608
+ private _viewBefore(): ViewBefore {
1609
+ const selection = this._hasSelection() ? this._selectionRange() : null;
1610
+ const brackets =
1611
+ this._focused && this.props.matchBrackets !== false
1612
+ ? this._findBracketMatch()
1613
+ : null;
1614
+ return {
1615
+ caretLine: this._caret.line,
1616
+ selection: selection && [selection[0].line, selection[1].line],
1617
+ brackets: brackets && [brackets[0].line, brackets[1].line],
1618
+ scrollX: this._scrollX,
1619
+ scrollY: this._scrollY,
1620
+ gutter: this._gutterWidth(),
1621
+ empty: this._lines.length === 1 && this._lines[0].length === 0,
1622
+ maxX: this._maxScrollX(),
1623
+ maxY: this._maxScrollY(),
1624
+ };
1625
+ }
890
1626
 
891
- private _noteExternal(): void {
892
- this._undoRun = null;
893
- const value = this._lines.join('\n');
894
- const top = this._history[this._historyIndex];
895
- if (top && top.value === value) return;
896
- this._pushHistory({ value, caret: this._caret, anchor: this._anchor });
1627
+ /**
1628
+ * Rows (inclusive ranges, any order) as the device-pixel bands they are
1629
+ * drawn in, `[top, bottom)`, merged where they touch and cut to the
1630
+ * content box. Glyphs ink a little past their row — a descender's
1631
+ * anti-aliasing below it, an accent above — into pixels the next row's
1632
+ * claim covers and this row's does not: a line edited left its old
1633
+ * descenders in the row under it. So a band reaches that far into its
1634
+ * neighbours, which the paint draws for it (`paint` culls a row either
1635
+ * side).
1636
+ */
1637
+ private _rowBands(rows: Array<[number, number]>): Array<[number, number]> {
1638
+ const content = this._contentRect();
1639
+ const lineH = this._lineH;
1640
+ const spill = 2 * this._scale;
1641
+ const bands: Array<[number, number]> = [];
1642
+ const band = (from: number, to: number): void => {
1643
+ const top = Math.max(
1644
+ content.y,
1645
+ Math.floor(content.y + from * lineH - this._scrollY - spill),
1646
+ );
1647
+ const bottom = Math.min(
1648
+ content.y + content.height,
1649
+ Math.ceil(content.y + (to + 1) * lineH - this._scrollY + spill),
1650
+ );
1651
+ if (bottom > top) bands.push([top, bottom]);
1652
+ };
1653
+ const sorted = rows.slice().sort((a, b) => a[0] - b[0]);
1654
+ let open: [number, number] | null = null;
1655
+ for (const [a, b] of sorted) {
1656
+ if (open && a <= open[1] + 1) open[1] = Math.max(open[1], b);
1657
+ else {
1658
+ if (open) band(open[0], open[1]);
1659
+ open = [a, b];
1660
+ }
1661
+ }
1662
+ if (open) band(open[0], open[1]);
1663
+ return bands;
1664
+ }
1665
+
1666
+ /** Claim the rows the squiggles of both lists are on: a linter's answer
1667
+ * arriving is a few rows, not the editor, and the layouts stay. */
1668
+ private _repaintDiagnostics(
1669
+ before: readonly Diagnostic[],
1670
+ after: readonly Diagnostic[],
1671
+ ): void {
1672
+ const content = this._contentRect();
1673
+ const lineH = this._lineH;
1674
+ if (!this.root || content.width <= 0 || !(lineH > 0)) return;
1675
+ const first = Math.floor(this._scrollY / lineH);
1676
+ const bottom = Math.ceil((this._scrollY + content.height) / lineH);
1677
+ const rows: Array<[number, number]> = [];
1678
+ const last = this._lines.length - 1;
1679
+ for (const d of [...before, ...after]) {
1680
+ // where the paint puts it: clamped to the text
1681
+ const a = Math.max(
1682
+ first,
1683
+ Math.min(last, Math.min(d.from.line, d.to.line)),
1684
+ );
1685
+ const b = Math.min(
1686
+ bottom,
1687
+ Math.min(last, Math.max(d.from.line, d.to.line)),
1688
+ );
1689
+ if (a <= b) rows.push([a, b]);
1690
+ }
1691
+ for (const [top, end] of this._rowBands(rows)) {
1692
+ this.invalidate(
1693
+ false,
1694
+ { x: content.x, y: top, width: content.width, height: end - top },
1695
+ 'text',
1696
+ );
1697
+ }
1698
+ }
1699
+
1700
+ /**
1701
+ * Claim what a change since `before` moved: the rows it touched rather
1702
+ * than the editor, so a keystroke paints its line, not the forty in view.
1703
+ *
1704
+ * A row is the caret's, the active line's and its gutter number's, before
1705
+ * and after; every row of the selection and the bracket pair, before and
1706
+ * after; the rows an edit replaced, and every row below them when it
1707
+ * added or took lines away; and a row below the edit whose tokens it
1708
+ * moved — an opened comment or string recolours what follows (the test
1709
+ * `_lineEntry` makes: the line's tokens are no longer the ones it was laid
1710
+ * out with). A thumb's strip goes with a change to what sizes it — the
1711
+ * line count, the widest line — and of the right one, only what no row
1712
+ * already repaints: a strip that overlapped a row would reach core as the
1713
+ * box around both, which is the editor again. A scroll to the caret along
1714
+ * one axis is a blit (`_blitScroll`) with those rows pinned — repainted
1715
+ * after the copy, where they now are. Whatever else moves the whole view
1716
+ * — a scroll both ways, a gutter that grew a digit, the placeholder — is
1717
+ * the editor.
1718
+ */
1719
+ private _repaintSince(
1720
+ before: ViewBefore,
1721
+ edit: { fromLine: number; removed: number; inserted: number } | null,
1722
+ ): void {
1723
+ this._caretOn = true;
1724
+ const root = this.root;
1725
+ if (!root) return;
1726
+ const content = this._contentRect();
1727
+ const lineH = this._lineH;
1728
+ const empty = this._lines.length === 1 && this._lines[0].length === 0;
1729
+ // how far the pixels move: the other way from the offsets
1730
+ const shiftX = before.scrollX - this._scrollX;
1731
+ const shiftY = before.scrollY - this._scrollY;
1732
+ if (
1733
+ content.width <= 0 ||
1734
+ content.height <= 0 ||
1735
+ !(lineH > 0) ||
1736
+ before.gutter !== this._gutterWidth() ||
1737
+ (before.empty !== empty && this.props.placeholder != null)
1738
+ ) {
1739
+ this._repaint();
1740
+ return;
1741
+ }
1742
+ const first = Math.max(0, Math.floor(this._scrollY / lineH));
1743
+ // rows in view, text or not: a row past the last line is still where a
1744
+ // line was drawn before an edit took it away
1745
+ const bottom = Math.ceil((this._scrollY + content.height) / lineH);
1746
+ const last = Math.min(this._lines.length - 1, bottom);
1747
+ // rows as inclusive ranges, merged below
1748
+ const rows: Array<[number, number]> = [];
1749
+ const add = (from: number, to: number): void => {
1750
+ const a = Math.max(first, Math.min(from, to));
1751
+ const b = Math.min(bottom, Math.max(from, to));
1752
+ if (a <= b) rows.push([a, b]);
1753
+ };
1754
+ add(before.caretLine, before.caretLine);
1755
+ add(this._caret.line, this._caret.line);
1756
+ if (before.selection) add(before.selection[0], before.selection[1]);
1757
+ if (this._hasSelection()) {
1758
+ const [a, b] = this._selectionRange();
1759
+ add(a.line, b.line);
1760
+ }
1761
+ if (before.brackets) {
1762
+ add(before.brackets[0], before.brackets[0]);
1763
+ add(before.brackets[1], before.brackets[1]);
1764
+ }
1765
+ if (this._focused && this.props.matchBrackets !== false) {
1766
+ const now = this._findBracketMatch();
1767
+ if (now) {
1768
+ add(now[0].line, now[0].line);
1769
+ add(now[1].line, now[1].line);
1770
+ }
1771
+ }
1772
+ if (edit) {
1773
+ // laid out now rather than in the paint, so the widest line — what
1774
+ // the horizontal thumb is sized by — is known before it is claimed
1775
+ const to = Math.min(last, edit.fromLine + edit.inserted - 1);
1776
+ for (let i = Math.max(first, edit.fromLine); i <= to; i++) {
1777
+ this._lineEntry(i);
1778
+ }
1779
+ if (edit.inserted !== edit.removed) {
1780
+ add(edit.fromLine, bottom);
1781
+ } else {
1782
+ add(edit.fromLine, edit.fromLine + edit.inserted - 1);
1783
+ // the rows after it that its tokens re-coloured
1784
+ const tok = this._tokenizer();
1785
+ for (
1786
+ let i = Math.max(first, edit.fromLine + edit.inserted);
1787
+ tok && i <= last;
1788
+ i++
1789
+ ) {
1790
+ const cached = this._lineCache.get(i);
1791
+ if (
1792
+ !cached ||
1793
+ cached.raw !== this._lines[i] ||
1794
+ cached.tokens !== (tok.lineTokens(i) ?? NO_TOKENS)
1795
+ ) {
1796
+ add(i, i);
1797
+ }
1798
+ }
1799
+ }
1800
+ }
1801
+ const bands = this._rowBands(rows);
1802
+ if (shiftX !== 0 || shiftY !== 0) {
1803
+ // the rows inside the region are pinned: core repaints them after the
1804
+ // copy, changed or not, so what is left to claim is what of them lies
1805
+ // beside it — the gutter, sideways; the thumbs' strips go with the blit
1806
+ const region = this._scrollRegion(shiftX !== 0);
1807
+ const pinned = bands.map(([top, bottom]) => ({
1808
+ x: region.x,
1809
+ y: top,
1810
+ width: region.width,
1811
+ height: bottom - top,
1812
+ }));
1813
+ if (!this._blitScroll(shiftX, shiftY, pinned)) {
1814
+ this._repaint();
1815
+ return;
1816
+ }
1817
+ if (region.x > content.x) {
1818
+ // stopping where the region does: past it is the horizontal
1819
+ // thumb's strip, claimed whole — and two claims that overlap reach
1820
+ // core as the box around both, which here would take in the region
1821
+ // and refuse the blit
1822
+ const end = region.y + region.height;
1823
+ for (const [top, bottom] of bands) {
1824
+ if (Math.min(bottom, end) <= top) continue;
1825
+ this.invalidate(
1826
+ false,
1827
+ {
1828
+ x: content.x,
1829
+ y: top,
1830
+ width: region.x - content.x,
1831
+ height: Math.min(bottom, end) - top,
1832
+ },
1833
+ 'text',
1834
+ );
1835
+ }
1836
+ }
1837
+ return;
1838
+ }
1839
+ for (const [top, bottom] of bands) {
1840
+ this.invalidate(
1841
+ false,
1842
+ { x: content.x, y: top, width: content.width, height: bottom - top },
1843
+ 'text',
1844
+ );
1845
+ }
1846
+ const strip = 4 * this._scale; // a thumb's inset, see `paint`
1847
+ const maxY = this._maxScrollY();
1848
+ if (maxY !== before.maxY && (maxY > 0 || before.maxY > 0)) {
1849
+ // the rows between the bands, in the right thumb's column
1850
+ let y = content.y;
1851
+ for (const [top, bottom] of [
1852
+ ...bands,
1853
+ [content.y + content.height, content.y + content.height],
1854
+ ]) {
1855
+ if (top > y) {
1856
+ this.invalidate(
1857
+ false,
1858
+ {
1859
+ x: content.x + content.width - strip,
1860
+ y,
1861
+ width: strip,
1862
+ height: top - y,
1863
+ },
1864
+ 'text',
1865
+ );
1866
+ }
1867
+ y = Math.max(y, bottom);
1868
+ }
1869
+ }
1870
+ const maxX = this._maxScrollX();
1871
+ if (maxX !== before.maxX && (maxX > 0 || before.maxX > 0)) {
1872
+ this.invalidate(
1873
+ false,
1874
+ {
1875
+ x: content.x,
1876
+ y: content.y + content.height - strip,
1877
+ width: content.width,
1878
+ height: strip,
1879
+ },
1880
+ 'text',
1881
+ );
1882
+ }
897
1883
  }
898
1884
 
899
- private _pushHistory(entry: HistoryEntry): void {
900
- this._history.length = this._historyIndex + 1;
901
- this._history.push(entry);
1885
+ // --- history -------------------------------------------------------------
1886
+
1887
+ /**
1888
+ * Record a change about to be made — `removed` lines at `from` becoming
1889
+ * `lines` — and the selection it will leave. Called before the lines
1890
+ * change, because merging needs the text the step before it left.
1891
+ *
1892
+ * `run` names a coalescable kind of edit: a change of the same run as
1893
+ * the last one, on lines that one touched or beside them, joins its step
1894
+ * rather than starting one — a word typed is one undo.
1895
+ */
1896
+ private _record(
1897
+ from: number,
1898
+ removed: number,
1899
+ lines: readonly string[],
1900
+ run: string | null,
1901
+ caret: Position,
1902
+ anchor: Position,
1903
+ ): void {
1904
+ const top = this._history[this._historyIndex - 1];
1905
+ const text = this._lines;
1906
+ if (
1907
+ top &&
1908
+ run !== null &&
1909
+ run === this._undoRun &&
1910
+ this._historyIndex === this._history.length &&
1911
+ from <= top.from + top.after.length &&
1912
+ from + removed >= top.from
1913
+ ) {
1914
+ // One step from the text before `top` to the text after this: the
1915
+ // union of the two ranges, in the text between them, read on either
1916
+ // side of what each changed.
1917
+ const lo = Math.min(top.from, from);
1918
+ const hi = Math.max(top.from + top.after.length, from + removed);
1919
+ top.before = [
1920
+ ...text.slice(lo, top.from),
1921
+ ...top.before,
1922
+ ...text.slice(top.from + top.after.length, hi),
1923
+ ];
1924
+ top.after = [
1925
+ ...text.slice(lo, from),
1926
+ ...lines,
1927
+ ...text.slice(from + removed, hi),
1928
+ ];
1929
+ top.from = lo;
1930
+ top.caret = caret;
1931
+ top.anchor = anchor;
1932
+ return;
1933
+ }
1934
+ this._history.length = this._historyIndex;
1935
+ this._history.push({
1936
+ from,
1937
+ before: text.slice(from, from + removed),
1938
+ after: lines,
1939
+ caretBefore: this._caret,
1940
+ anchorBefore: this._anchor,
1941
+ caret,
1942
+ anchor,
1943
+ });
902
1944
  if (this._history.length > UNDO_LIMIT) {
903
1945
  this._history.splice(0, this._history.length - UNDO_LIMIT);
904
1946
  }
905
- this._historyIndex = this._history.length - 1;
1947
+ this._historyIndex = this._history.length;
906
1948
  }
907
1949
 
908
1950
  breakUndoRun(): void {
@@ -913,31 +1955,143 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
913
1955
  return this._historyIndex > 0;
914
1956
  }
915
1957
 
1958
+ get diagnostics(): readonly Diagnostic[] {
1959
+ return this._currentDiagnostics();
1960
+ }
1961
+
1962
+ private _currentDiagnostics(): Diagnostic[] {
1963
+ const prop = (this.props as unknown as CodeEditorProps).diagnostics;
1964
+ if (prop !== this._diagnosticsFrom) {
1965
+ // A render hands the same answer over as a new array — a list mapped
1966
+ // from the linter's result inline — and that is not a new answer: the
1967
+ // squiggles stay where the edits since the last one moved them,
1968
+ // rather than going back to where the old text had them.
1969
+ if (!sameDiagnostics(prop, this._diagnosticsFrom)) {
1970
+ this._diagnostics = prop ? prop.slice() : [];
1971
+ }
1972
+ this._diagnosticsFrom = prop;
1973
+ }
1974
+ return this._diagnostics;
1975
+ }
1976
+
1977
+ /**
1978
+ * Move the diagnostics through `removed` lines at `from` becoming `lines`,
1979
+ * before the text changes. A squiggle is where a linter looked, and the
1980
+ * linter is asked again only once the text settles: meanwhile, typing a
1981
+ * line above one left it under the line that had been there. The change
1982
+ * is narrowed to the characters it really touched — the text the old and
1983
+ * new lines share at either end — and a position in that text keeps its
1984
+ * character. One inside the change goes to its edge: a diagnostic's start
1985
+ * to the far side of what was inserted there, so typing before a flagged
1986
+ * word moves the flag along with it, and its end to the near side, so
1987
+ * typing after the word does not stretch the flag over the new text. A
1988
+ * diagnostic whose whole range the edit took away goes with it; one that
1989
+ * points at a place rather than a range stays.
1990
+ */
1991
+ private _mapDiagnostics(
1992
+ from: number,
1993
+ removed: number,
1994
+ lines: readonly string[],
1995
+ ): void {
1996
+ const list = this._currentDiagnostics();
1997
+ if (list.length === 0) return;
1998
+ const before = this._lines.slice(from, from + removed).join('\n');
1999
+ const after = lines.join('\n');
2000
+ let head = 0;
2001
+ const most = Math.min(before.length, after.length);
2002
+ while (head < most && before.charCodeAt(head) === after.charCodeAt(head)) {
2003
+ head++;
2004
+ }
2005
+ let tail = 0;
2006
+ while (
2007
+ tail < most - head &&
2008
+ before.charCodeAt(before.length - 1 - tail) ===
2009
+ after.charCodeAt(after.length - 1 - tail)
2010
+ ) {
2011
+ tail++;
2012
+ }
2013
+ const shift = lines.length - removed;
2014
+ const grow = after.length - before.length;
2015
+ // where each old line starts inside `before`
2016
+ const starts: number[] = [];
2017
+ for (let i = 0, at = 0; i < removed; i++) {
2018
+ starts.push(at);
2019
+ at += this._lines[from + i].length + 1;
2020
+ }
2021
+ const move = (p: Position, far: boolean): Position => {
2022
+ if (p.line < from) return p;
2023
+ if (p.line >= from + removed) return { line: p.line + shift, ch: p.ch };
2024
+ const text = this._lines[p.line];
2025
+ const o = starts[p.line - from] + Math.min(p.ch, text.length);
2026
+ let n: number;
2027
+ if (o < head) n = o;
2028
+ else if (o > before.length - tail) n = o + grow;
2029
+ else n = far ? after.length - tail : head;
2030
+ // back to a line and column of the new lines
2031
+ let line = 0;
2032
+ let at = 0;
2033
+ while (line < lines.length - 1 && n > at + lines[line].length) {
2034
+ at += lines[line].length + 1;
2035
+ line++;
2036
+ }
2037
+ return { line: from + line, ch: n - at };
2038
+ };
2039
+ const kept: Diagnostic[] = [];
2040
+ for (const d of list) {
2041
+ const a = minPos(d.from, d.to);
2042
+ const b = maxPos(d.from, d.to);
2043
+ const point = posEqual(a, b);
2044
+ const start = move(a, !point);
2045
+ const end = move(b, false);
2046
+ if (comparePos(end, start) < 0 || (!point && posEqual(start, end))) {
2047
+ if (!point) continue; // what it pointed at is gone
2048
+ }
2049
+ kept.push({
2050
+ ...d,
2051
+ from: start,
2052
+ to: comparePos(end, start) < 0 ? start : end,
2053
+ });
2054
+ }
2055
+ this._diagnostics = kept;
2056
+ }
2057
+
916
2058
  get canRedo(): boolean {
917
- return this._historyIndex < this._history.length - 1;
2059
+ return this._historyIndex < this._history.length;
918
2060
  }
919
2061
 
920
2062
  undo(): void {
921
2063
  this._syncProps();
922
2064
  if (!this.canUndo) return;
923
- this._historyIndex--;
924
- this._applyHistory(this._history[this._historyIndex]);
2065
+ // the selection the change found, so an undo lands where the change
2066
+ // was made — not where the one before it left the caret, which for the
2067
+ // first change of a session was the top of the file
2068
+ const step = this._history[--this._historyIndex];
2069
+ this._applyStep(step.from, step.after.length, step.before, {
2070
+ caret: step.caretBefore,
2071
+ anchor: step.anchorBefore,
2072
+ });
925
2073
  }
926
2074
 
927
2075
  redo(): void {
928
2076
  this._syncProps();
929
2077
  if (!this.canRedo) return;
930
- this._historyIndex++;
931
- this._applyHistory(this._history[this._historyIndex]);
2078
+ const step = this._history[this._historyIndex++];
2079
+ this._applyStep(step.from, step.before.length, step.after, step);
932
2080
  }
933
2081
 
934
- private _applyHistory(entry: HistoryEntry): void {
2082
+ private _applyStep(
2083
+ from: number,
2084
+ removed: number,
2085
+ lines: readonly string[],
2086
+ selection: { caret: Position; anchor: Position },
2087
+ ): void {
2088
+ const view = this._viewBefore();
935
2089
  this._undoRun = null;
936
2090
  this._goalX = null;
937
- this._applyLines(entry.value.split('\n'), null);
938
- this._caret = clampPos(this._lines, entry.caret);
939
- this._anchor = clampPos(this._lines, entry.anchor);
940
- this._afterEdit(entry.value);
2091
+ this._replaceLines(from, removed, lines);
2092
+ this._caret = clampPos(this._lines, selection.caret);
2093
+ this._anchor = clampPos(this._lines, selection.anchor);
2094
+ this._afterEdit(view);
941
2095
  }
942
2096
 
943
2097
  // --- editing core --------------------------------------------------------
@@ -978,6 +2132,7 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
978
2132
  ): void {
979
2133
  this._syncProps();
980
2134
  if (this.props.readOnly) return;
2135
+ const view = this._viewBefore();
981
2136
  const a = clampPos(this._lines, minPos(from, to));
982
2137
  const b = clampPos(this._lines, maxPos(from, to));
983
2138
  const insert = String(text).replace(/\r\n?/g, '\n');
@@ -987,77 +2142,145 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
987
2142
  inserted[0] = before + inserted[0];
988
2143
  const caretCh = inserted[inserted.length - 1].length;
989
2144
  inserted[inserted.length - 1] += after;
990
- const newLines = this._lines.slice();
991
- newLines.splice(a.line, b.line - a.line + 1, ...inserted);
992
-
993
- const coalesce =
994
- run !== null &&
995
- run === this._undoRun &&
996
- this._historyIndex === this._history.length - 1;
997
- this._applyLines(newLines, {
998
- fromLine: a.line,
999
- removed: b.line - a.line + 1,
1000
- inserted: inserted.length,
1001
- });
1002
- this._caret = clampPos(this._lines, {
1003
- line: a.line + inserted.length - 1,
1004
- ch: caretCh,
1005
- });
1006
- this._anchor = { ...this._caret };
2145
+ const caret = { line: a.line + inserted.length - 1, ch: caretCh };
2146
+ this._edit(a.line, b.line - a.line + 1, inserted, run, caret, { ...caret });
2147
+ this._caret = caret;
2148
+ this._anchor = { ...caret };
1007
2149
  this._goalX = null;
2150
+ this._afterEdit(view);
2151
+ }
1008
2152
 
1009
- const value = this._lines.join('\n');
1010
- if (coalesce) {
1011
- this._history[this._historyIndex] = {
1012
- value,
1013
- caret: this._caret,
1014
- anchor: this._anchor,
1015
- };
1016
- } else {
1017
- this._pushHistory({ value, caret: this._caret, anchor: this._anchor });
2153
+ /**
2154
+ * The one way the lines change: `removed` lines at `from` become `lines`,
2155
+ * recorded for undo under `run` with the selection the change leaves.
2156
+ * Narrowed first to the lines it really changes — a replacement that
2157
+ * leaves lines at either end as they were, a select-all and paste of the
2158
+ * same file or a formatter's output, touches only the ones between — and
2159
+ * a change that changes nothing is not one.
2160
+ */
2161
+ private _edit(
2162
+ from: number,
2163
+ removed: number,
2164
+ lines: readonly string[],
2165
+ run: string | null,
2166
+ caret: Position,
2167
+ anchor: Position,
2168
+ ): void {
2169
+ const text = this._lines;
2170
+ let head = 0;
2171
+ while (
2172
+ head < removed &&
2173
+ head < lines.length &&
2174
+ text[from + head] === lines[head]
2175
+ ) {
2176
+ head++;
2177
+ }
2178
+ let tail = 0;
2179
+ while (
2180
+ tail < removed - head &&
2181
+ tail < lines.length - head &&
2182
+ text[from + removed - 1 - tail] === lines[lines.length - 1 - tail]
2183
+ ) {
2184
+ tail++;
2185
+ }
2186
+ const gone = removed - head - tail;
2187
+ const added = lines.slice(head, lines.length - tail);
2188
+ if (gone > 0 || added.length > 0) {
2189
+ this._record(from + head, gone, added, run, caret, anchor);
2190
+ this._replaceLines(from + head, gone, added);
1018
2191
  }
1019
2192
  this._undoRun = run;
1020
- this._afterEdit(value);
1021
2193
  }
1022
2194
 
1023
- private _applyLines(
1024
- newLines: string[],
1025
- edit: { fromLine: number; removed: number; inserted: number } | null,
2195
+ /**
2196
+ * `removed` lines at `from` become `lines`, in the live array. A
2197
+ * tokenizer keeps the array `setLines` handed it (that is the contract in
2198
+ * `Tokenizer`) and is told about changes through `edit()` alone, so an
2199
+ * edit has to *mutate* it — swapping in a fresh one would leave every
2200
+ * engine tokenizing the pre-edit text at the new line numbers, painting a
2201
+ * split line with its neighbour's colours.
2202
+ */
2203
+ private _replaceLines(
2204
+ from: number,
2205
+ removed: number,
2206
+ lines: readonly string[],
1026
2207
  ): void {
1027
- if (edit) {
1028
- // A tokenizer keeps the array `setLines` handed it (that is the
1029
- // contract in `Tokenizer`) and is told about changes through `edit()`
1030
- // alone, so an incremental edit has to *mutate* the live array —
1031
- // swapping in a fresh one would leave every engine tokenizing the
1032
- // pre-edit text at the new line numbers, painting a split line with
1033
- // its neighbour's colours. Callers only touch the edited span, so
1034
- // splicing that span across reproduces `newLines` exactly.
1035
- this._lines.splice(
1036
- edit.fromLine,
1037
- edit.removed,
1038
- ...newLines.slice(edit.fromLine, edit.fromLine + edit.inserted),
1039
- );
1040
- } else {
1041
- this._lines = newLines;
1042
- }
1043
- const tok = this._tokenizer();
1044
- if (edit && tok) {
1045
- tok.edit(edit);
1046
- // line indices at and below the edit moved or changed; drop them
1047
- for (const key of [...this._lineCache.keys()]) {
1048
- if (key >= edit.fromLine) this._lineCache.delete(key);
2208
+ this._mapDiagnostics(from, removed, lines);
2209
+ // the widest line known going: what it was is the widest of the lines
2210
+ // still laid out — the one edited is laid out again before it is drawn
2211
+ let widestGoes = false;
2212
+ for (let i = from; i < from + removed && !widestGoes; i++) {
2213
+ const width = this._lineCache.get(i)?.layout?.width ?? 0;
2214
+ widestGoes = width > 0 && width >= this._widest;
2215
+ }
2216
+ spliceAll(this._lines, from, removed, lines);
2217
+ const edit = { fromLine: from, removed, inserted: lines.length };
2218
+ this._tokenizer()?.edit(edit);
2219
+ this._moveLineCache(edit);
2220
+ this._lastEdit = edit;
2221
+ if (widestGoes) {
2222
+ let widest = 0;
2223
+ for (const entry of this._lineCache.values()) {
2224
+ widest = Math.max(widest, entry.layout?.width ?? 0);
2225
+ }
2226
+ this._widest = widest;
2227
+ }
2228
+ }
2229
+
2230
+ /** Let go of the line entries farther than a quarter of the cache from
2231
+ * the lines in view — see `LINE_CACHE_MAX`. */
2232
+ private _trimLineCache(first: number, last: number): void {
2233
+ const reach = LINE_CACHE_MAX / 4;
2234
+ for (const line of this._lineCache.keys()) {
2235
+ if (line < first - reach || line > last + reach) {
2236
+ this._lineCache.delete(line);
2237
+ }
2238
+ }
2239
+ }
2240
+
2241
+ /**
2242
+ * The line cache across an edit: the edited lines' entries go, and the
2243
+ * ones after them move by the lines it added or took away. Every other
2244
+ * entry stays. An entry is checked against its line's text and tokens
2245
+ * whenever it is asked for (`_lineEntry`), so a line an edit re-coloured
2246
+ * — an opened comment, a closed string — is laid out again then, and a
2247
+ * line it left alone is not: typing on the first line of a file used to
2248
+ * lay out every line in view again on every keystroke.
2249
+ */
2250
+ private _moveLineCache(edit: {
2251
+ fromLine: number;
2252
+ removed: number;
2253
+ inserted: number;
2254
+ }): void {
2255
+ const end = edit.fromLine + edit.removed;
2256
+ const shift = edit.inserted - edit.removed;
2257
+ if (shift === 0) {
2258
+ for (let line = edit.fromLine; line < end; line++) {
2259
+ this._dropLine(line);
2260
+ }
2261
+ return;
2262
+ }
2263
+ const moved = new Map<number, LineCacheEntry>();
2264
+ for (const [line, entry] of this._lineCache) {
2265
+ if (line < edit.fromLine) moved.set(line, entry);
2266
+ else if (line >= end) moved.set(line + shift, entry);
2267
+ else if (entry.layout instanceof ChunkedLayout) {
2268
+ this._chunkedStash = { styleKey: entry.styleKey, layout: entry.layout };
1049
2269
  }
1050
- if (edit.removed !== edit.inserted) this._lineCache.clear();
1051
- } else {
1052
- tok?.setLines(this._lines);
1053
- this._lineCache.clear();
1054
2270
  }
2271
+ this._lineCache = moved;
1055
2272
  }
1056
2273
 
1057
- private _afterEdit(value: string): void {
1058
- this._synced = value;
2274
+ private _afterEdit(before: ViewBefore): void {
2275
+ // The whole text as one string is what `onChange`, `onSelectionChange`
2276
+ // and a controlled `value` speak, and joining fifty thousand lines is a
2277
+ // millisecond or two a keystroke — joined when one of them reads it.
2278
+ let joined: string | null = null;
2279
+ const value = (): string => (joined ??= this._lines.join('\n'));
2280
+ this._synced = this.props.value != null ? value() : null;
1059
2281
  this._ensureCaretVisible();
1060
- this._repaint();
2282
+ this._repaintSince(before, this._lastEdit);
2283
+ this._lastEdit = null;
1061
2284
  this._fire('onChange', value);
1062
2285
  this._fire('onSelectionChange', value);
1063
2286
  }
@@ -1070,21 +2293,23 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
1070
2293
  moveCaret(pos: Position, extend: boolean): void {
1071
2294
  this._syncProps();
1072
2295
  this.breakUndoRun();
2296
+ const view = this._viewBefore();
1073
2297
  this._caret = clampPos(this._lines, pos);
1074
2298
  if (!extend) this._anchor = { ...this._caret };
1075
2299
  this._ensureCaretVisible();
1076
- this._repaint();
1077
- this._fire('onSelectionChange', this.value);
2300
+ this._repaintSince(view, null);
2301
+ this._fire('onSelectionChange', () => this.value);
1078
2302
  }
1079
2303
 
1080
2304
  select(anchor: Position, head: Position): void {
1081
2305
  this._syncProps();
1082
2306
  this.breakUndoRun();
2307
+ const view = this._viewBefore();
1083
2308
  this._anchor = clampPos(this._lines, anchor);
1084
2309
  this._caret = clampPos(this._lines, head);
1085
2310
  this._ensureCaretVisible();
1086
- this._repaint();
1087
- this._fire('onSelectionChange', this.value);
2311
+ this._repaintSince(view, null);
2312
+ this._fire('onSelectionChange', () => this.value);
1088
2313
  }
1089
2314
 
1090
2315
  // `: this`, matching the base declaration core grew in #294 — a subclass
@@ -1199,19 +2424,20 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
1199
2424
  fromLine: number,
1200
2425
  toLine: number,
1201
2426
  ): void {
1202
- const caret = this._caret;
1203
- const anchor = this._anchor;
1204
- this._applyLines(newLines, {
2427
+ const view = this._viewBefore();
2428
+ const caret = clampPos(newLines, this._caret);
2429
+ const anchor = clampPos(newLines, this._anchor);
2430
+ this._edit(
1205
2431
  fromLine,
1206
- removed: toLine - fromLine + 1,
1207
- inserted: toLine - fromLine + 1,
1208
- });
1209
- this._caret = clampPos(this._lines, caret);
1210
- this._anchor = clampPos(this._lines, anchor);
1211
- this._undoRun = null;
1212
- const value = this._lines.join('\n');
1213
- this._pushHistory({ value, caret: this._caret, anchor: this._anchor });
1214
- this._afterEdit(value);
2432
+ toLine - fromLine + 1,
2433
+ newLines.slice(fromLine, toLine + 1),
2434
+ null,
2435
+ caret,
2436
+ anchor,
2437
+ );
2438
+ this._caret = caret;
2439
+ this._anchor = anchor;
2440
+ this._afterEdit(view);
1215
2441
  }
1216
2442
 
1217
2443
  // --- input: keyboard -----------------------------------------------------
@@ -1464,8 +2690,11 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
1464
2690
  const entry = this._lineEntry(line);
1465
2691
  let ch = 0;
1466
2692
  if (entry.layout) {
1467
- const cp = entry.layout.indexAt(this._goalX, 0);
1468
- ch = entry.map.toRaw(cpToUtf16(entry.display, cp));
2693
+ ch = entry.map.toRaw(
2694
+ entry.layout instanceof ChunkedLayout
2695
+ ? entry.layout.indexAtUtf16(this._goalX, 0)
2696
+ : cpToUtf16(entry.display, entry.layout.indexAt(this._goalX, 0)),
2697
+ );
1469
2698
  }
1470
2699
  const goal = this._goalX; // moveCaret clears it; this move keeps it
1471
2700
  this.moveCaret({ line, ch }, extend);
@@ -1604,12 +2833,37 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
1604
2833
  if (this._blinkTimer == null) {
1605
2834
  this._blinkTimer = startInterval(() => {
1606
2835
  this._caretOn = !this._caretOn;
1607
- this.root?.invalidate(false, this.abs, 'text');
2836
+ this._claimCaretRow();
1608
2837
  }, CARET_BLINK_MS);
1609
2838
  }
1610
2839
  this._repaint();
1611
2840
  }
1612
2841
 
2842
+ /** A blink changes the caret and nothing else: claim the text row it
2843
+ * is on rather than the editor, and `paint` draws that one line. The
2844
+ * whole row, so a caret x worked out a pixel off still falls inside. */
2845
+ private _claimCaretRow(): void {
2846
+ const content = this._contentRect();
2847
+ const gutterW = this._gutterWidth();
2848
+ const top = content.y + this._caret.line * this._lineH - this._scrollY;
2849
+ const y = Math.max(content.y, Math.floor(top));
2850
+ const bottom = Math.min(
2851
+ content.y + content.height,
2852
+ Math.ceil(top + this._lineH),
2853
+ );
2854
+ if (bottom <= y) return; // scrolled out of view
2855
+ this.invalidate(
2856
+ false,
2857
+ {
2858
+ x: content.x + gutterW,
2859
+ y,
2860
+ width: Math.max(0, content.width - gutterW),
2861
+ height: bottom - y,
2862
+ },
2863
+ 'text',
2864
+ );
2865
+ }
2866
+
1613
2867
  handleBlur(): void {
1614
2868
  this._focused = false;
1615
2869
  this.breakUndoRun();
@@ -1625,21 +2879,43 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
1625
2879
  prevProps: Record<string, unknown>,
1626
2880
  ): void {
1627
2881
  const before = prevProps ?? this.props;
2882
+ // the squiggles on screen, before the prop they came from is replaced
2883
+ const shown = this._diagnostics;
1628
2884
  super.applyProps(nextProps, prevProps);
1629
- if (nextProps.value != null) this._syncProps();
2885
+ if (nextProps.value != null && String(nextProps.value) !== this._synced) {
2886
+ // the rows the new value changed, as an edit claims them
2887
+ const view = this._viewBefore();
2888
+ this._syncProps();
2889
+ this._repaintSince(view, this._lastEdit);
2890
+ this._lastEdit = null;
2891
+ }
1630
2892
  if (nextProps.language !== before.language) {
1631
2893
  this._tokenizer(); // re-resolves against the new props
1632
2894
  this._repaint();
1633
2895
  }
1634
2896
  if (nextProps.rows !== before.rows) this.invalidateMeasure('props');
1635
2897
  if (
1636
- nextProps.tokenStyles !== before.tokenStyles ||
1637
- nextProps.diagnostics !== before.diagnostics ||
1638
- nextProps.tabSize !== before.tabSize ||
1639
- nextProps.lineNumbers !== before.lineNumbers
2898
+ !sameTokenStyles(
2899
+ nextProps.tokenStyles as TokenStyles | undefined,
2900
+ before.tokenStyles as TokenStyles | undefined,
2901
+ ) ||
2902
+ nextProps.tabSize !== before.tabSize
1640
2903
  ) {
1641
2904
  this._lineCache.clear();
2905
+ this._chunkedStash = null;
1642
2906
  this._repaint();
2907
+ } else if (nextProps.lineNumbers !== before.lineNumbers) {
2908
+ this._repaint(); // every column moves with the gutter; no layout does
2909
+ } else if (
2910
+ !sameDiagnostics(
2911
+ nextProps.diagnostics as readonly Diagnostic[] | undefined,
2912
+ before.diagnostics as readonly Diagnostic[] | undefined,
2913
+ )
2914
+ ) {
2915
+ this._repaintDiagnostics(
2916
+ shown,
2917
+ (nextProps.diagnostics as readonly Diagnostic[] | undefined) ?? [],
2918
+ );
1643
2919
  }
1644
2920
  }
1645
2921
 
@@ -1670,6 +2946,7 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
1670
2946
  stroke?(): void;
1671
2947
  strokeStyle?: unknown;
1672
2948
  lineWidth?: number;
2949
+ lineJoin?: string;
1673
2950
  };
1674
2951
  if (typeof c.fillRect !== 'function') return; // mock backend: geometry only
1675
2952
  const content = this._contentRect();
@@ -1698,13 +2975,49 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
1698
2975
  return true;
1699
2976
  };
1700
2977
  const dark = this._onDark();
2978
+ // What of a line this pass can show, in the line's own x: a band, a
2979
+ // squiggle or a caret is drawn over that and no further. A selection
2980
+ // across a minified line was one rectangle as wide as the line, and past
2981
+ // 32,767 device pixels an X11 coordinate does not fit its 16 bits — the
2982
+ // paint threw, and the window stopped painting.
2983
+ const reachFrom = this._scrollX - 8 * s;
2984
+ const reachTo = this._scrollX + content.width - gutterW + 8 * s;
2985
+ const within = (x0: number, x1: number): [number, number] | null => {
2986
+ const a = Math.max(x0, reachFrom);
2987
+ const b = Math.min(x1, reachTo);
2988
+ return b > a ? [a, b] : null;
2989
+ };
1701
2990
 
1702
- const first = Math.max(0, Math.floor(this._scrollY / lineH));
1703
- const last = Math.min(
2991
+ let first = Math.max(0, Math.floor(this._scrollY / lineH));
2992
+ let last = Math.min(
1704
2993
  this._lines.length - 1,
1705
2994
  Math.ceil((this._scrollY + content.height) / lineH),
1706
2995
  );
2996
+ if (this._lineCache.size > LINE_CACHE_MAX) this._trimLineCache(first, last);
2997
+ // Only the lines this pass repaints, and one either side for ink that
2998
+ // overhangs its row: a scroll blit hands over the band it exposed, and
2999
+ // a blink the caret's row. The clip would throw the rest away after
3000
+ // they were laid out and drawn.
3001
+ const damage = this.paintDamage();
3002
+ if (damage) {
3003
+ const from = damage.y - content.y + this._scrollY;
3004
+ first = Math.max(first, Math.floor(from / lineH) - 1);
3005
+ last = Math.min(last, Math.ceil((from + damage.height) / lineH) + 1);
3006
+ }
1707
3007
  const lineY = (i: number): number => content.y + i * lineH - this._scrollY;
3008
+ // A fill as tall as a line, on whole device pixels. A line is as tall as
3009
+ // its face says, a fraction of a pixel, and a backend rounds a fill's
3010
+ // fractional edges its own way: X11 cut a band's top and its height
3011
+ // separately, which left a row of background between two selected lines
3012
+ // every seventh or eighth line — at a row that moved with the scroll, so
3013
+ // a blit copied a seam a repaint did not draw. Edges rounded from the
3014
+ // same arithmetic meet whichever line they are shared by.
3015
+ const fillRow = (i: number, x0: number, x1: number): void => {
3016
+ const left = Math.round(x0);
3017
+ const top = Math.round(lineY(i));
3018
+ const bottom = Math.round(lineY(i + 1));
3019
+ c.fillRect!(left, top, Math.round(x1) - left, bottom - top);
3020
+ };
1708
3021
 
1709
3022
  // Everything the editor draws — gutter included — stays inside the
1710
3023
  // content box. The first and last visible rows are partial when
@@ -1743,12 +3056,7 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
1743
3056
  c.fillStyle =
1744
3057
  this._resolveColor(props.activeLineColor) ??
1745
3058
  (dark ? 'rgba(255,255,255,0.06)' : 'rgba(120,140,180,0.10)');
1746
- c.fillRect(
1747
- content.x + gutterW,
1748
- lineY(this._caret.line),
1749
- content.width - gutterW,
1750
- lineH,
1751
- );
3059
+ fillRow(this._caret.line, content.x + gutterW, content.x + content.width);
1752
3060
  }
1753
3061
 
1754
3062
  // selection bands
@@ -1767,12 +3075,8 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
1767
3075
  i === b.line
1768
3076
  ? this._caretX({ line: i, ch: b.ch })
1769
3077
  : (entry.layout?.width ?? 0) + this._charW * 0.5; // the newline
1770
- c.fillRect(
1771
- textX + startX,
1772
- lineY(i),
1773
- Math.max(2 * s, endX - startX),
1774
- lineH,
1775
- );
3078
+ const band = within(startX, startX + Math.max(2 * s, endX - startX));
3079
+ if (band) fillRow(i, textX + band[0], textX + band[1]);
1776
3080
  }
1777
3081
  }
1778
3082
 
@@ -1787,7 +3091,8 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
1787
3091
  if (pos.line < first || pos.line > last) continue;
1788
3092
  const x0 = this._caretX(pos);
1789
3093
  const x1 = this._caretX({ line: pos.line, ch: pos.ch + 1 });
1790
- c.fillRect(textX + x0, lineY(pos.line), Math.max(s, x1 - x0), lineH);
3094
+ const box = within(x0, x0 + Math.max(s, x1 - x0));
3095
+ if (box) fillRow(pos.line, textX + box[0], textX + box[1]);
1791
3096
  }
1792
3097
  }
1793
3098
  }
@@ -1815,18 +3120,63 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
1815
3120
  layout.draw(ctx, textX, lineY(0) + (lineH - layout.height) / 2);
1816
3121
  }
1817
3122
  } else {
1818
- for (let i = first; i <= last; i++) {
1819
- const entry = this._lineEntry(i);
1820
- entry.layout?.draw(
1821
- ctx,
1822
- textX,
1823
- lineY(i) + (lineH - (entry.layout.height || lineH)) / 2,
3123
+ // across, what the text region shows of a line, in its own x — and
3124
+ // of that, what this pass repaints, give or take a character of ink
3125
+ // past a glyph's advance: a thumb's strip is a column at the right
3126
+ // edge that most lines never reach
3127
+ let shownFrom = this._scrollX;
3128
+ let shownTo = this._scrollX + content.width - gutterW;
3129
+ if (damage) {
3130
+ shownFrom = Math.max(shownFrom, damage.x - textX - this._charW);
3131
+ shownTo = Math.min(
3132
+ shownTo,
3133
+ damage.x + damage.width - textX + this._charW,
1824
3134
  );
1825
3135
  }
3136
+ for (let i = first; shownFrom < shownTo && i <= last; i++) {
3137
+ const entry = this._lineEntry(i);
3138
+ const layout = entry.layout;
3139
+ if (!layout) continue;
3140
+ // a long line's width past what is laid out is a guess, and one
3141
+ // short of the truth would skip text that is there: its pieces are
3142
+ // placed by laying out the ones before them (`drawSpan`)
3143
+ if (!(layout instanceof ChunkedLayout) && layout.width < shownFrom) {
3144
+ continue;
3145
+ }
3146
+ // Every row's text on the base face's baseline. Centred by its own
3147
+ // height, a line with an emoji in it — laid out taller, in a
3148
+ // fallback face — sat a pixel off every other line.
3149
+ const top = lineY(i);
3150
+ const base = this._baseline;
3151
+ const own = base == null ? null : baselineOf(layout);
3152
+ const y =
3153
+ own == null
3154
+ ? top + (lineH - (layout.height || lineH)) / 2
3155
+ : top + base! - own;
3156
+ // …and a row laid out taller than the base keeps its ink in its
3157
+ // own row: past it, the next row's pixels are ones an edit to this
3158
+ // row does not repaint, and an old descender stayed there
3159
+ const tall =
3160
+ layout.height > lineH + 0.5 &&
3161
+ clipTo(content.x + gutterW, top, content.width - gutterW, lineH);
3162
+ if (layout instanceof ChunkedLayout) {
3163
+ layout.drawSpan(
3164
+ ctx,
3165
+ textX,
3166
+ y,
3167
+ shownFrom,
3168
+ shownTo,
3169
+ base == null ? null : top + base,
3170
+ );
3171
+ } else {
3172
+ layout.draw(ctx, textX, y);
3173
+ }
3174
+ if (tall) c.restore!();
3175
+ }
1826
3176
  }
1827
3177
 
1828
3178
  // diagnostics
1829
- const diagnostics = props.diagnostics ?? [];
3179
+ const diagnostics = this._currentDiagnostics();
1830
3180
  for (const d of diagnostics) {
1831
3181
  const color = SEVERITY_COLORS[d.severity ?? 'error'] ?? '#e5484d';
1832
3182
  const from = clampPos(this._lines, d.from);
@@ -1847,10 +3197,16 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
1847
3197
  // short text — measure where the drawn layout actually ends
1848
3198
  const entry = this._lineEntry(i);
1849
3199
  const glyphBottom =
1850
- lineY(i) +
1851
- (lineH - (entry.layout?.height || lineH)) / 2 +
1852
- (entry.layout?.height || lineH);
1853
- const y = Math.min(lineY(i) + lineH - 2 * s, glyphBottom - s);
3200
+ this._baseline != null
3201
+ ? lineY(i) + this._textBottom
3202
+ : lineY(i) +
3203
+ (lineH - (entry.layout?.height || lineH)) / 2 +
3204
+ (entry.layout?.height || lineH);
3205
+ // …and inside its own row: the zigzag is 2px tall and stroked 1px
3206
+ // wide, so it inks half a stroke past its path either way. Ink past
3207
+ // the row's bottom is in the next row's pixels, which an edit to
3208
+ // this row does not repaint — the old squiggle left a speck there.
3209
+ const y = Math.min(lineY(i) + lineH - 2.5 * s, glyphBottom - s);
1854
3210
  const width = Math.max(this._charW * 0.6, x1 - x0);
1855
3211
  if (
1856
3212
  typeof c.beginPath === 'function' &&
@@ -1858,30 +3214,44 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
1858
3214
  typeof c.lineTo === 'function' &&
1859
3215
  typeof c.stroke === 'function'
1860
3216
  ) {
1861
- c.beginPath();
1862
3217
  // a hairline zig-zag, 4px a step and 2px tall, on the panel's grid
3218
+ // — the steps in reach, each where it would be if all were drawn
1863
3219
  const step = 4 * s;
1864
- let up = true;
1865
- for (let x = 0; x <= width; x += step) {
1866
- const px = textX + x0 + x;
1867
- const py = y + (up ? 0 : 2 * s);
1868
- if (x === 0) c.moveTo(px, py);
1869
- else c.lineTo(px, py);
1870
- up = !up;
3220
+ const from = Math.max(0, Math.floor((reachFrom - x0) / step));
3221
+ const to = Math.min(
3222
+ Math.floor(width / step),
3223
+ Math.ceil((reachTo - x0) / step),
3224
+ );
3225
+ if (to > from) {
3226
+ c.beginPath();
3227
+ for (let k = from; k <= to; k++) {
3228
+ const px = textX + x0 + k * step;
3229
+ const py = y + (k % 2 === 0 ? 0 : 2 * s);
3230
+ if (k === from) c.moveTo(px, py);
3231
+ else c.lineTo(px, py);
3232
+ }
3233
+ c.strokeStyle = color;
3234
+ c.lineWidth = s;
3235
+ // mitred, the zig-zag's corners reach half a pixel past the
3236
+ // stroke — into the next row, at the bottom ones
3237
+ c.lineJoin = 'round';
3238
+ c.stroke();
1871
3239
  }
1872
- c.strokeStyle = color;
1873
- c.lineWidth = s;
1874
- c.stroke();
1875
3240
  } else {
1876
- c.fillStyle = color;
1877
- c.fillRect(textX + x0, y + s, width, s);
3241
+ const line = within(x0, x0 + width);
3242
+ if (line) {
3243
+ c.fillStyle = color;
3244
+ c.fillRect(textX + line[0], y + s, line[1] - line[0], s);
3245
+ }
1878
3246
  }
1879
3247
  }
1880
3248
  }
1881
3249
 
1882
3250
  // caret — 2 logical px, in the (theme-resolved) text colour
1883
- if (this._focused && this._caretOn) {
1884
- const x = this._caretX(this._caret);
3251
+ const caretAt =
3252
+ this._focused && this._caretOn ? this._caretX(this._caret) : 0;
3253
+ if (this._focused && this._caretOn && within(caretAt, caretAt + 2 * s)) {
3254
+ const x = caretAt;
1885
3255
  c.fillStyle = this._resolveColor(props.caretColor) ?? baseColor;
1886
3256
  c.fillRect(
1887
3257
  Math.round(textX + x),
@@ -1893,8 +3263,9 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
1893
3263
 
1894
3264
  if (textClip) c.restore!();
1895
3265
 
1896
- // gutter numbers: inside the content clip, outside the text clip
1897
- if (gutterW > 0) {
3266
+ // gutter numbers: inside the content clip, outside the text clip — and
3267
+ // not for a pass that lies to the right of them
3268
+ if (gutterW > 0 && !(damage && damage.x >= content.x + gutterW)) {
1898
3269
  const fonts = this._fonts();
1899
3270
  if (fonts) {
1900
3271
  const dim =
@@ -1956,6 +3327,25 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
1956
3327
  3 * s,
1957
3328
  );
1958
3329
  }
3330
+ // A line this pass laid out wider than the view gives the text a
3331
+ // horizontal thumb, where there was none when the scroll decided what
3332
+ // to blit: the frame repaints the band it exposed and not the strip
3333
+ // the thumb is drawn in, so the strip is claimed for the next one.
3334
+ if (maxX > 0 !== this._thumbAcross) {
3335
+ this._thumbAcross = maxX > 0;
3336
+ if (damage) {
3337
+ this.invalidate(
3338
+ false,
3339
+ {
3340
+ x: content.x,
3341
+ y: content.y + content.height - 4 * s,
3342
+ width: content.width,
3343
+ height: 4 * s,
3344
+ },
3345
+ 'scroll',
3346
+ );
3347
+ }
3348
+ }
1959
3349
 
1960
3350
  if (outerClip) c.restore!();
1961
3351
  }