@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
@@ -35,6 +35,7 @@
35
35
  import { CARET_BLINK_MS, Node } from 'react-x11/node';
36
36
  import { ctrlChordLetter, XK_BACKSPACE, XK_DELETE, XK_DOWN, XK_END, XK_ESCAPE, XK_HOME, XK_KP_ENTER, XK_LEFT, XK_PAGE_DOWN, XK_PAGE_UP, XK_RETURN, XK_RIGHT, XK_TAB, XK_UP, } from 'react-x11/keysyms';
37
37
  import { startInterval, stopInterval } from '../code-language/timers.js';
38
+ import { spliceAll } from '../internal/splice.js';
38
39
  import { clampPos, comparePos, maxPos, minPos, movePos, posEqual, tabMap, wordBoundary, wordRangeAt, } from './doc.js';
39
40
  import { DARK_TOKEN_STYLES, isDarkBackground, LIGHT_TOKEN_STYLES, tokenStyleFor, } from '../code-language/theme.js';
40
41
  /**
@@ -69,6 +70,54 @@ const SEVERITY_COLORS = {
69
70
  information: '#3b82f6',
70
71
  hint: '#8b8f98',
71
72
  };
73
+ /** Two token style maps that colour alike — the same object, or an
74
+ * inline one written again with the same entries, which a render hands
75
+ * over every time and which laid every line out again. */
76
+ function sameTokenStyles(a, b) {
77
+ if (a === b)
78
+ return true;
79
+ if (!a || !b)
80
+ return false;
81
+ const ka = Object.keys(a);
82
+ if (ka.length !== Object.keys(b).length)
83
+ return false;
84
+ for (const k of ka) {
85
+ const x = a[k];
86
+ const y = b[k];
87
+ if (x === y)
88
+ continue;
89
+ if (!x || !y)
90
+ return false;
91
+ const fx = Object.keys(x);
92
+ if (fx.length !== Object.keys(y).length)
93
+ return false;
94
+ for (const f of fx)
95
+ if (x[f] !== y[f])
96
+ return false;
97
+ }
98
+ return true;
99
+ }
100
+ /** Two diagnostics lists that say the same thing, in the same order. */
101
+ function sameDiagnostics(a, b) {
102
+ if (a === b)
103
+ return true;
104
+ if ((a?.length ?? 0) !== (b?.length ?? 0))
105
+ return false;
106
+ if (!a || !b)
107
+ return true; // both empty
108
+ for (let i = 0; i < a.length; i++) {
109
+ const x = a[i];
110
+ const y = b[i];
111
+ if (!posEqual(x.from, y.from) ||
112
+ !posEqual(x.to, y.to) ||
113
+ x.severity !== y.severity ||
114
+ x.message !== y.message ||
115
+ x.source !== y.source) {
116
+ return false;
117
+ }
118
+ }
119
+ return true;
120
+ }
72
121
  function cpToUtf16(text, cp) {
73
122
  let i = 0;
74
123
  for (let n = 0; n < cp && i < text.length; n++) {
@@ -86,10 +135,268 @@ function utf16ToCp(text, index) {
86
135
  }
87
136
  return cp;
88
137
  }
138
+ /** A line's tokens with no language. One array, so that a cached line
139
+ * entry, which is checked against its tokens by identity, is found again —
140
+ * a fresh `[]` per lookup made every paint of a plain-text editor lay out
141
+ * every line in view. */
142
+ const NO_TOKENS = Object.freeze([]);
143
+ /** Line entries past which the ones farthest from the view are let go. An
144
+ * entry holds a text layout, native memory on every backend, and a file
145
+ * scrolled end to end had one for every line in it. */
146
+ const LINE_CACHE_MAX = 2048;
147
+ /** Display code units past which a line is laid out in pieces
148
+ * (`ChunkedLayout`). */
149
+ const CHUNKED_PAST = 2048;
150
+ /** A piece is at least this long, and is cut here at the latest. */
151
+ const CHUNK_MIN = 256;
152
+ const CHUNK_MAX = 2048;
153
+ /**
154
+ * A line too long to lay out whole, laid out in pieces placed end to end.
155
+ *
156
+ * A minified file or a log line of a hundred thousand characters is one
157
+ * text layout otherwise, and everything the editor asks of a layout is
158
+ * linear in its length or worse: putting the caret at the end of a
159
+ * 100,000-character line took 4.3 s in CoreText's caret lookup, every
160
+ * keystroke shaped the whole line again, and on X11 a line past 32,767
161
+ * device pixels threw out of the paint. A piece answers in a layout of its own size, only the
162
+ * pieces a keystroke changed are shaped again (`_chunkLayouts`, keyed by
163
+ * what a piece holds — which is why the cuts are made by content,
164
+ * `chunkBreaks`), and only the pieces in view are drawn (`drawSpan`).
165
+ *
166
+ * What it gives up is shaping across the seams: a kerning pair or a
167
+ * ligature that straddles one is set as two. Code is set in a monospace
168
+ * face, and a seam is one place in a few hundred characters.
169
+ */
170
+ /** A layout's first baseline, from its top — where its text sits, whatever
171
+ * fallback face in it is taller than the base. Both engines give their
172
+ * lines one (ntk's `lines[].baseline`, the CoreText bridge's too); null
173
+ * for one that does not. */
174
+ function baselineOf(layout) {
175
+ const lines = layout
176
+ .lines;
177
+ const b = lines?.[0]?.baseline;
178
+ return typeof b === 'number' && Number.isFinite(b) ? b : null;
179
+ }
180
+ /**
181
+ * How far a laid-out piece takes the line across: where the caret stands at
182
+ * its end. Not its layout's width, which leaves out the white space a piece
183
+ * ends on, as a layout's width does. A piece cut after a space set the next
184
+ * one a column to its left, so the space vanished from the screen and every
185
+ * caret after it was a column out: seven columns by the end of a line of
186
+ * 78,000 characters, from the eight pieces that ended on one. The larger of
187
+ * the two, since the caret after a right-to-left piece is at its left.
188
+ */
189
+ function advanceOf(layout, text) {
190
+ const end = layout.caretPosition(utf16ToCp(text, text.length)).x;
191
+ return Math.max(layout.width, end);
192
+ }
193
+ class ChunkedLayout {
194
+ chunks;
195
+ _layOut;
196
+ _grew;
197
+ /**
198
+ * The pieces before this one are laid out, and every piece up to it sits
199
+ * where it is drawn. Past it, a piece sits where the guesses before it
200
+ * put it.
201
+ *
202
+ * Laying out a line of a million characters shaped every piece of it
203
+ * before the first paint: 1.4 s to open a minified file, of which the
204
+ * view needed one piece. So a piece is laid out when anything asks about
205
+ * it or a piece after it: a draw, a caret, a click. Everything left of
206
+ * what is asked about is laid out too, so whatever is drawn is where one
207
+ * layout of the whole line would put it. Only the width of the part
208
+ * nobody has looked at is a guess, and the editor's scroll extent follows
209
+ * it as it firms up (`grew`).
210
+ */
211
+ _laid = 0;
212
+ constructor(
213
+ /** The pieces, in order, and never empty. */
214
+ chunks,
215
+ /** A piece's layout, from its spans. */
216
+ _layOut,
217
+ /** Told the line's width when laying out more of it changed it. */
218
+ _grew = () => { }) {
219
+ this.chunks = chunks;
220
+ this._layOut = _layOut;
221
+ this._grew = _grew;
222
+ while (this._laid < chunks.length && chunks[this._laid].layout) {
223
+ this._laid += 1;
224
+ }
225
+ // the first piece at once: the line's height, and its baseline
226
+ this._layOutThrough(0);
227
+ }
228
+ get width() {
229
+ // the last piece's ink, as one layout of the whole line measures it
230
+ const last = this.chunks[this.chunks.length - 1];
231
+ return last.x + (last.layout?.width ?? last.advance);
232
+ }
233
+ get height() {
234
+ let height = 0;
235
+ for (let k = 0; k < this._laid; k++) {
236
+ height = Math.max(height, this.chunks[k].layout.height);
237
+ }
238
+ return height;
239
+ }
240
+ /** Lay out the pieces through `k`, and move every piece after them to
241
+ * where the laid-out ones now end. */
242
+ _layOutThrough(k) {
243
+ const chunks = this.chunks;
244
+ const from = this._laid;
245
+ const through = Math.min(k, chunks.length - 1);
246
+ if (through < from)
247
+ return;
248
+ const before = this.width;
249
+ for (let j = from; j <= through; j++) {
250
+ const piece = chunks[j];
251
+ if (!piece.layout) {
252
+ piece.layout = this._layOut(piece.spans);
253
+ piece.spans = null;
254
+ piece.advance = advanceOf(piece.layout, piece.text);
255
+ }
256
+ }
257
+ this._laid = through + 1;
258
+ // the first piece that was not laid out sits where the ones before it
259
+ // end, and every piece after it where the one before it does
260
+ let x = chunks[from].x;
261
+ for (let j = from; j < chunks.length; j++) {
262
+ const piece = chunks[j];
263
+ piece.x = x;
264
+ x += piece.advance;
265
+ }
266
+ const width = this.width;
267
+ if (width !== before)
268
+ this._grew(width);
269
+ }
270
+ /** The last piece whose `key` is at or before `at`, laid out. */
271
+ _pieceAt(at, key) {
272
+ const chunks = this.chunks;
273
+ if (key === 'x') {
274
+ // a piece's place across is known only as far as the pieces are laid
275
+ // out: lay them out until the one `at` falls in is among them
276
+ while (this._laid < chunks.length && chunks[this._laid].x <= at) {
277
+ this._layOutThrough(this._laid);
278
+ }
279
+ }
280
+ let lo = 0;
281
+ let hi = chunks.length - 1;
282
+ while (lo < hi) {
283
+ const mid = (lo + hi + 1) >> 1;
284
+ if (chunks[mid][key] <= at)
285
+ lo = mid;
286
+ else
287
+ hi = mid - 1;
288
+ }
289
+ this._layOutThrough(lo);
290
+ return chunks[lo];
291
+ }
292
+ caretPosition(cp) {
293
+ const piece = this._pieceAt(cp, 'cp');
294
+ const at = piece.layout.caretPosition(cp - piece.cp);
295
+ return { x: piece.x + at.x, y: at.y, height: at.height };
296
+ }
297
+ indexAt(x, y) {
298
+ const piece = this._pieceAt(x, 'x');
299
+ return piece.cp + piece.layout.indexAt(x - piece.x, y);
300
+ }
301
+ /** `caretPosition`'s x for a UTF-16 offset into the display text, with
302
+ * the code point counted inside the one piece rather than the line. */
303
+ caretXAt(u16) {
304
+ const piece = this._pieceAt(u16, 'u16');
305
+ const cp = utf16ToCp(piece.text, u16 - piece.u16);
306
+ return piece.x + piece.layout.caretPosition(cp).x;
307
+ }
308
+ /** `indexAt`, answered as a UTF-16 offset into the display text. */
309
+ indexAtUtf16(x, y) {
310
+ const piece = this._pieceAt(x, 'x');
311
+ const cp = piece.layout.indexAt(x - piece.x, y);
312
+ return piece.u16 + cpToUtf16(piece.text, cp);
313
+ }
314
+ draw(ctx, x, y) {
315
+ this.drawSpan(ctx, x, y, -Infinity, Infinity);
316
+ }
317
+ /**
318
+ * The pieces that reach into `[from, to)` across the line, each with its
319
+ * first baseline at `baseline` — a piece holding an emoji is laid out
320
+ * taller than the rest, and set by its top it sat on a baseline of its
321
+ * own. `y` places a piece whose engine gives no baseline. The pieces up to
322
+ * `to` are laid out on the way, so each is drawn where it belongs.
323
+ */
324
+ drawSpan(ctx, x, y, from, to, baseline = null) {
325
+ const chunks = this.chunks;
326
+ for (let k = 0; k < chunks.length; k++) {
327
+ // where an unlaid piece sits is exact here: the ones before it are
328
+ // laid out by now
329
+ if (chunks[k].x >= to)
330
+ break;
331
+ this._layOutThrough(k);
332
+ const piece = chunks[k];
333
+ if (piece.x + piece.layout.width <= from)
334
+ continue;
335
+ const b = baseline == null ? null : baselineOf(piece.layout);
336
+ piece.layout.draw(ctx, x + piece.x, b == null ? y : baseline - b);
337
+ }
338
+ }
339
+ }
340
+ /** Where a long line is cut into pieces: after a delimiter that the few
341
+ * characters before it pick out, between `CHUNK_MIN` and `CHUNK_MAX` apart.
342
+ * Chosen by what the text says there rather than by how far the line has
343
+ * come, so a character typed into a piece moves the cuts near it and none
344
+ * of the rest — the pieces after it are the same text as before, and their
345
+ * layouts are found again rather than shaped again. */
346
+ function chunkBreaks(display, from = 0) {
347
+ const breaks = [from];
348
+ let last = from;
349
+ for (let i = from + 1; i < display.length; i++) {
350
+ const since = i - last;
351
+ if (since < CHUNK_MIN)
352
+ continue;
353
+ const code = display.charCodeAt(i - 1);
354
+ // never between the halves of a surrogate pair
355
+ if (code >= 0xd800 && code <= 0xdbff)
356
+ continue;
357
+ const delimiter = code === 32 || code === 44 || code === 59 || code === 123 || code === 125;
358
+ const picked = delimiter &&
359
+ ((display.charCodeAt(i - 2) * 31 + display.charCodeAt(i - 3) * 7) &
360
+ 15) ===
361
+ 0;
362
+ if (picked || since >= CHUNK_MAX) {
363
+ breaks.push(i);
364
+ last = i;
365
+ }
366
+ }
367
+ return breaks;
368
+ }
369
+ /** A line's spans cut at `breaks`. */
370
+ function chunkSpans(spans, breaks) {
371
+ const pieces = [];
372
+ let piece = [];
373
+ let at = 0;
374
+ let next = 1;
375
+ for (const span of spans) {
376
+ let text = span.text;
377
+ while (text.length > 0) {
378
+ const cut = next < breaks.length ? breaks[next] : Infinity;
379
+ const take = Math.min(text.length, cut - at);
380
+ piece.push({ ...span, text: text.slice(0, take) });
381
+ text = text.slice(take);
382
+ at += take;
383
+ if (at === cut) {
384
+ pieces.push(piece);
385
+ piece = [];
386
+ next += 1;
387
+ }
388
+ }
389
+ }
390
+ if (piece.length > 0)
391
+ pieces.push(piece);
392
+ return pieces;
393
+ }
89
394
  export class CodeEditorNode extends Node {
90
395
  _lines;
91
396
  /** What `_lines` was last built from — how a controlled `value` that the
92
- * parent refused to update snaps back on the next read (see `_syncProps`). */
397
+ * parent refused to update snaps back on the next read (see `_syncProps`).
398
+ * Null where no `value` prop reads it: the text is joined only for a
399
+ * reader. */
93
400
  _synced;
94
401
  _caret = { line: 0, ch: 0 };
95
402
  _anchor = { line: 0, ch: 0 };
@@ -98,15 +405,25 @@ export class CodeEditorNode extends Node {
98
405
  _focused = false;
99
406
  _caretOn = false;
100
407
  _blinkTimer = null;
408
+ /** Whether the last paint drew a horizontal thumb — see `paint`. */
409
+ _thumbAcross = false;
101
410
  _goalX = null;
102
411
  _drag = null;
103
412
  _dragOrigin = { line: 0, ch: 0 };
104
413
  /** Escape was the last key, so the next Tab leaves instead of indenting —
105
414
  * the editor's way of not being a keyboard trap (see `defaultKeyDown`). */
106
415
  _tabEscapes = false;
107
- _history;
416
+ /** The steps, oldest first; `_historyIndex` of them are applied. */
417
+ _history = [];
108
418
  _historyIndex = 0;
109
419
  _undoRun = null;
420
+ /** The lines the last change replaced, in the text it left: what
421
+ * `_repaintSince` claims of an edit. */
422
+ /** The `diagnostics` prop, moved through the edits since it was set —
423
+ * see `_mapDiagnostics` — and the prop array it was taken from. */
424
+ _diagnostics = [];
425
+ _diagnosticsFrom = undefined;
426
+ _lastEdit = null;
110
427
  _pendingValue = null;
111
428
  _keyNative = null;
112
429
  _tok = null;
@@ -115,6 +432,12 @@ export class CodeEditorNode extends Node {
115
432
  _metricsKey = '';
116
433
  _lineH = 0;
117
434
  _charW = 0;
435
+ /** The base face's first baseline and the bottom of its glyphs, from the
436
+ * top of a row: every row's text sits on the one, and a squiggle hugs
437
+ * the other, whatever a fallback face in the row would make of its own
438
+ * layout. Null baseline: an engine that does not say (see `paint`). */
439
+ _baseline = null;
440
+ _textBottom = 0;
118
441
  _widest = 0;
119
442
  constructor(props, app) {
120
443
  super(ELEMENT, props, app);
@@ -125,9 +448,6 @@ export class CodeEditorNode extends Node {
125
448
  : '';
126
449
  this._lines = initial.split('\n');
127
450
  this._synced = initial;
128
- this._history = [
129
- { value: initial, caret: this._caret, anchor: this._anchor },
130
- ];
131
451
  // Without this nothing focuses the editor and no key ever reaches it —
132
452
  // an app's `focusable`/`tabIndex` prop still overrides either way, which
133
453
  // is how `<CodeEditor disabled>` stays out of the tab order.
@@ -172,9 +492,10 @@ export class CodeEditorNode extends Node {
172
492
  const text = next == null ? '' : String(next);
173
493
  if (text === this._lines.join('\n'))
174
494
  return;
495
+ const view = this._viewBefore();
175
496
  this._setText(text);
176
- this._noteExternal();
177
- this._repaint();
497
+ this._repaintSince(view, this._lastEdit);
498
+ this._lastEdit = null;
178
499
  }
179
500
  get name() {
180
501
  return this.props.name;
@@ -203,16 +524,28 @@ export class CodeEditorNode extends Node {
203
524
  if (text === this._synced)
204
525
  return;
205
526
  this._setText(text);
206
- this._noteExternal();
207
527
  }
528
+ /**
529
+ * A text from outside — a controlled `value`, the `value` setter — as an
530
+ * undoable step of its own, and as the edit it really is: the lines it
531
+ * shares with the text before it at either end are neither tokenized nor
532
+ * laid out again. A formatter's output, or a parent echoing a value back
533
+ * with one line changed, used to reset the whole editor.
534
+ */
208
535
  _setText(text) {
209
- this._lines = text.split('\n');
536
+ const next = text.split('\n');
537
+ const caret = clampPos(next, this._caret);
538
+ const anchor = clampPos(next, this._anchor);
210
539
  this._synced = text;
211
- this._caret = clampPos(this._lines, this._caret);
212
- this._anchor = clampPos(this._lines, this._anchor);
213
- this._lineCache.clear();
214
- this._widest = 0;
215
- this._tokenizer()?.setLines(this._lines);
540
+ this._edit(0, this._lines.length, next, null, caret, anchor);
541
+ this._caret = caret;
542
+ this._anchor = anchor;
543
+ // the widest line may have gone: what is known of the lines left
544
+ let widest = 0;
545
+ for (const entry of this._lineCache.values()) {
546
+ widest = Math.max(widest, entry.layout?.width ?? 0);
547
+ }
548
+ this._widest = widest;
216
549
  }
217
550
  // --- tokenizer -----------------------------------------------------------
218
551
  _tokenizer() {
@@ -225,7 +558,7 @@ export class CodeEditorNode extends Node {
225
558
  invalidate: (fromLine) => {
226
559
  for (const key of this._lineCache.keys()) {
227
560
  if (key >= fromLine)
228
- this._lineCache.delete(key);
561
+ this._dropLine(key);
229
562
  }
230
563
  this._repaint();
231
564
  },
@@ -233,6 +566,7 @@ export class CodeEditorNode extends Node {
233
566
  : null;
234
567
  this._tok?.setLines(this._lines);
235
568
  this._lineCache.clear();
569
+ this._chunkedStash = null;
236
570
  }
237
571
  return this._tok;
238
572
  }
@@ -298,6 +632,13 @@ export class CodeEditorNode extends Node {
298
632
  const probe = fonts.layout('Mg', s);
299
633
  this._lineH = probe.height || Number(s.size) * 1.4;
300
634
  this._charW = fonts.layout('0', s).width || Number(s.size) * 0.6;
635
+ this._baseline = baselineOf(probe);
636
+ const line = probe
637
+ .lines?.[0];
638
+ this._textBottom =
639
+ this._baseline != null && typeof line?.descent === 'number'
640
+ ? this._baseline + line.descent
641
+ : this._lineH;
301
642
  }
302
643
  _lineHeight() {
303
644
  this._metrics();
@@ -369,16 +710,20 @@ export class CodeEditorNode extends Node {
369
710
  }
370
711
  _lineEntry(line) {
371
712
  const raw = this._lines[line];
372
- const map = tabMap(raw, this._tabSize());
373
- const tokens = this._tokenizer()?.lineTokens(line) ?? [];
713
+ const tabSize = this._tabSize();
714
+ const tokens = this._tokenizer()?.lineTokens(line) ?? NO_TOKENS;
374
715
  const styleKey = this._metricsKey;
375
716
  const cached = this._lineCache.get(line);
717
+ // the line's own string, not its display: building the tab map is
718
+ // linear in the line, and a long one is asked for many times a frame
376
719
  if (cached &&
377
- cached.display === map.display &&
720
+ cached.raw === raw &&
721
+ cached.tabSize === tabSize &&
378
722
  cached.tokens === tokens &&
379
723
  cached.styleKey === styleKey) {
380
724
  return cached;
381
725
  }
726
+ const map = tabMap(raw, tabSize);
382
727
  const fonts = this._fonts();
383
728
  const base = this._textStyle();
384
729
  let layout = null;
@@ -413,9 +758,23 @@ export class CodeEditorNode extends Node {
413
758
  if (cursor < map.display.length) {
414
759
  spans.push({ text: map.display.slice(cursor) });
415
760
  }
416
- layout = fonts.layout(spans.length > 0 ? spans : [{ text: '' }], base);
761
+ if (map.display.length > CHUNKED_PAST) {
762
+ const stash = this._chunkedStash;
763
+ const previous = cached?.layout instanceof ChunkedLayout &&
764
+ cached.styleKey === styleKey
765
+ ? cached.layout
766
+ : stash?.styleKey === styleKey
767
+ ? stash.layout
768
+ : null;
769
+ layout = this._chunkedLayout(fonts, base, styleKey, map.display, spans, previous);
770
+ }
771
+ else {
772
+ layout = fonts.layout(spans.length > 0 ? spans : [{ text: '' }], base);
773
+ }
417
774
  }
418
775
  const entry = {
776
+ raw,
777
+ tabSize,
419
778
  display: map.display,
420
779
  tokens,
421
780
  styleKey,
@@ -427,12 +786,167 @@ export class CodeEditorNode extends Node {
427
786
  this._widest = layout.width;
428
787
  return entry;
429
788
  }
789
+ /**
790
+ * A long line in pieces (`ChunkedLayout`), each piece's layout kept by
791
+ * what it holds, so an edit shapes only the pieces whose text or colours
792
+ * it changed. Bounded like the line cache it serves.
793
+ */
794
+ _chunkLayouts = new Map();
795
+ /** The last long line an edit took out of the line cache, which the next
796
+ * long line laid out starts from (`_chunkedLayout`). Checked piece by
797
+ * piece against what it is asked for, so one from another line, or from
798
+ * before an edit that changed everything, is simply not used. */
799
+ _chunkedStash = null;
800
+ /** Take a line's entry out of the cache, keeping a long one's pieces for
801
+ * the rebuild the edit that dropped it is about to ask for. */
802
+ _dropLine(line) {
803
+ const entry = this._lineCache.get(line);
804
+ if (entry?.layout instanceof ChunkedLayout) {
805
+ this._chunkedStash = { styleKey: entry.styleKey, layout: entry.layout };
806
+ }
807
+ this._lineCache.delete(line);
808
+ }
809
+ _chunkedLayout(fonts, base, styleKey, display, spans, previous) {
810
+ const chunks = [];
811
+ // The pieces an edit left alone, from the start of the line: the same
812
+ // text at the same place, in the same runs. Their layouts, and where
813
+ // they sit, are what they were, and the break after each is a break of
814
+ // the new line too, since a break depends on the text before it alone —
815
+ // all but the old line's last piece, which ended where the old line did.
816
+ // A keystroke at the end of a line of a million characters built a key
817
+ // from every piece's text and looked each one up: most of its 30 ms.
818
+ let from = 0;
819
+ let si = 0;
820
+ let spanStart = 0;
821
+ if (previous) {
822
+ const old = previous.chunks;
823
+ for (let k = 0; k < old.length - 1; k++) {
824
+ const chunk = old[k];
825
+ // a piece nobody laid out is no saving to keep, and its place across
826
+ // was a guess
827
+ if (!chunk.layout)
828
+ break;
829
+ if (chunk.u16 !== from || !display.startsWith(chunk.text, from))
830
+ break;
831
+ const end = from + chunk.text.length;
832
+ let i = si;
833
+ let start = spanStart;
834
+ let at = from;
835
+ let r = 0;
836
+ let same = true;
837
+ while (at < end) {
838
+ const span = spans[i];
839
+ const spanEnd = start + span.text.length;
840
+ const take = Math.min(spanEnd, end) - at;
841
+ const runs = chunk.runs;
842
+ if (runs[r] !== take ||
843
+ runs[r + 1] !== (span.color ?? '') ||
844
+ runs[r + 2] !== (span.weight ?? '') ||
845
+ runs[r + 3] !== (span.style ?? '')) {
846
+ same = false;
847
+ break;
848
+ }
849
+ r += 4;
850
+ at += take;
851
+ if (at === spanEnd) {
852
+ start = spanEnd;
853
+ i += 1;
854
+ }
855
+ }
856
+ if (!same || r !== chunk.runs.length)
857
+ break;
858
+ chunks.push(chunk);
859
+ from = end;
860
+ si = i;
861
+ spanStart = start;
862
+ }
863
+ }
864
+ const last = chunks[chunks.length - 1];
865
+ let cp = last ? last.cp + utf16ToCp(last.text, last.text.length) : 0;
866
+ let x = last ? last.x + last.advance : 0;
867
+ // the spans from where the kept pieces end, the first one cut there
868
+ const rest = spans.slice(si);
869
+ if (rest.length > 0 && from > spanStart) {
870
+ rest[0] = {
871
+ ...rest[0],
872
+ text: rest[0].text.slice(from - spanStart),
873
+ };
874
+ }
875
+ // The rest are laid out when something asks about them (ChunkedLayout),
876
+ // and guessed across until then: a column a code point, which is what
877
+ // a monospace face makes of ASCII to the pixel.
878
+ this._metrics();
879
+ const column = this._charW;
880
+ const breaks = chunkBreaks(display, from).map((b) => b - from);
881
+ let u16 = from;
882
+ for (const piece of chunkSpans(rest, breaks)) {
883
+ const text = piece.map((sp) => sp.text).join('');
884
+ const runs = [];
885
+ for (const sp of piece) {
886
+ runs.push(sp.text.length, sp.color ?? '', sp.weight ?? '', sp.style ?? '');
887
+ }
888
+ const points = utf16ToCp(text, text.length);
889
+ const guess = points * column;
890
+ chunks.push({
891
+ cp,
892
+ u16,
893
+ x,
894
+ text,
895
+ layout: null,
896
+ spans: piece,
897
+ advance: guess,
898
+ runs,
899
+ });
900
+ cp += points;
901
+ u16 += text.length;
902
+ x += guess;
903
+ }
904
+ if (chunks.length === 0) {
905
+ // an empty display is never chunked; kept here all the same so the
906
+ // layout is never empty
907
+ const layout = fonts.layout([{ text: '' }], base);
908
+ chunks.push({
909
+ cp: 0,
910
+ u16: 0,
911
+ x: 0,
912
+ text: '',
913
+ layout,
914
+ spans: null,
915
+ advance: layout.width,
916
+ runs: [],
917
+ });
918
+ }
919
+ return new ChunkedLayout(chunks, (piece) => this._pieceLayout(fonts, base, styleKey, piece), (width) => {
920
+ if (width > this._widest)
921
+ this._widest = width;
922
+ });
923
+ }
924
+ /** A long line's piece, laid out — or found, where the same text in the
925
+ * same colours was laid out before (`_chunkLayouts`). */
926
+ _pieceLayout(fonts, base, styleKey, piece) {
927
+ const key = styleKey +
928
+ '|' +
929
+ piece
930
+ .map((sp) => `${sp.color ?? ''},${sp.weight ?? ''},${sp.style ?? ''},${sp.text}`)
931
+ .join('\u0000');
932
+ let layout = this._chunkLayouts.get(key);
933
+ if (!layout) {
934
+ if (this._chunkLayouts.size > 4096)
935
+ this._chunkLayouts.clear();
936
+ layout = fonts.layout(piece, base);
937
+ this._chunkLayouts.set(key, layout);
938
+ }
939
+ return layout;
940
+ }
430
941
  /** Caret x (device pixels from the text origin) for a position. */
431
942
  _caretX(pos) {
432
943
  const entry = this._lineEntry(pos.line);
433
944
  if (!entry.layout)
434
945
  return 0;
435
946
  const disp = entry.map.toDisplay(pos.ch);
947
+ if (entry.layout instanceof ChunkedLayout) {
948
+ return entry.layout.caretXAt(disp);
949
+ }
436
950
  return entry.layout.caretPosition(utf16ToCp(entry.display, disp)).x;
437
951
  }
438
952
  /** Window coordinates, device pixels → document position. */
@@ -444,8 +958,10 @@ export class CodeEditorNode extends Node {
444
958
  const entry = this._lineEntry(line);
445
959
  if (!entry.layout)
446
960
  return { line, ch: 0 };
447
- const cp = entry.layout.indexAt(x - textX + this._scrollX, 0);
448
- const disp = cpToUtf16(entry.display, cp);
961
+ const at = x - textX + this._scrollX;
962
+ const disp = entry.layout instanceof ChunkedLayout
963
+ ? entry.layout.indexAtUtf16(at, 0)
964
+ : cpToUtf16(entry.display, entry.layout.indexAt(at, 0));
449
965
  return clampPos(this._lines, { line, ch: entry.map.toRaw(disp) });
450
966
  }
451
967
  /** Font facts the completion popup sizes itself with — logical pixels,
@@ -495,20 +1011,24 @@ export class CodeEditorNode extends Node {
495
1011
  };
496
1012
  }
497
1013
  // --- scrolling -----------------------------------------------------------
1014
+ // Both limits, and every offset a caret is revealed at, are whole device
1015
+ // pixels: a line is a measured height, and a shift that is not whole is
1016
+ // not a copy (`_blitScroll`).
498
1017
  _maxScrollY() {
499
1018
  const content = this._contentRect();
500
- return Math.max(0, this._lines.length * this._lineHeight() - content.height);
1019
+ return Math.max(0, Math.ceil(this._lines.length * this._lineHeight() - content.height));
501
1020
  }
502
1021
  _maxScrollX() {
503
1022
  const content = this._contentRect();
504
1023
  const textW = content.width - this._gutterWidth();
505
- return Math.max(0, this._widest + CARET_MARGIN * this._scale - textW);
1024
+ return Math.max(0, Math.ceil(this._widest + CARET_MARGIN * this._scale - textW));
506
1025
  }
507
- /** Wheel notches (deltaX/deltaY arrive as ±1 per notch) → three lines a
508
- * notch, in the device pixels the offsets are kept in. */
1026
+ /** The wheel, in the unit core hands every scroller: logical pixels, not
1027
+ * notches — a notch is 48 of them (react-x11's `WHEEL_NOTCH_PX`) and a
1028
+ * touchpad reports what it measured — so a notch moves the text as far
1029
+ * as it moves a `<box>` pane beside the editor. */
509
1030
  handleWheel(ev) {
510
- const step = this._lineHeight() * 3;
511
- return this._scrollByDevice(ev.deltaX * step, ev.deltaY * step);
1031
+ return this.scrollBy(ev.deltaX, ev.deltaY);
512
1032
  }
513
1033
  /** Logical pixels, like core's own `scrollBy`: application code writes
514
1034
  * the unit it writes its styles in. Returns true when the offset moved
@@ -523,9 +1043,78 @@ export class CodeEditorNode extends Node {
523
1043
  const ny = Math.max(0, Math.min(this._scrollY + dy, this._maxScrollY()));
524
1044
  if (nx === this._scrollX && ny === this._scrollY)
525
1045
  return false;
1046
+ const shiftX = this._scrollX - nx;
1047
+ const shiftY = this._scrollY - ny;
526
1048
  this._scrollX = nx;
527
1049
  this._scrollY = ny;
528
- this.root?.invalidate(false, this.abs, 'scroll');
1050
+ if (!this._blitScroll(shiftX, shiftY)) {
1051
+ this.root?.invalidate(false, this.abs, 'scroll');
1052
+ }
1053
+ return true;
1054
+ }
1055
+ /**
1056
+ * The rect a scroll of the view shifts (core's `scrollContents`): the
1057
+ * content box less the scroll thumbs' strips, which stay put while the
1058
+ * text moves under them — and sideways, less the gutter as well, which
1059
+ * stays put while the text moves beside it.
1060
+ */
1061
+ _scrollRegion(sideways) {
1062
+ const box = this._contentRect();
1063
+ const bar = 4 * this._scale; // a thumb's inset, see `paint`
1064
+ const across = this._maxScrollX() > 0 ? bar : 0;
1065
+ const gutter = sideways ? this._gutterWidth() : 0;
1066
+ return {
1067
+ x: box.x + gutter,
1068
+ y: box.y,
1069
+ width: box.width - gutter - bar,
1070
+ height: box.height - across,
1071
+ };
1072
+ }
1073
+ /**
1074
+ * A scroll along one axis as a blit: inside `_scrollRegion` the new frame
1075
+ * is the old one shifted. The thumbs' strips are carved out of the region
1076
+ * and claimed beside it, edge to edge, which leaves the frame a blit; the
1077
+ * band the shift exposes is all `paint` draws (`paintDamage()`).
1078
+ * `pinned` are rows that change as well as move — an edit's, the
1079
+ * caret's, when revealing it scrolled — which the frame repaints after
1080
+ * the copy instead of refusing it, so the caller claims inside them.
1081
+ *
1082
+ * False where the promise would not hold: both ways at once, since the
1083
+ * gutter moves with the text down and not across; off the device grid,
1084
+ * where a shift is not a copy; and under a background image, which does
1085
+ * not translate. Core checks the rest — the border ring, a rounded
1086
+ * corner, anything drawn over the box — and repaints the region itself
1087
+ * when one of them fails.
1088
+ */
1089
+ _blitScroll(shiftX, shiftY, pinned = null) {
1090
+ if ((shiftX !== 0) === (shiftY !== 0))
1091
+ return false;
1092
+ if (!Number.isInteger(shiftX) || !Number.isInteger(shiftY))
1093
+ return false;
1094
+ if (this.style.backgroundImage)
1095
+ return false;
1096
+ const box = this._contentRect();
1097
+ const bar = 4 * this._scale;
1098
+ const rect = this._scrollRegion(shiftX !== 0);
1099
+ const whole = [rect.x, rect.y, rect.width, rect.height, bar];
1100
+ if (!whole.every(Number.isInteger))
1101
+ return false;
1102
+ if (rect.width <= 0 || rect.height <= 0)
1103
+ return false;
1104
+ if (Math.abs(shiftX) >= rect.width || Math.abs(shiftY) >= rect.height) {
1105
+ return false;
1106
+ }
1107
+ this.scrollContents(rect, shiftX, shiftY, null, pinned);
1108
+ this.invalidate(false, { x: box.x + box.width - bar, y: box.y, width: bar, height: box.height }, 'scroll');
1109
+ const across = box.height - rect.height;
1110
+ if (across > 0) {
1111
+ this.invalidate(false, {
1112
+ x: box.x,
1113
+ y: rect.y + rect.height,
1114
+ width: box.width - bar,
1115
+ height: across,
1116
+ }, 'scroll');
1117
+ }
529
1118
  return true;
530
1119
  }
531
1120
  _ensureCaretVisible() {
@@ -533,19 +1122,19 @@ export class CodeEditorNode extends Node {
533
1122
  const content = this._contentRect();
534
1123
  const lineTop = this._caret.line * this._lineH;
535
1124
  if (lineTop < this._scrollY)
536
- this._scrollY = lineTop;
1125
+ this._scrollY = Math.floor(lineTop);
537
1126
  const lineBottom = lineTop + this._lineH;
538
1127
  if (lineBottom > this._scrollY + content.height) {
539
- this._scrollY = lineBottom - content.height;
1128
+ this._scrollY = Math.ceil(lineBottom - content.height);
540
1129
  }
541
1130
  const textW = content.width - this._gutterWidth();
542
1131
  const x = this._caretX(this._caret);
543
1132
  const margin = CARET_MARGIN * this._scale;
544
1133
  if (x < this._scrollX + margin) {
545
- this._scrollX = Math.max(0, x - margin);
1134
+ this._scrollX = Math.max(0, Math.floor(x - margin));
546
1135
  }
547
1136
  if (x > this._scrollX + textW - margin) {
548
- this._scrollX = x - textW + margin;
1137
+ this._scrollX = Math.ceil(x - textW + margin);
549
1138
  }
550
1139
  this._scrollY = Math.max(0, Math.min(this._scrollY, this._maxScrollY()));
551
1140
  this._scrollX = Math.max(0, this._scrollX);
@@ -569,12 +1158,14 @@ export class CodeEditorNode extends Node {
569
1158
  };
570
1159
  return ev;
571
1160
  }
1161
+ /** `value` is a thunk where joining the text is the caller's cost: the
1162
+ * text is joined for a handler that is there to read it, and only then. */
572
1163
  _fire(prop, value) {
573
1164
  const handler = this.props[prop];
574
1165
  if (!handler)
575
1166
  return;
576
1167
  const previous = this._pendingValue;
577
- this._pendingValue = value;
1168
+ this._pendingValue = typeof value === 'function' ? value() : value;
578
1169
  try {
579
1170
  handler(this._makeEvent(prop === 'onSubmit'
580
1171
  ? 'submit'
@@ -590,22 +1181,297 @@ export class CodeEditorNode extends Node {
590
1181
  this._caretOn = true;
591
1182
  this.root?.invalidate(false, this.abs, 'text');
592
1183
  }
593
- // --- history -------------------------------------------------------------
594
- _noteExternal() {
595
- this._undoRun = null;
596
- const value = this._lines.join('\n');
597
- const top = this._history[this._historyIndex];
598
- if (top && top.value === value)
1184
+ /** What an edit or a caret move can change on screen, read before it. */
1185
+ _viewBefore() {
1186
+ const selection = this._hasSelection() ? this._selectionRange() : null;
1187
+ const brackets = this._focused && this.props.matchBrackets !== false
1188
+ ? this._findBracketMatch()
1189
+ : null;
1190
+ return {
1191
+ caretLine: this._caret.line,
1192
+ selection: selection && [selection[0].line, selection[1].line],
1193
+ brackets: brackets && [brackets[0].line, brackets[1].line],
1194
+ scrollX: this._scrollX,
1195
+ scrollY: this._scrollY,
1196
+ gutter: this._gutterWidth(),
1197
+ empty: this._lines.length === 1 && this._lines[0].length === 0,
1198
+ maxX: this._maxScrollX(),
1199
+ maxY: this._maxScrollY(),
1200
+ };
1201
+ }
1202
+ /**
1203
+ * Rows (inclusive ranges, any order) as the device-pixel bands they are
1204
+ * drawn in, `[top, bottom)`, merged where they touch and cut to the
1205
+ * content box. Glyphs ink a little past their row — a descender's
1206
+ * anti-aliasing below it, an accent above — into pixels the next row's
1207
+ * claim covers and this row's does not: a line edited left its old
1208
+ * descenders in the row under it. So a band reaches that far into its
1209
+ * neighbours, which the paint draws for it (`paint` culls a row either
1210
+ * side).
1211
+ */
1212
+ _rowBands(rows) {
1213
+ const content = this._contentRect();
1214
+ const lineH = this._lineH;
1215
+ const spill = 2 * this._scale;
1216
+ const bands = [];
1217
+ const band = (from, to) => {
1218
+ const top = Math.max(content.y, Math.floor(content.y + from * lineH - this._scrollY - spill));
1219
+ const bottom = Math.min(content.y + content.height, Math.ceil(content.y + (to + 1) * lineH - this._scrollY + spill));
1220
+ if (bottom > top)
1221
+ bands.push([top, bottom]);
1222
+ };
1223
+ const sorted = rows.slice().sort((a, b) => a[0] - b[0]);
1224
+ let open = null;
1225
+ for (const [a, b] of sorted) {
1226
+ if (open && a <= open[1] + 1)
1227
+ open[1] = Math.max(open[1], b);
1228
+ else {
1229
+ if (open)
1230
+ band(open[0], open[1]);
1231
+ open = [a, b];
1232
+ }
1233
+ }
1234
+ if (open)
1235
+ band(open[0], open[1]);
1236
+ return bands;
1237
+ }
1238
+ /** Claim the rows the squiggles of both lists are on: a linter's answer
1239
+ * arriving is a few rows, not the editor, and the layouts stay. */
1240
+ _repaintDiagnostics(before, after) {
1241
+ const content = this._contentRect();
1242
+ const lineH = this._lineH;
1243
+ if (!this.root || content.width <= 0 || !(lineH > 0))
599
1244
  return;
600
- this._pushHistory({ value, caret: this._caret, anchor: this._anchor });
1245
+ const first = Math.floor(this._scrollY / lineH);
1246
+ const bottom = Math.ceil((this._scrollY + content.height) / lineH);
1247
+ const rows = [];
1248
+ const last = this._lines.length - 1;
1249
+ for (const d of [...before, ...after]) {
1250
+ // where the paint puts it: clamped to the text
1251
+ const a = Math.max(first, Math.min(last, Math.min(d.from.line, d.to.line)));
1252
+ const b = Math.min(bottom, Math.min(last, Math.max(d.from.line, d.to.line)));
1253
+ if (a <= b)
1254
+ rows.push([a, b]);
1255
+ }
1256
+ for (const [top, end] of this._rowBands(rows)) {
1257
+ this.invalidate(false, { x: content.x, y: top, width: content.width, height: end - top }, 'text');
1258
+ }
601
1259
  }
602
- _pushHistory(entry) {
603
- this._history.length = this._historyIndex + 1;
604
- this._history.push(entry);
1260
+ /**
1261
+ * Claim what a change since `before` moved: the rows it touched rather
1262
+ * than the editor, so a keystroke paints its line, not the forty in view.
1263
+ *
1264
+ * A row is the caret's, the active line's and its gutter number's, before
1265
+ * and after; every row of the selection and the bracket pair, before and
1266
+ * after; the rows an edit replaced, and every row below them when it
1267
+ * added or took lines away; and a row below the edit whose tokens it
1268
+ * moved — an opened comment or string recolours what follows (the test
1269
+ * `_lineEntry` makes: the line's tokens are no longer the ones it was laid
1270
+ * out with). A thumb's strip goes with a change to what sizes it — the
1271
+ * line count, the widest line — and of the right one, only what no row
1272
+ * already repaints: a strip that overlapped a row would reach core as the
1273
+ * box around both, which is the editor again. A scroll to the caret along
1274
+ * one axis is a blit (`_blitScroll`) with those rows pinned — repainted
1275
+ * after the copy, where they now are. Whatever else moves the whole view
1276
+ * — a scroll both ways, a gutter that grew a digit, the placeholder — is
1277
+ * the editor.
1278
+ */
1279
+ _repaintSince(before, edit) {
1280
+ this._caretOn = true;
1281
+ const root = this.root;
1282
+ if (!root)
1283
+ return;
1284
+ const content = this._contentRect();
1285
+ const lineH = this._lineH;
1286
+ const empty = this._lines.length === 1 && this._lines[0].length === 0;
1287
+ // how far the pixels move: the other way from the offsets
1288
+ const shiftX = before.scrollX - this._scrollX;
1289
+ const shiftY = before.scrollY - this._scrollY;
1290
+ if (content.width <= 0 ||
1291
+ content.height <= 0 ||
1292
+ !(lineH > 0) ||
1293
+ before.gutter !== this._gutterWidth() ||
1294
+ (before.empty !== empty && this.props.placeholder != null)) {
1295
+ this._repaint();
1296
+ return;
1297
+ }
1298
+ const first = Math.max(0, Math.floor(this._scrollY / lineH));
1299
+ // rows in view, text or not: a row past the last line is still where a
1300
+ // line was drawn before an edit took it away
1301
+ const bottom = Math.ceil((this._scrollY + content.height) / lineH);
1302
+ const last = Math.min(this._lines.length - 1, bottom);
1303
+ // rows as inclusive ranges, merged below
1304
+ const rows = [];
1305
+ const add = (from, to) => {
1306
+ const a = Math.max(first, Math.min(from, to));
1307
+ const b = Math.min(bottom, Math.max(from, to));
1308
+ if (a <= b)
1309
+ rows.push([a, b]);
1310
+ };
1311
+ add(before.caretLine, before.caretLine);
1312
+ add(this._caret.line, this._caret.line);
1313
+ if (before.selection)
1314
+ add(before.selection[0], before.selection[1]);
1315
+ if (this._hasSelection()) {
1316
+ const [a, b] = this._selectionRange();
1317
+ add(a.line, b.line);
1318
+ }
1319
+ if (before.brackets) {
1320
+ add(before.brackets[0], before.brackets[0]);
1321
+ add(before.brackets[1], before.brackets[1]);
1322
+ }
1323
+ if (this._focused && this.props.matchBrackets !== false) {
1324
+ const now = this._findBracketMatch();
1325
+ if (now) {
1326
+ add(now[0].line, now[0].line);
1327
+ add(now[1].line, now[1].line);
1328
+ }
1329
+ }
1330
+ if (edit) {
1331
+ // laid out now rather than in the paint, so the widest line — what
1332
+ // the horizontal thumb is sized by — is known before it is claimed
1333
+ const to = Math.min(last, edit.fromLine + edit.inserted - 1);
1334
+ for (let i = Math.max(first, edit.fromLine); i <= to; i++) {
1335
+ this._lineEntry(i);
1336
+ }
1337
+ if (edit.inserted !== edit.removed) {
1338
+ add(edit.fromLine, bottom);
1339
+ }
1340
+ else {
1341
+ add(edit.fromLine, edit.fromLine + edit.inserted - 1);
1342
+ // the rows after it that its tokens re-coloured
1343
+ const tok = this._tokenizer();
1344
+ for (let i = Math.max(first, edit.fromLine + edit.inserted); tok && i <= last; i++) {
1345
+ const cached = this._lineCache.get(i);
1346
+ if (!cached ||
1347
+ cached.raw !== this._lines[i] ||
1348
+ cached.tokens !== (tok.lineTokens(i) ?? NO_TOKENS)) {
1349
+ add(i, i);
1350
+ }
1351
+ }
1352
+ }
1353
+ }
1354
+ const bands = this._rowBands(rows);
1355
+ if (shiftX !== 0 || shiftY !== 0) {
1356
+ // the rows inside the region are pinned: core repaints them after the
1357
+ // copy, changed or not, so what is left to claim is what of them lies
1358
+ // beside it — the gutter, sideways; the thumbs' strips go with the blit
1359
+ const region = this._scrollRegion(shiftX !== 0);
1360
+ const pinned = bands.map(([top, bottom]) => ({
1361
+ x: region.x,
1362
+ y: top,
1363
+ width: region.width,
1364
+ height: bottom - top,
1365
+ }));
1366
+ if (!this._blitScroll(shiftX, shiftY, pinned)) {
1367
+ this._repaint();
1368
+ return;
1369
+ }
1370
+ if (region.x > content.x) {
1371
+ // stopping where the region does: past it is the horizontal
1372
+ // thumb's strip, claimed whole — and two claims that overlap reach
1373
+ // core as the box around both, which here would take in the region
1374
+ // and refuse the blit
1375
+ const end = region.y + region.height;
1376
+ for (const [top, bottom] of bands) {
1377
+ if (Math.min(bottom, end) <= top)
1378
+ continue;
1379
+ this.invalidate(false, {
1380
+ x: content.x,
1381
+ y: top,
1382
+ width: region.x - content.x,
1383
+ height: Math.min(bottom, end) - top,
1384
+ }, 'text');
1385
+ }
1386
+ }
1387
+ return;
1388
+ }
1389
+ for (const [top, bottom] of bands) {
1390
+ this.invalidate(false, { x: content.x, y: top, width: content.width, height: bottom - top }, 'text');
1391
+ }
1392
+ const strip = 4 * this._scale; // a thumb's inset, see `paint`
1393
+ const maxY = this._maxScrollY();
1394
+ if (maxY !== before.maxY && (maxY > 0 || before.maxY > 0)) {
1395
+ // the rows between the bands, in the right thumb's column
1396
+ let y = content.y;
1397
+ for (const [top, bottom] of [
1398
+ ...bands,
1399
+ [content.y + content.height, content.y + content.height],
1400
+ ]) {
1401
+ if (top > y) {
1402
+ this.invalidate(false, {
1403
+ x: content.x + content.width - strip,
1404
+ y,
1405
+ width: strip,
1406
+ height: top - y,
1407
+ }, 'text');
1408
+ }
1409
+ y = Math.max(y, bottom);
1410
+ }
1411
+ }
1412
+ const maxX = this._maxScrollX();
1413
+ if (maxX !== before.maxX && (maxX > 0 || before.maxX > 0)) {
1414
+ this.invalidate(false, {
1415
+ x: content.x,
1416
+ y: content.y + content.height - strip,
1417
+ width: content.width,
1418
+ height: strip,
1419
+ }, 'text');
1420
+ }
1421
+ }
1422
+ // --- history -------------------------------------------------------------
1423
+ /**
1424
+ * Record a change about to be made — `removed` lines at `from` becoming
1425
+ * `lines` — and the selection it will leave. Called before the lines
1426
+ * change, because merging needs the text the step before it left.
1427
+ *
1428
+ * `run` names a coalescable kind of edit: a change of the same run as
1429
+ * the last one, on lines that one touched or beside them, joins its step
1430
+ * rather than starting one — a word typed is one undo.
1431
+ */
1432
+ _record(from, removed, lines, run, caret, anchor) {
1433
+ const top = this._history[this._historyIndex - 1];
1434
+ const text = this._lines;
1435
+ if (top &&
1436
+ run !== null &&
1437
+ run === this._undoRun &&
1438
+ this._historyIndex === this._history.length &&
1439
+ from <= top.from + top.after.length &&
1440
+ from + removed >= top.from) {
1441
+ // One step from the text before `top` to the text after this: the
1442
+ // union of the two ranges, in the text between them, read on either
1443
+ // side of what each changed.
1444
+ const lo = Math.min(top.from, from);
1445
+ const hi = Math.max(top.from + top.after.length, from + removed);
1446
+ top.before = [
1447
+ ...text.slice(lo, top.from),
1448
+ ...top.before,
1449
+ ...text.slice(top.from + top.after.length, hi),
1450
+ ];
1451
+ top.after = [
1452
+ ...text.slice(lo, from),
1453
+ ...lines,
1454
+ ...text.slice(from + removed, hi),
1455
+ ];
1456
+ top.from = lo;
1457
+ top.caret = caret;
1458
+ top.anchor = anchor;
1459
+ return;
1460
+ }
1461
+ this._history.length = this._historyIndex;
1462
+ this._history.push({
1463
+ from,
1464
+ before: text.slice(from, from + removed),
1465
+ after: lines,
1466
+ caretBefore: this._caret,
1467
+ anchorBefore: this._anchor,
1468
+ caret,
1469
+ anchor,
1470
+ });
605
1471
  if (this._history.length > UNDO_LIMIT) {
606
1472
  this._history.splice(0, this._history.length - UNDO_LIMIT);
607
1473
  }
608
- this._historyIndex = this._history.length - 1;
1474
+ this._historyIndex = this._history.length;
609
1475
  }
610
1476
  breakUndoRun() {
611
1477
  this._undoRun = null;
@@ -613,30 +1479,135 @@ export class CodeEditorNode extends Node {
613
1479
  get canUndo() {
614
1480
  return this._historyIndex > 0;
615
1481
  }
1482
+ get diagnostics() {
1483
+ return this._currentDiagnostics();
1484
+ }
1485
+ _currentDiagnostics() {
1486
+ const prop = this.props.diagnostics;
1487
+ if (prop !== this._diagnosticsFrom) {
1488
+ // A render hands the same answer over as a new array — a list mapped
1489
+ // from the linter's result inline — and that is not a new answer: the
1490
+ // squiggles stay where the edits since the last one moved them,
1491
+ // rather than going back to where the old text had them.
1492
+ if (!sameDiagnostics(prop, this._diagnosticsFrom)) {
1493
+ this._diagnostics = prop ? prop.slice() : [];
1494
+ }
1495
+ this._diagnosticsFrom = prop;
1496
+ }
1497
+ return this._diagnostics;
1498
+ }
1499
+ /**
1500
+ * Move the diagnostics through `removed` lines at `from` becoming `lines`,
1501
+ * before the text changes. A squiggle is where a linter looked, and the
1502
+ * linter is asked again only once the text settles: meanwhile, typing a
1503
+ * line above one left it under the line that had been there. The change
1504
+ * is narrowed to the characters it really touched — the text the old and
1505
+ * new lines share at either end — and a position in that text keeps its
1506
+ * character. One inside the change goes to its edge: a diagnostic's start
1507
+ * to the far side of what was inserted there, so typing before a flagged
1508
+ * word moves the flag along with it, and its end to the near side, so
1509
+ * typing after the word does not stretch the flag over the new text. A
1510
+ * diagnostic whose whole range the edit took away goes with it; one that
1511
+ * points at a place rather than a range stays.
1512
+ */
1513
+ _mapDiagnostics(from, removed, lines) {
1514
+ const list = this._currentDiagnostics();
1515
+ if (list.length === 0)
1516
+ return;
1517
+ const before = this._lines.slice(from, from + removed).join('\n');
1518
+ const after = lines.join('\n');
1519
+ let head = 0;
1520
+ const most = Math.min(before.length, after.length);
1521
+ while (head < most && before.charCodeAt(head) === after.charCodeAt(head)) {
1522
+ head++;
1523
+ }
1524
+ let tail = 0;
1525
+ while (tail < most - head &&
1526
+ before.charCodeAt(before.length - 1 - tail) ===
1527
+ after.charCodeAt(after.length - 1 - tail)) {
1528
+ tail++;
1529
+ }
1530
+ const shift = lines.length - removed;
1531
+ const grow = after.length - before.length;
1532
+ // where each old line starts inside `before`
1533
+ const starts = [];
1534
+ for (let i = 0, at = 0; i < removed; i++) {
1535
+ starts.push(at);
1536
+ at += this._lines[from + i].length + 1;
1537
+ }
1538
+ const move = (p, far) => {
1539
+ if (p.line < from)
1540
+ return p;
1541
+ if (p.line >= from + removed)
1542
+ return { line: p.line + shift, ch: p.ch };
1543
+ const text = this._lines[p.line];
1544
+ const o = starts[p.line - from] + Math.min(p.ch, text.length);
1545
+ let n;
1546
+ if (o < head)
1547
+ n = o;
1548
+ else if (o > before.length - tail)
1549
+ n = o + grow;
1550
+ else
1551
+ n = far ? after.length - tail : head;
1552
+ // back to a line and column of the new lines
1553
+ let line = 0;
1554
+ let at = 0;
1555
+ while (line < lines.length - 1 && n > at + lines[line].length) {
1556
+ at += lines[line].length + 1;
1557
+ line++;
1558
+ }
1559
+ return { line: from + line, ch: n - at };
1560
+ };
1561
+ const kept = [];
1562
+ for (const d of list) {
1563
+ const a = minPos(d.from, d.to);
1564
+ const b = maxPos(d.from, d.to);
1565
+ const point = posEqual(a, b);
1566
+ const start = move(a, !point);
1567
+ const end = move(b, false);
1568
+ if (comparePos(end, start) < 0 || (!point && posEqual(start, end))) {
1569
+ if (!point)
1570
+ continue; // what it pointed at is gone
1571
+ }
1572
+ kept.push({
1573
+ ...d,
1574
+ from: start,
1575
+ to: comparePos(end, start) < 0 ? start : end,
1576
+ });
1577
+ }
1578
+ this._diagnostics = kept;
1579
+ }
616
1580
  get canRedo() {
617
- return this._historyIndex < this._history.length - 1;
1581
+ return this._historyIndex < this._history.length;
618
1582
  }
619
1583
  undo() {
620
1584
  this._syncProps();
621
1585
  if (!this.canUndo)
622
1586
  return;
623
- this._historyIndex--;
624
- this._applyHistory(this._history[this._historyIndex]);
1587
+ // the selection the change found, so an undo lands where the change
1588
+ // was made — not where the one before it left the caret, which for the
1589
+ // first change of a session was the top of the file
1590
+ const step = this._history[--this._historyIndex];
1591
+ this._applyStep(step.from, step.after.length, step.before, {
1592
+ caret: step.caretBefore,
1593
+ anchor: step.anchorBefore,
1594
+ });
625
1595
  }
626
1596
  redo() {
627
1597
  this._syncProps();
628
1598
  if (!this.canRedo)
629
1599
  return;
630
- this._historyIndex++;
631
- this._applyHistory(this._history[this._historyIndex]);
1600
+ const step = this._history[this._historyIndex++];
1601
+ this._applyStep(step.from, step.before.length, step.after, step);
632
1602
  }
633
- _applyHistory(entry) {
1603
+ _applyStep(from, removed, lines, selection) {
1604
+ const view = this._viewBefore();
634
1605
  this._undoRun = null;
635
1606
  this._goalX = null;
636
- this._applyLines(entry.value.split('\n'), null);
637
- this._caret = clampPos(this._lines, entry.caret);
638
- this._anchor = clampPos(this._lines, entry.anchor);
639
- this._afterEdit(entry.value);
1607
+ this._replaceLines(from, removed, lines);
1608
+ this._caret = clampPos(this._lines, selection.caret);
1609
+ this._anchor = clampPos(this._lines, selection.anchor);
1610
+ this._afterEdit(view);
640
1611
  }
641
1612
  // --- editing core --------------------------------------------------------
642
1613
  _selectionRange() {
@@ -671,6 +1642,7 @@ export class CodeEditorNode extends Node {
671
1642
  this._syncProps();
672
1643
  if (this.props.readOnly)
673
1644
  return;
1645
+ const view = this._viewBefore();
674
1646
  const a = clampPos(this._lines, minPos(from, to));
675
1647
  const b = clampPos(this._lines, maxPos(from, to));
676
1648
  const insert = String(text).replace(/\r\n?/g, '\n');
@@ -680,70 +1652,123 @@ export class CodeEditorNode extends Node {
680
1652
  inserted[0] = before + inserted[0];
681
1653
  const caretCh = inserted[inserted.length - 1].length;
682
1654
  inserted[inserted.length - 1] += after;
683
- const newLines = this._lines.slice();
684
- newLines.splice(a.line, b.line - a.line + 1, ...inserted);
685
- const coalesce = run !== null &&
686
- run === this._undoRun &&
687
- this._historyIndex === this._history.length - 1;
688
- this._applyLines(newLines, {
689
- fromLine: a.line,
690
- removed: b.line - a.line + 1,
691
- inserted: inserted.length,
692
- });
693
- this._caret = clampPos(this._lines, {
694
- line: a.line + inserted.length - 1,
695
- ch: caretCh,
696
- });
697
- this._anchor = { ...this._caret };
1655
+ const caret = { line: a.line + inserted.length - 1, ch: caretCh };
1656
+ this._edit(a.line, b.line - a.line + 1, inserted, run, caret, { ...caret });
1657
+ this._caret = caret;
1658
+ this._anchor = { ...caret };
698
1659
  this._goalX = null;
699
- const value = this._lines.join('\n');
700
- if (coalesce) {
701
- this._history[this._historyIndex] = {
702
- value,
703
- caret: this._caret,
704
- anchor: this._anchor,
705
- };
706
- }
707
- else {
708
- this._pushHistory({ value, caret: this._caret, anchor: this._anchor });
1660
+ this._afterEdit(view);
1661
+ }
1662
+ /**
1663
+ * The one way the lines change: `removed` lines at `from` become `lines`,
1664
+ * recorded for undo under `run` with the selection the change leaves.
1665
+ * Narrowed first to the lines it really changes — a replacement that
1666
+ * leaves lines at either end as they were, a select-all and paste of the
1667
+ * same file or a formatter's output, touches only the ones between — and
1668
+ * a change that changes nothing is not one.
1669
+ */
1670
+ _edit(from, removed, lines, run, caret, anchor) {
1671
+ const text = this._lines;
1672
+ let head = 0;
1673
+ while (head < removed &&
1674
+ head < lines.length &&
1675
+ text[from + head] === lines[head]) {
1676
+ head++;
1677
+ }
1678
+ let tail = 0;
1679
+ while (tail < removed - head &&
1680
+ tail < lines.length - head &&
1681
+ text[from + removed - 1 - tail] === lines[lines.length - 1 - tail]) {
1682
+ tail++;
1683
+ }
1684
+ const gone = removed - head - tail;
1685
+ const added = lines.slice(head, lines.length - tail);
1686
+ if (gone > 0 || added.length > 0) {
1687
+ this._record(from + head, gone, added, run, caret, anchor);
1688
+ this._replaceLines(from + head, gone, added);
709
1689
  }
710
1690
  this._undoRun = run;
711
- this._afterEdit(value);
712
1691
  }
713
- _applyLines(newLines, edit) {
714
- if (edit) {
715
- // A tokenizer keeps the array `setLines` handed it (that is the
716
- // contract in `Tokenizer`) and is told about changes through `edit()`
717
- // alone, so an incremental edit has to *mutate* the live array —
718
- // swapping in a fresh one would leave every engine tokenizing the
719
- // pre-edit text at the new line numbers, painting a split line with
720
- // its neighbour's colours. Callers only touch the edited span, so
721
- // splicing that span across reproduces `newLines` exactly.
722
- this._lines.splice(edit.fromLine, edit.removed, ...newLines.slice(edit.fromLine, edit.fromLine + edit.inserted));
1692
+ /**
1693
+ * `removed` lines at `from` become `lines`, in the live array. A
1694
+ * tokenizer keeps the array `setLines` handed it (that is the contract in
1695
+ * `Tokenizer`) and is told about changes through `edit()` alone, so an
1696
+ * edit has to *mutate* it — swapping in a fresh one would leave every
1697
+ * engine tokenizing the pre-edit text at the new line numbers, painting a
1698
+ * split line with its neighbour's colours.
1699
+ */
1700
+ _replaceLines(from, removed, lines) {
1701
+ this._mapDiagnostics(from, removed, lines);
1702
+ // the widest line known going: what it was is the widest of the lines
1703
+ // still laid out — the one edited is laid out again before it is drawn
1704
+ let widestGoes = false;
1705
+ for (let i = from; i < from + removed && !widestGoes; i++) {
1706
+ const width = this._lineCache.get(i)?.layout?.width ?? 0;
1707
+ widestGoes = width > 0 && width >= this._widest;
1708
+ }
1709
+ spliceAll(this._lines, from, removed, lines);
1710
+ const edit = { fromLine: from, removed, inserted: lines.length };
1711
+ this._tokenizer()?.edit(edit);
1712
+ this._moveLineCache(edit);
1713
+ this._lastEdit = edit;
1714
+ if (widestGoes) {
1715
+ let widest = 0;
1716
+ for (const entry of this._lineCache.values()) {
1717
+ widest = Math.max(widest, entry.layout?.width ?? 0);
1718
+ }
1719
+ this._widest = widest;
723
1720
  }
724
- else {
725
- this._lines = newLines;
726
- }
727
- const tok = this._tokenizer();
728
- if (edit && tok) {
729
- tok.edit(edit);
730
- // line indices at and below the edit moved or changed; drop them
731
- for (const key of [...this._lineCache.keys()]) {
732
- if (key >= edit.fromLine)
733
- this._lineCache.delete(key);
1721
+ }
1722
+ /** Let go of the line entries farther than a quarter of the cache from
1723
+ * the lines in view — see `LINE_CACHE_MAX`. */
1724
+ _trimLineCache(first, last) {
1725
+ const reach = LINE_CACHE_MAX / 4;
1726
+ for (const line of this._lineCache.keys()) {
1727
+ if (line < first - reach || line > last + reach) {
1728
+ this._lineCache.delete(line);
734
1729
  }
735
- if (edit.removed !== edit.inserted)
736
- this._lineCache.clear();
737
1730
  }
738
- else {
739
- tok?.setLines(this._lines);
740
- this._lineCache.clear();
1731
+ }
1732
+ /**
1733
+ * The line cache across an edit: the edited lines' entries go, and the
1734
+ * ones after them move by the lines it added or took away. Every other
1735
+ * entry stays. An entry is checked against its line's text and tokens
1736
+ * whenever it is asked for (`_lineEntry`), so a line an edit re-coloured
1737
+ * — an opened comment, a closed string — is laid out again then, and a
1738
+ * line it left alone is not: typing on the first line of a file used to
1739
+ * lay out every line in view again on every keystroke.
1740
+ */
1741
+ _moveLineCache(edit) {
1742
+ const end = edit.fromLine + edit.removed;
1743
+ const shift = edit.inserted - edit.removed;
1744
+ if (shift === 0) {
1745
+ for (let line = edit.fromLine; line < end; line++) {
1746
+ this._dropLine(line);
1747
+ }
1748
+ return;
1749
+ }
1750
+ const moved = new Map();
1751
+ for (const [line, entry] of this._lineCache) {
1752
+ if (line < edit.fromLine)
1753
+ moved.set(line, entry);
1754
+ else if (line >= end)
1755
+ moved.set(line + shift, entry);
1756
+ else if (entry.layout instanceof ChunkedLayout) {
1757
+ this._chunkedStash = { styleKey: entry.styleKey, layout: entry.layout };
1758
+ }
741
1759
  }
1760
+ this._lineCache = moved;
742
1761
  }
743
- _afterEdit(value) {
744
- this._synced = value;
1762
+ _afterEdit(before) {
1763
+ // The whole text as one string is what `onChange`, `onSelectionChange`
1764
+ // and a controlled `value` speak, and joining fifty thousand lines is a
1765
+ // millisecond or two a keystroke — joined when one of them reads it.
1766
+ let joined = null;
1767
+ const value = () => (joined ??= this._lines.join('\n'));
1768
+ this._synced = this.props.value != null ? value() : null;
745
1769
  this._ensureCaretVisible();
746
- this._repaint();
1770
+ this._repaintSince(before, this._lastEdit);
1771
+ this._lastEdit = null;
747
1772
  this._fire('onChange', value);
748
1773
  this._fire('onSelectionChange', value);
749
1774
  }
@@ -754,21 +1779,23 @@ export class CodeEditorNode extends Node {
754
1779
  moveCaret(pos, extend) {
755
1780
  this._syncProps();
756
1781
  this.breakUndoRun();
1782
+ const view = this._viewBefore();
757
1783
  this._caret = clampPos(this._lines, pos);
758
1784
  if (!extend)
759
1785
  this._anchor = { ...this._caret };
760
1786
  this._ensureCaretVisible();
761
- this._repaint();
762
- this._fire('onSelectionChange', this.value);
1787
+ this._repaintSince(view, null);
1788
+ this._fire('onSelectionChange', () => this.value);
763
1789
  }
764
1790
  select(anchor, head) {
765
1791
  this._syncProps();
766
1792
  this.breakUndoRun();
1793
+ const view = this._viewBefore();
767
1794
  this._anchor = clampPos(this._lines, anchor);
768
1795
  this._caret = clampPos(this._lines, head);
769
1796
  this._ensureCaretVisible();
770
- this._repaint();
771
- this._fire('onSelectionChange', this.value);
1797
+ this._repaintSince(view, null);
1798
+ this._fire('onSelectionChange', () => this.value);
772
1799
  }
773
1800
  // `: this`, matching the base declaration core grew in #294 — a subclass
774
1801
  // narrowing it to void stops being structurally a DrawnNode.
@@ -875,19 +1902,13 @@ export class CodeEditorNode extends Node {
875
1902
  /** Line-rewriting commands share this: one undo step, selection kept on
876
1903
  * the same lines (clamped to their new lengths). */
877
1904
  _replaceAllPreservingSelection(newLines, fromLine, toLine) {
878
- const caret = this._caret;
879
- const anchor = this._anchor;
880
- this._applyLines(newLines, {
881
- fromLine,
882
- removed: toLine - fromLine + 1,
883
- inserted: toLine - fromLine + 1,
884
- });
885
- this._caret = clampPos(this._lines, caret);
886
- this._anchor = clampPos(this._lines, anchor);
887
- this._undoRun = null;
888
- const value = this._lines.join('\n');
889
- this._pushHistory({ value, caret: this._caret, anchor: this._anchor });
890
- this._afterEdit(value);
1905
+ const view = this._viewBefore();
1906
+ const caret = clampPos(newLines, this._caret);
1907
+ const anchor = clampPos(newLines, this._anchor);
1908
+ this._edit(fromLine, toLine - fromLine + 1, newLines.slice(fromLine, toLine + 1), null, caret, anchor);
1909
+ this._caret = caret;
1910
+ this._anchor = anchor;
1911
+ this._afterEdit(view);
891
1912
  }
892
1913
  // --- input: keyboard -----------------------------------------------------
893
1914
  /**
@@ -1139,8 +2160,9 @@ export class CodeEditorNode extends Node {
1139
2160
  const entry = this._lineEntry(line);
1140
2161
  let ch = 0;
1141
2162
  if (entry.layout) {
1142
- const cp = entry.layout.indexAt(this._goalX, 0);
1143
- ch = entry.map.toRaw(cpToUtf16(entry.display, cp));
2163
+ ch = entry.map.toRaw(entry.layout instanceof ChunkedLayout
2164
+ ? entry.layout.indexAtUtf16(this._goalX, 0)
2165
+ : cpToUtf16(entry.display, entry.layout.indexAt(this._goalX, 0)));
1144
2166
  }
1145
2167
  const goal = this._goalX; // moveCaret clears it; this move keeps it
1146
2168
  this.moveCaret({ line, ch }, extend);
@@ -1271,11 +2293,29 @@ export class CodeEditorNode extends Node {
1271
2293
  if (this._blinkTimer == null) {
1272
2294
  this._blinkTimer = startInterval(() => {
1273
2295
  this._caretOn = !this._caretOn;
1274
- this.root?.invalidate(false, this.abs, 'text');
2296
+ this._claimCaretRow();
1275
2297
  }, CARET_BLINK_MS);
1276
2298
  }
1277
2299
  this._repaint();
1278
2300
  }
2301
+ /** A blink changes the caret and nothing else: claim the text row it
2302
+ * is on rather than the editor, and `paint` draws that one line. The
2303
+ * whole row, so a caret x worked out a pixel off still falls inside. */
2304
+ _claimCaretRow() {
2305
+ const content = this._contentRect();
2306
+ const gutterW = this._gutterWidth();
2307
+ const top = content.y + this._caret.line * this._lineH - this._scrollY;
2308
+ const y = Math.max(content.y, Math.floor(top));
2309
+ const bottom = Math.min(content.y + content.height, Math.ceil(top + this._lineH));
2310
+ if (bottom <= y)
2311
+ return; // scrolled out of view
2312
+ this.invalidate(false, {
2313
+ x: content.x + gutterW,
2314
+ y,
2315
+ width: Math.max(0, content.width - gutterW),
2316
+ height: bottom - y,
2317
+ }, 'text');
2318
+ }
1279
2319
  handleBlur() {
1280
2320
  this._focused = false;
1281
2321
  this.breakUndoRun();
@@ -1286,22 +2326,34 @@ export class CodeEditorNode extends Node {
1286
2326
  // --- lifecycle -----------------------------------------------------------
1287
2327
  applyProps(nextProps, prevProps) {
1288
2328
  const before = prevProps ?? this.props;
2329
+ // the squiggles on screen, before the prop they came from is replaced
2330
+ const shown = this._diagnostics;
1289
2331
  super.applyProps(nextProps, prevProps);
1290
- if (nextProps.value != null)
2332
+ if (nextProps.value != null && String(nextProps.value) !== this._synced) {
2333
+ // the rows the new value changed, as an edit claims them
2334
+ const view = this._viewBefore();
1291
2335
  this._syncProps();
2336
+ this._repaintSince(view, this._lastEdit);
2337
+ this._lastEdit = null;
2338
+ }
1292
2339
  if (nextProps.language !== before.language) {
1293
2340
  this._tokenizer(); // re-resolves against the new props
1294
2341
  this._repaint();
1295
2342
  }
1296
2343
  if (nextProps.rows !== before.rows)
1297
2344
  this.invalidateMeasure('props');
1298
- if (nextProps.tokenStyles !== before.tokenStyles ||
1299
- nextProps.diagnostics !== before.diagnostics ||
1300
- nextProps.tabSize !== before.tabSize ||
1301
- nextProps.lineNumbers !== before.lineNumbers) {
2345
+ if (!sameTokenStyles(nextProps.tokenStyles, before.tokenStyles) ||
2346
+ nextProps.tabSize !== before.tabSize) {
1302
2347
  this._lineCache.clear();
2348
+ this._chunkedStash = null;
1303
2349
  this._repaint();
1304
2350
  }
2351
+ else if (nextProps.lineNumbers !== before.lineNumbers) {
2352
+ this._repaint(); // every column moves with the gutter; no layout does
2353
+ }
2354
+ else if (!sameDiagnostics(nextProps.diagnostics, before.diagnostics)) {
2355
+ this._repaintDiagnostics(shown, nextProps.diagnostics ?? []);
2356
+ }
1305
2357
  }
1306
2358
  destroySubtree() {
1307
2359
  stopInterval(this._blinkTimer);
@@ -1344,9 +2396,46 @@ export class CodeEditorNode extends Node {
1344
2396
  return true;
1345
2397
  };
1346
2398
  const dark = this._onDark();
1347
- const first = Math.max(0, Math.floor(this._scrollY / lineH));
1348
- const last = Math.min(this._lines.length - 1, Math.ceil((this._scrollY + content.height) / lineH));
2399
+ // What of a line this pass can show, in the line's own x: a band, a
2400
+ // squiggle or a caret is drawn over that and no further. A selection
2401
+ // across a minified line was one rectangle as wide as the line, and past
2402
+ // 32,767 device pixels an X11 coordinate does not fit its 16 bits — the
2403
+ // paint threw, and the window stopped painting.
2404
+ const reachFrom = this._scrollX - 8 * s;
2405
+ const reachTo = this._scrollX + content.width - gutterW + 8 * s;
2406
+ const within = (x0, x1) => {
2407
+ const a = Math.max(x0, reachFrom);
2408
+ const b = Math.min(x1, reachTo);
2409
+ return b > a ? [a, b] : null;
2410
+ };
2411
+ let first = Math.max(0, Math.floor(this._scrollY / lineH));
2412
+ let last = Math.min(this._lines.length - 1, Math.ceil((this._scrollY + content.height) / lineH));
2413
+ if (this._lineCache.size > LINE_CACHE_MAX)
2414
+ this._trimLineCache(first, last);
2415
+ // Only the lines this pass repaints, and one either side for ink that
2416
+ // overhangs its row: a scroll blit hands over the band it exposed, and
2417
+ // a blink the caret's row. The clip would throw the rest away after
2418
+ // they were laid out and drawn.
2419
+ const damage = this.paintDamage();
2420
+ if (damage) {
2421
+ const from = damage.y - content.y + this._scrollY;
2422
+ first = Math.max(first, Math.floor(from / lineH) - 1);
2423
+ last = Math.min(last, Math.ceil((from + damage.height) / lineH) + 1);
2424
+ }
1349
2425
  const lineY = (i) => content.y + i * lineH - this._scrollY;
2426
+ // A fill as tall as a line, on whole device pixels. A line is as tall as
2427
+ // its face says, a fraction of a pixel, and a backend rounds a fill's
2428
+ // fractional edges its own way: X11 cut a band's top and its height
2429
+ // separately, which left a row of background between two selected lines
2430
+ // every seventh or eighth line — at a row that moved with the scroll, so
2431
+ // a blit copied a seam a repaint did not draw. Edges rounded from the
2432
+ // same arithmetic meet whichever line they are shared by.
2433
+ const fillRow = (i, x0, x1) => {
2434
+ const left = Math.round(x0);
2435
+ const top = Math.round(lineY(i));
2436
+ const bottom = Math.round(lineY(i + 1));
2437
+ c.fillRect(left, top, Math.round(x1) - left, bottom - top);
2438
+ };
1350
2439
  // Everything the editor draws — gutter included — stays inside the
1351
2440
  // content box. The first and last visible rows are partial when
1352
2441
  // scrolled, and without this their overhang (up to a line each way)
@@ -1369,7 +2458,7 @@ export class CodeEditorNode extends Node {
1369
2458
  c.fillStyle =
1370
2459
  this._resolveColor(props.activeLineColor) ??
1371
2460
  (dark ? 'rgba(255,255,255,0.06)' : 'rgba(120,140,180,0.10)');
1372
- c.fillRect(content.x + gutterW, lineY(this._caret.line), content.width - gutterW, lineH);
2461
+ fillRow(this._caret.line, content.x + gutterW, content.x + content.width);
1373
2462
  }
1374
2463
  // selection bands
1375
2464
  if (this._hasSelection()) {
@@ -1385,7 +2474,9 @@ export class CodeEditorNode extends Node {
1385
2474
  const endX = i === b.line
1386
2475
  ? this._caretX({ line: i, ch: b.ch })
1387
2476
  : (entry.layout?.width ?? 0) + this._charW * 0.5; // the newline
1388
- c.fillRect(textX + startX, lineY(i), Math.max(2 * s, endX - startX), lineH);
2477
+ const band = within(startX, startX + Math.max(2 * s, endX - startX));
2478
+ if (band)
2479
+ fillRow(i, textX + band[0], textX + band[1]);
1389
2480
  }
1390
2481
  }
1391
2482
  // matching brackets
@@ -1400,7 +2491,9 @@ export class CodeEditorNode extends Node {
1400
2491
  continue;
1401
2492
  const x0 = this._caretX(pos);
1402
2493
  const x1 = this._caretX({ line: pos.line, ch: pos.ch + 1 });
1403
- c.fillRect(textX + x0, lineY(pos.line), Math.max(s, x1 - x0), lineH);
2494
+ const box = within(x0, x0 + Math.max(s, x1 - x0));
2495
+ if (box)
2496
+ fillRow(pos.line, textX + box[0], textX + box[1]);
1404
2497
  }
1405
2498
  }
1406
2499
  }
@@ -1423,13 +2516,53 @@ export class CodeEditorNode extends Node {
1423
2516
  }
1424
2517
  }
1425
2518
  else {
1426
- for (let i = first; i <= last; i++) {
2519
+ // across, what the text region shows of a line, in its own x — and
2520
+ // of that, what this pass repaints, give or take a character of ink
2521
+ // past a glyph's advance: a thumb's strip is a column at the right
2522
+ // edge that most lines never reach
2523
+ let shownFrom = this._scrollX;
2524
+ let shownTo = this._scrollX + content.width - gutterW;
2525
+ if (damage) {
2526
+ shownFrom = Math.max(shownFrom, damage.x - textX - this._charW);
2527
+ shownTo = Math.min(shownTo, damage.x + damage.width - textX + this._charW);
2528
+ }
2529
+ for (let i = first; shownFrom < shownTo && i <= last; i++) {
1427
2530
  const entry = this._lineEntry(i);
1428
- entry.layout?.draw(ctx, textX, lineY(i) + (lineH - (entry.layout.height || lineH)) / 2);
2531
+ const layout = entry.layout;
2532
+ if (!layout)
2533
+ continue;
2534
+ // a long line's width past what is laid out is a guess, and one
2535
+ // short of the truth would skip text that is there: its pieces are
2536
+ // placed by laying out the ones before them (`drawSpan`)
2537
+ if (!(layout instanceof ChunkedLayout) && layout.width < shownFrom) {
2538
+ continue;
2539
+ }
2540
+ // Every row's text on the base face's baseline. Centred by its own
2541
+ // height, a line with an emoji in it — laid out taller, in a
2542
+ // fallback face — sat a pixel off every other line.
2543
+ const top = lineY(i);
2544
+ const base = this._baseline;
2545
+ const own = base == null ? null : baselineOf(layout);
2546
+ const y = own == null
2547
+ ? top + (lineH - (layout.height || lineH)) / 2
2548
+ : top + base - own;
2549
+ // …and a row laid out taller than the base keeps its ink in its
2550
+ // own row: past it, the next row's pixels are ones an edit to this
2551
+ // row does not repaint, and an old descender stayed there
2552
+ const tall = layout.height > lineH + 0.5 &&
2553
+ clipTo(content.x + gutterW, top, content.width - gutterW, lineH);
2554
+ if (layout instanceof ChunkedLayout) {
2555
+ layout.drawSpan(ctx, textX, y, shownFrom, shownTo, base == null ? null : top + base);
2556
+ }
2557
+ else {
2558
+ layout.draw(ctx, textX, y);
2559
+ }
2560
+ if (tall)
2561
+ c.restore();
1429
2562
  }
1430
2563
  }
1431
2564
  // diagnostics
1432
- const diagnostics = props.diagnostics ?? [];
2565
+ const diagnostics = this._currentDiagnostics();
1433
2566
  for (const d of diagnostics) {
1434
2567
  const color = SEVERITY_COLORS[d.severity ?? 'error'] ?? '#e5484d';
1435
2568
  const from = clampPos(this._lines, d.from);
@@ -1444,48 +2577,65 @@ export class CodeEditorNode extends Node {
1444
2577
  // band, so a squiggle at the band's bottom floats visibly below
1445
2578
  // short text — measure where the drawn layout actually ends
1446
2579
  const entry = this._lineEntry(i);
1447
- const glyphBottom = lineY(i) +
1448
- (lineH - (entry.layout?.height || lineH)) / 2 +
1449
- (entry.layout?.height || lineH);
1450
- const y = Math.min(lineY(i) + lineH - 2 * s, glyphBottom - s);
2580
+ const glyphBottom = this._baseline != null
2581
+ ? lineY(i) + this._textBottom
2582
+ : lineY(i) +
2583
+ (lineH - (entry.layout?.height || lineH)) / 2 +
2584
+ (entry.layout?.height || lineH);
2585
+ // …and inside its own row: the zigzag is 2px tall and stroked 1px
2586
+ // wide, so it inks half a stroke past its path either way. Ink past
2587
+ // the row's bottom is in the next row's pixels, which an edit to
2588
+ // this row does not repaint — the old squiggle left a speck there.
2589
+ const y = Math.min(lineY(i) + lineH - 2.5 * s, glyphBottom - s);
1451
2590
  const width = Math.max(this._charW * 0.6, x1 - x0);
1452
2591
  if (typeof c.beginPath === 'function' &&
1453
2592
  typeof c.moveTo === 'function' &&
1454
2593
  typeof c.lineTo === 'function' &&
1455
2594
  typeof c.stroke === 'function') {
1456
- c.beginPath();
1457
2595
  // a hairline zig-zag, 4px a step and 2px tall, on the panel's grid
2596
+ // — the steps in reach, each where it would be if all were drawn
1458
2597
  const step = 4 * s;
1459
- let up = true;
1460
- for (let x = 0; x <= width; x += step) {
1461
- const px = textX + x0 + x;
1462
- const py = y + (up ? 0 : 2 * s);
1463
- if (x === 0)
1464
- c.moveTo(px, py);
1465
- else
1466
- c.lineTo(px, py);
1467
- up = !up;
2598
+ const from = Math.max(0, Math.floor((reachFrom - x0) / step));
2599
+ const to = Math.min(Math.floor(width / step), Math.ceil((reachTo - x0) / step));
2600
+ if (to > from) {
2601
+ c.beginPath();
2602
+ for (let k = from; k <= to; k++) {
2603
+ const px = textX + x0 + k * step;
2604
+ const py = y + (k % 2 === 0 ? 0 : 2 * s);
2605
+ if (k === from)
2606
+ c.moveTo(px, py);
2607
+ else
2608
+ c.lineTo(px, py);
2609
+ }
2610
+ c.strokeStyle = color;
2611
+ c.lineWidth = s;
2612
+ // mitred, the zig-zag's corners reach half a pixel past the
2613
+ // stroke — into the next row, at the bottom ones
2614
+ c.lineJoin = 'round';
2615
+ c.stroke();
1468
2616
  }
1469
- c.strokeStyle = color;
1470
- c.lineWidth = s;
1471
- c.stroke();
1472
2617
  }
1473
2618
  else {
1474
- c.fillStyle = color;
1475
- c.fillRect(textX + x0, y + s, width, s);
2619
+ const line = within(x0, x0 + width);
2620
+ if (line) {
2621
+ c.fillStyle = color;
2622
+ c.fillRect(textX + line[0], y + s, line[1] - line[0], s);
2623
+ }
1476
2624
  }
1477
2625
  }
1478
2626
  }
1479
2627
  // caret — 2 logical px, in the (theme-resolved) text colour
1480
- if (this._focused && this._caretOn) {
1481
- const x = this._caretX(this._caret);
2628
+ const caretAt = this._focused && this._caretOn ? this._caretX(this._caret) : 0;
2629
+ if (this._focused && this._caretOn && within(caretAt, caretAt + 2 * s)) {
2630
+ const x = caretAt;
1482
2631
  c.fillStyle = this._resolveColor(props.caretColor) ?? baseColor;
1483
2632
  c.fillRect(Math.round(textX + x), lineY(this._caret.line) + s, 2 * s, lineH - 2 * s);
1484
2633
  }
1485
2634
  if (textClip)
1486
2635
  c.restore();
1487
- // gutter numbers: inside the content clip, outside the text clip
1488
- if (gutterW > 0) {
2636
+ // gutter numbers: inside the content clip, outside the text clip — and
2637
+ // not for a pass that lies to the right of them
2638
+ if (gutterW > 0 && !(damage && damage.x >= content.x + gutterW)) {
1489
2639
  const fonts = this._fonts();
1490
2640
  if (fonts) {
1491
2641
  const dim = this._resolveColor(props.gutterColor) ??
@@ -1522,6 +2672,21 @@ export class CodeEditorNode extends Node {
1522
2672
  c.fillStyle = 'rgba(128,128,128,0.35)';
1523
2673
  c.fillRect(content.x + gutterW + xOff, content.y + content.height - 4 * s, thumbW, 3 * s);
1524
2674
  }
2675
+ // A line this pass laid out wider than the view gives the text a
2676
+ // horizontal thumb, where there was none when the scroll decided what
2677
+ // to blit: the frame repaints the band it exposed and not the strip
2678
+ // the thumb is drawn in, so the strip is claimed for the next one.
2679
+ if (maxX > 0 !== this._thumbAcross) {
2680
+ this._thumbAcross = maxX > 0;
2681
+ if (damage) {
2682
+ this.invalidate(false, {
2683
+ x: content.x,
2684
+ y: content.y + content.height - 4 * s,
2685
+ width: content.width,
2686
+ height: 4 * s,
2687
+ }, 'scroll');
2688
+ }
2689
+ }
1525
2690
  if (outerClip)
1526
2691
  c.restore();
1527
2692
  }