@react-x11/components 0.9.0 → 0.10.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.
- package/README.md +10 -4
- package/dist/charts/node.d.ts +11 -1
- package/dist/charts/node.d.ts.map +1 -1
- package/dist/charts/node.js +44 -14
- package/dist/charts/node.js.map +1 -1
- package/dist/charts/render.d.ts +12 -3
- package/dist/charts/render.d.ts.map +1 -1
- package/dist/charts/render.js +94 -25
- package/dist/charts/render.js.map +1 -1
- package/dist/code/index.d.ts.map +1 -1
- package/dist/code/index.js +3 -1
- package/dist/code/index.js.map +1 -1
- package/dist/code-editor/completion.d.ts.map +1 -1
- package/dist/code-editor/completion.js +3 -2
- package/dist/code-editor/completion.js.map +1 -1
- package/dist/code-editor/index.d.ts.map +1 -1
- package/dist/code-editor/index.js +19 -0
- package/dist/code-editor/index.js.map +1 -1
- package/dist/code-editor/node.d.ts +177 -7
- package/dist/code-editor/node.d.ts.map +1 -1
- package/dist/code-editor/node.js +1329 -164
- package/dist/code-editor/node.js.map +1 -1
- package/dist/code-language/languages/glsl.d.ts.map +1 -1
- package/dist/code-language/languages/glsl.js +10 -2
- package/dist/code-language/languages/glsl.js.map +1 -1
- package/dist/code-language/languages/javascript.d.ts.map +1 -1
- package/dist/code-language/languages/javascript.js +18 -3
- package/dist/code-language/languages/javascript.js.map +1 -1
- package/dist/code-language/languages/json.d.ts.map +1 -1
- package/dist/code-language/languages/json.js +10 -2
- package/dist/code-language/languages/json.js.map +1 -1
- package/dist/code-language/languages/shell.d.ts.map +1 -1
- package/dist/code-language/languages/shell.js +10 -2
- package/dist/code-language/languages/shell.js.map +1 -1
- package/dist/code-language/languages/sql.d.ts.map +1 -1
- package/dist/code-language/languages/sql.js +20 -2
- package/dist/code-language/languages/sql.js.map +1 -1
- package/dist/code-language/stream.d.ts +22 -0
- package/dist/code-language/stream.d.ts.map +1 -1
- package/dist/code-language/stream.js +259 -9
- package/dist/code-language/stream.js.map +1 -1
- package/dist/code-language/types.d.ts +5 -4
- package/dist/code-language/types.d.ts.map +1 -1
- package/dist/codeblock/index.d.ts +13 -2
- package/dist/codeblock/index.d.ts.map +1 -1
- package/dist/codeblock/index.js +20 -3
- package/dist/codeblock/index.js.map +1 -1
- package/dist/flow/draw.d.ts +50 -0
- package/dist/flow/draw.d.ts.map +1 -1
- package/dist/flow/draw.js +132 -33
- package/dist/flow/draw.js.map +1 -1
- package/dist/flow/gl/color.d.ts +13 -0
- package/dist/flow/gl/color.d.ts.map +1 -0
- package/dist/flow/gl/color.js +82 -0
- package/dist/flow/gl/color.js.map +1 -0
- package/dist/flow/gl/index.d.ts +60 -0
- package/dist/flow/gl/index.d.ts.map +1 -0
- package/dist/flow/gl/index.js +283 -0
- package/dist/flow/gl/index.js.map +1 -0
- package/dist/flow/gl/pack.d.ts +133 -0
- package/dist/flow/gl/pack.d.ts.map +1 -0
- package/dist/flow/gl/pack.js +408 -0
- package/dist/flow/gl/pack.js.map +1 -0
- package/dist/flow/gl/renderer.d.ts +95 -0
- package/dist/flow/gl/renderer.d.ts.map +1 -0
- package/dist/flow/gl/renderer.js +459 -0
- package/dist/flow/gl/renderer.js.map +1 -0
- package/dist/flow/gl/shaders.d.ts +79 -0
- package/dist/flow/gl/shaders.d.ts.map +1 -0
- package/dist/flow/gl/shaders.js +346 -0
- package/dist/flow/gl/shaders.js.map +1 -0
- package/dist/flow/gl/text.d.ts +318 -0
- package/dist/flow/gl/text.d.ts.map +1 -0
- package/dist/flow/gl/text.js +1103 -0
- package/dist/flow/gl/text.js.map +1 -0
- package/dist/flow/index.d.ts +1 -1
- package/dist/flow/index.d.ts.map +1 -1
- package/dist/flow/index.js +619 -64
- package/dist/flow/index.js.map +1 -1
- package/dist/flow/node.d.ts +621 -72
- package/dist/flow/node.d.ts.map +1 -1
- package/dist/flow/node.js +2253 -1018
- package/dist/flow/node.js.map +1 -1
- package/dist/flow/paint.d.ts +35 -0
- package/dist/flow/paint.d.ts.map +1 -0
- package/dist/flow/paint.js +393 -0
- package/dist/flow/paint.js.map +1 -0
- package/dist/flow/paths.d.ts +14 -0
- package/dist/flow/paths.d.ts.map +1 -1
- package/dist/flow/paths.js +45 -19
- package/dist/flow/paths.js.map +1 -1
- package/dist/flow/scene.d.ts +584 -0
- package/dist/flow/scene.d.ts.map +1 -0
- package/dist/flow/scene.js +1453 -0
- package/dist/flow/scene.js.map +1 -0
- package/dist/flow/types.d.ts +111 -1
- package/dist/flow/types.d.ts.map +1 -1
- package/dist/html/controls.d.ts +28 -0
- package/dist/html/controls.d.ts.map +1 -1
- package/dist/html/controls.js +52 -2
- package/dist/html/controls.js.map +1 -1
- package/dist/html/css/calc.d.ts +14 -0
- package/dist/html/css/calc.d.ts.map +1 -0
- package/dist/html/css/calc.js +344 -0
- package/dist/html/css/calc.js.map +1 -0
- package/dist/html/css/cascade.d.ts +240 -6
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +1327 -66
- package/dist/html/css/cascade.js.map +1 -1
- package/dist/html/css/color.d.ts +23 -0
- package/dist/html/css/color.d.ts.map +1 -0
- package/dist/html/css/color.js +740 -0
- package/dist/html/css/color.js.map +1 -0
- package/dist/html/css/content.d.ts +92 -0
- package/dist/html/css/content.d.ts.map +1 -0
- package/dist/html/css/content.js +388 -0
- package/dist/html/css/content.js.map +1 -0
- package/dist/html/css/counter-styles.d.ts +76 -0
- package/dist/html/css/counter-styles.d.ts.map +1 -0
- package/dist/html/css/counter-styles.js +893 -0
- package/dist/html/css/counter-styles.js.map +1 -0
- package/dist/html/css/decode.d.ts +12 -0
- package/dist/html/css/decode.d.ts.map +1 -0
- package/dist/html/css/decode.js +116 -0
- package/dist/html/css/decode.js.map +1 -0
- package/dist/html/css/parse.d.ts +110 -10
- package/dist/html/css/parse.d.ts.map +1 -1
- package/dist/html/css/parse.js +1473 -180
- package/dist/html/css/parse.js.map +1 -1
- package/dist/html/css/style.d.ts +498 -15
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +3698 -206
- package/dist/html/css/style.js.map +1 -1
- package/dist/html/css/ua.d.ts.map +1 -1
- package/dist/html/css/ua.js +57 -11
- package/dist/html/css/ua.js.map +1 -1
- package/dist/html/css/values.d.ts +37 -3
- package/dist/html/css/values.d.ts.map +1 -1
- package/dist/html/css/values.js +122 -48
- package/dist/html/css/values.js.map +1 -1
- package/dist/html/css/vars.d.ts +52 -0
- package/dist/html/css/vars.d.ts.map +1 -0
- package/dist/html/css/vars.js +358 -0
- package/dist/html/css/vars.js.map +1 -0
- package/dist/html/dom.d.ts +30 -4
- package/dist/html/dom.d.ts.map +1 -1
- package/dist/html/dom.js +278 -17
- package/dist/html/dom.js.map +1 -1
- package/dist/html/fonts.d.ts +106 -0
- package/dist/html/fonts.d.ts.map +1 -0
- package/dist/html/fonts.js +565 -0
- package/dist/html/fonts.js.map +1 -0
- package/dist/html/gif.d.ts +15 -0
- package/dist/html/gif.d.ts.map +1 -0
- package/dist/html/gif.js +229 -0
- package/dist/html/gif.js.map +1 -0
- package/dist/html/index.d.ts +50 -11
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +115 -9
- package/dist/html/index.js.map +1 -1
- package/dist/html/layout/block.d.ts +226 -3
- package/dist/html/layout/block.d.ts.map +1 -1
- package/dist/html/layout/block.js +2937 -197
- package/dist/html/layout/block.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +340 -17
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +1714 -182
- package/dist/html/layout/boxes.js.map +1 -1
- package/dist/html/layout/cache.d.ts +58 -0
- package/dist/html/layout/cache.d.ts.map +1 -0
- package/dist/html/layout/cache.js +224 -0
- package/dist/html/layout/cache.js.map +1 -0
- package/dist/html/layout/collapse.d.ts +45 -0
- package/dist/html/layout/collapse.d.ts.map +1 -0
- package/dist/html/layout/collapse.js +246 -0
- package/dist/html/layout/collapse.js.map +1 -0
- package/dist/html/layout/css-grid.d.ts +7 -0
- package/dist/html/layout/css-grid.d.ts.map +1 -0
- package/dist/html/layout/css-grid.js +900 -0
- package/dist/html/layout/css-grid.js.map +1 -0
- package/dist/html/layout/flex.d.ts.map +1 -1
- package/dist/html/layout/flex.js +1025 -89
- package/dist/html/layout/flex.js.map +1 -1
- package/dist/html/layout/floats.d.ts +45 -6
- package/dist/html/layout/floats.d.ts.map +1 -1
- package/dist/html/layout/floats.js +113 -14
- package/dist/html/layout/floats.js.map +1 -1
- package/dist/html/layout/grid-lines.d.ts +39 -0
- package/dist/html/layout/grid-lines.d.ts.map +1 -0
- package/dist/html/layout/grid-lines.js +119 -0
- package/dist/html/layout/grid-lines.js.map +1 -0
- package/dist/html/layout/grid.d.ts +53 -0
- package/dist/html/layout/grid.d.ts.map +1 -0
- package/dist/html/layout/grid.js +153 -0
- package/dist/html/layout/grid.js.map +1 -0
- package/dist/html/layout/inline.d.ts +151 -1
- package/dist/html/layout/inline.d.ts.map +1 -1
- package/dist/html/layout/inline.js +3375 -174
- package/dist/html/layout/inline.js.map +1 -1
- package/dist/html/layout/shaping.d.ts +6 -0
- package/dist/html/layout/shaping.d.ts.map +1 -0
- package/dist/html/layout/shaping.js +100 -0
- package/dist/html/layout/shaping.js.map +1 -0
- package/dist/html/layout/table.d.ts +3 -0
- package/dist/html/layout/table.d.ts.map +1 -1
- package/dist/html/layout/table.js +681 -175
- package/dist/html/layout/table.js.map +1 -1
- package/dist/html/layout/tracks.d.ts +44 -0
- package/dist/html/layout/tracks.d.ts.map +1 -0
- package/dist/html/layout/tracks.js +349 -0
- package/dist/html/layout/tracks.js.map +1 -0
- package/dist/html/node.d.ts +169 -4
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +1331 -100
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts +92 -3
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +3503 -141
- package/dist/html/paint.js.map +1 -1
- package/dist/html/resources.d.ts +60 -14
- package/dist/html/resources.d.ts.map +1 -1
- package/dist/html/resources.js +122 -36
- package/dist/html/resources.js.map +1 -1
- package/dist/html/surfaces.d.ts +23 -0
- package/dist/html/surfaces.d.ts.map +1 -0
- package/dist/html/surfaces.js +68 -0
- package/dist/html/surfaces.js.map +1 -0
- package/dist/html/svg.d.ts +80 -0
- package/dist/html/svg.d.ts.map +1 -0
- package/dist/html/svg.js +435 -0
- package/dist/html/svg.js.map +1 -0
- package/dist/html/url.d.ts +23 -0
- package/dist/html/url.d.ts.map +1 -0
- package/dist/html/url.js +80 -0
- package/dist/html/url.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/collate.d.ts +3 -0
- package/dist/internal/collate.d.ts.map +1 -0
- package/dist/internal/collate.js +11 -0
- package/dist/internal/collate.js.map +1 -0
- package/dist/internal/markdown/ast.d.ts +11 -0
- package/dist/internal/markdown/ast.d.ts.map +1 -1
- package/dist/internal/markdown/parse.d.ts.map +1 -1
- package/dist/internal/markdown/parse.js +109 -8
- package/dist/internal/markdown/parse.js.map +1 -1
- package/dist/internal/prewarm.d.ts +18 -0
- package/dist/internal/prewarm.d.ts.map +1 -0
- package/dist/internal/prewarm.js +42 -0
- package/dist/internal/prewarm.js.map +1 -0
- package/dist/internal/sdf.d.ts +20 -0
- package/dist/internal/sdf.d.ts.map +1 -0
- package/dist/internal/sdf.js +216 -0
- package/dist/internal/sdf.js.map +1 -0
- package/dist/internal/splice.d.ts +12 -0
- package/dist/internal/splice.d.ts.map +1 -0
- package/dist/internal/splice.js +23 -0
- package/dist/internal/splice.js.map +1 -0
- package/dist/internal/window.d.ts +12 -0
- package/dist/internal/window.d.ts.map +1 -1
- package/dist/internal/window.js +23 -6
- package/dist/internal/window.js.map +1 -1
- package/dist/maps/anchors.d.ts +6 -1
- package/dist/maps/anchors.d.ts.map +1 -1
- package/dist/maps/anchors.js +260 -1
- package/dist/maps/anchors.js.map +1 -1
- package/dist/maps/controller.d.ts +57 -0
- package/dist/maps/controller.d.ts.map +1 -1
- package/dist/maps/controller.js +97 -18
- package/dist/maps/controller.js.map +1 -1
- package/dist/maps/gl/buckets.d.ts +5 -1
- package/dist/maps/gl/buckets.d.ts.map +1 -1
- package/dist/maps/gl/buckets.js +5 -2
- package/dist/maps/gl/buckets.js.map +1 -1
- package/dist/maps/gl/build-worker.d.ts +2 -0
- package/dist/maps/gl/build-worker.d.ts.map +1 -1
- package/dist/maps/gl/build-worker.js +1 -1
- package/dist/maps/gl/build-worker.js.map +1 -1
- package/dist/maps/gl/cover.d.ts +23 -4
- package/dist/maps/gl/cover.d.ts.map +1 -1
- package/dist/maps/gl/cover.js +61 -48
- package/dist/maps/gl/cover.js.map +1 -1
- package/dist/maps/gl/placement.d.ts +21 -8
- package/dist/maps/gl/placement.d.ts.map +1 -1
- package/dist/maps/gl/placement.js +133 -58
- package/dist/maps/gl/placement.js.map +1 -1
- package/dist/maps/gl/renderer.d.ts +36 -4
- package/dist/maps/gl/renderer.d.ts.map +1 -1
- package/dist/maps/gl/renderer.js +81 -24
- package/dist/maps/gl/renderer.js.map +1 -1
- package/dist/maps/gl/shaders.d.ts +18 -17
- package/dist/maps/gl/shaders.d.ts.map +1 -1
- package/dist/maps/gl/shaders.js +34 -32
- package/dist/maps/gl/shaders.js.map +1 -1
- package/dist/maps/gl/store.d.ts +10 -0
- package/dist/maps/gl/store.d.ts.map +1 -1
- package/dist/maps/gl/store.js +47 -4
- package/dist/maps/gl/store.js.map +1 -1
- package/dist/maps/gl/text.d.ts +153 -24
- package/dist/maps/gl/text.d.ts.map +1 -1
- package/dist/maps/gl/text.js +379 -63
- package/dist/maps/gl/text.js.map +1 -1
- package/dist/maps/gl/view.d.ts +293 -2
- package/dist/maps/gl/view.d.ts.map +1 -1
- package/dist/maps/gl/view.js +317 -82
- package/dist/maps/gl/view.js.map +1 -1
- package/dist/maps/icons.d.ts +32 -0
- package/dist/maps/icons.d.ts.map +1 -0
- package/dist/maps/icons.js +151 -0
- package/dist/maps/icons.js.map +1 -0
- package/dist/maps/index.d.ts +2 -0
- package/dist/maps/index.d.ts.map +1 -1
- package/dist/maps/index.js +2 -0
- package/dist/maps/index.js.map +1 -1
- package/dist/maps/labels.d.ts +56 -4
- package/dist/maps/labels.d.ts.map +1 -1
- package/dist/maps/labels.js +213 -46
- package/dist/maps/labels.js.map +1 -1
- package/dist/maps/node.d.ts +17 -0
- package/dist/maps/node.d.ts.map +1 -1
- package/dist/maps/node.js +64 -30
- package/dist/maps/node.js.map +1 -1
- package/dist/maps/proj.d.ts +48 -0
- package/dist/maps/proj.d.ts.map +1 -1
- package/dist/maps/proj.js +73 -0
- package/dist/maps/proj.js.map +1 -1
- package/dist/maps/sources.d.ts +5 -3
- package/dist/maps/sources.d.ts.map +1 -1
- package/dist/maps/sources.js +5 -3
- package/dist/maps/sources.js.map +1 -1
- package/dist/maps/style.d.ts +35 -0
- package/dist/maps/style.d.ts.map +1 -1
- package/dist/maps/style.js.map +1 -1
- package/dist/maps/styles.d.ts +12 -0
- package/dist/maps/styles.d.ts.map +1 -1
- package/dist/maps/styles.js +120 -6
- package/dist/maps/styles.js.map +1 -1
- package/dist/maps/tiles.d.ts +31 -11
- package/dist/maps/tiles.d.ts.map +1 -1
- package/dist/maps/tiles.js +61 -27
- package/dist/maps/tiles.js.map +1 -1
- package/dist/maps/types.d.ts +28 -4
- package/dist/maps/types.d.ts.map +1 -1
- package/dist/markdown/index.d.ts.map +1 -1
- package/dist/markdown/index.js +112 -43
- package/dist/markdown/index.js.map +1 -1
- package/dist/markdown/spans.d.ts.map +1 -1
- package/dist/markdown/spans.js +6 -0
- package/dist/markdown/spans.js.map +1 -1
- package/dist/qml/containers.d.ts.map +1 -1
- package/dist/qml/containers.js +6 -1
- package/dist/qml/containers.js.map +1 -1
- package/dist/qml/interaction.d.ts.map +1 -1
- package/dist/qml/interaction.js +38 -5
- package/dist/qml/interaction.js.map +1 -1
- package/dist/qml/layouts.d.ts.map +1 -1
- package/dist/qml/layouts.js +21 -6
- package/dist/qml/layouts.js.map +1 -1
- package/dist/qml/qtquick.d.ts.map +1 -1
- package/dist/qml/qtquick.js +9 -2
- package/dist/qml/qtquick.js.map +1 -1
- package/dist/qml/react.d.ts.map +1 -1
- package/dist/qml/react.js +9 -7
- package/dist/qml/react.js.map +1 -1
- package/dist/reorder/clock.d.ts +7 -0
- package/dist/reorder/clock.d.ts.map +1 -0
- package/dist/reorder/clock.js +26 -0
- package/dist/reorder/clock.js.map +1 -0
- package/dist/reorder/index.d.ts +25 -3
- package/dist/reorder/index.d.ts.map +1 -1
- package/dist/reorder/index.js +64 -19
- package/dist/reorder/index.js.map +1 -1
- package/dist/rich-text-editor/index.d.ts +0 -11
- package/dist/rich-text-editor/index.d.ts.map +1 -1
- package/dist/rich-text-editor/index.js +35 -1
- package/dist/rich-text-editor/index.js.map +1 -1
- package/dist/rich-text-editor/keymap.d.ts.map +1 -1
- package/dist/rich-text-editor/keymap.js +2 -1
- package/dist/rich-text-editor/keymap.js.map +1 -1
- package/dist/rich-text-editor/keys.d.ts +17 -6
- package/dist/rich-text-editor/keys.d.ts.map +1 -1
- package/dist/rich-text-editor/keys.js +135 -26
- package/dist/rich-text-editor/keys.js.map +1 -1
- package/dist/rich-text-editor/marks.d.ts +9 -0
- package/dist/rich-text-editor/marks.d.ts.map +1 -0
- package/dist/rich-text-editor/marks.js +83 -0
- package/dist/rich-text-editor/marks.js.map +1 -0
- package/dist/rich-text-editor/render.d.ts.map +1 -1
- package/dist/rich-text-editor/render.js +28 -10
- package/dist/rich-text-editor/render.js.map +1 -1
- package/dist/rich-text-editor/toolbar.d.ts.map +1 -1
- package/dist/rich-text-editor/toolbar.js +1 -1
- package/dist/rich-text-editor/toolbar.js.map +1 -1
- package/dist/rich-text-editor/virtual.d.ts.map +1 -1
- package/dist/rich-text-editor/virtual.js +17 -0
- package/dist/rich-text-editor/virtual.js.map +1 -1
- package/dist/richtext/node.d.ts +46 -2
- package/dist/richtext/node.d.ts.map +1 -1
- package/dist/richtext/node.js +187 -4
- package/dist/richtext/node.js.map +1 -1
- package/dist/richtext/runs.d.ts +13 -8
- package/dist/richtext/runs.d.ts.map +1 -1
- package/dist/richtext/runs.js +18 -8
- package/dist/richtext/runs.js.map +1 -1
- package/dist/table/index.d.ts.map +1 -1
- package/dist/table/index.js +23 -0
- package/dist/table/index.js.map +1 -1
- package/dist/table/rows.d.ts +3 -2
- package/dist/table/rows.d.ts.map +1 -1
- package/dist/table/rows.js +20 -6
- package/dist/table/rows.js.map +1 -1
- package/dist/terminal/index.d.ts.map +1 -1
- package/dist/terminal/index.js +4 -0
- package/dist/terminal/index.js.map +1 -1
- package/dist/terminal/vt/pty.d.ts +13 -2
- package/dist/terminal/vt/pty.d.ts.map +1 -1
- package/dist/terminal/vt/pty.js +18 -3
- package/dist/terminal/vt/pty.js.map +1 -1
- package/dist/terminal-output/index.d.ts.map +1 -1
- package/dist/terminal-output/index.js +6 -1
- package/dist/terminal-output/index.js.map +1 -1
- package/dist/three/canvas.d.ts.map +1 -1
- package/dist/three/canvas.js +85 -7
- package/dist/three/canvas.js.map +1 -1
- package/dist/three/events.d.ts +22 -4
- package/dist/three/events.d.ts.map +1 -1
- package/dist/three/events.js +34 -4
- package/dist/three/events.js.map +1 -1
- package/dist/tree/index.d.ts.map +1 -1
- package/dist/tree/index.js +17 -0
- package/dist/tree/index.js.map +1 -1
- package/package.json +42 -33
- package/src/charts/node.ts +62 -26
- package/src/charts/render.ts +120 -30
- package/src/code/index.ts +3 -1
- package/src/code-editor/completion.ts +3 -2
- package/src/code-editor/index.ts +19 -0
- package/src/code-editor/node.ts +1574 -184
- package/src/code-language/languages/glsl.ts +12 -2
- package/src/code-language/languages/javascript.ts +21 -4
- package/src/code-language/languages/json.ts +12 -2
- package/src/code-language/languages/shell.ts +12 -2
- package/src/code-language/languages/sql.ts +22 -2
- package/src/code-language/stream.ts +257 -17
- package/src/code-language/types.ts +5 -4
- package/src/codeblock/index.ts +20 -3
- package/src/flow/draw.ts +167 -41
- package/src/flow/gl/color.ts +85 -0
- package/src/flow/gl/index.ts +342 -0
- package/src/flow/gl/pack.ts +585 -0
- package/src/flow/gl/renderer.ts +679 -0
- package/src/flow/gl/shaders.ts +371 -0
- package/src/flow/gl/text.ts +1295 -0
- package/src/flow/index.ts +794 -67
- package/src/flow/node.ts +2496 -1256
- package/src/flow/paint.ts +511 -0
- package/src/flow/paths.ts +80 -15
- package/src/flow/scene.ts +2199 -0
- package/src/flow/types.ts +113 -1
- package/src/html/controls.ts +73 -2
- package/src/html/css/calc.ts +385 -0
- package/src/html/css/cascade.ts +1403 -62
- package/src/html/css/color.ts +755 -0
- package/src/html/css/content.ts +417 -0
- package/src/html/css/counter-styles.ts +973 -0
- package/src/html/css/decode.ts +130 -0
- package/src/html/css/parse.ts +1514 -160
- package/src/html/css/style.ts +4102 -222
- package/src/html/css/ua.ts +58 -12
- package/src/html/css/values.ts +150 -50
- package/src/html/css/vars.ts +348 -0
- package/src/html/dom.ts +284 -17
- package/src/html/fonts.ts +641 -0
- package/src/html/gif.ts +233 -0
- package/src/html/index.ts +180 -20
- package/src/html/layout/block.ts +3456 -230
- package/src/html/layout/boxes.ts +2100 -190
- package/src/html/layout/cache.ts +289 -0
- package/src/html/layout/collapse.ts +317 -0
- package/src/html/layout/css-grid.ts +1063 -0
- package/src/html/layout/flex.ts +1115 -106
- package/src/html/layout/floats.ts +128 -14
- package/src/html/layout/grid-lines.ts +141 -0
- package/src/html/layout/grid.ts +181 -0
- package/src/html/layout/inline.ts +3943 -192
- package/src/html/layout/shaping.ts +140 -0
- package/src/html/layout/table.ts +696 -186
- package/src/html/layout/tracks.ts +437 -0
- package/src/html/node.ts +1481 -118
- package/src/html/paint.ts +4293 -169
- package/src/html/resources.ts +161 -50
- package/src/html/surfaces.ts +81 -0
- package/src/html/svg.ts +493 -0
- package/src/html/url.ts +86 -0
- package/src/index.ts +2 -0
- package/src/internal/collate.ts +12 -0
- package/src/internal/markdown/ast.ts +11 -0
- package/src/internal/markdown/parse.ts +154 -11
- package/src/internal/prewarm.ts +45 -0
- package/src/internal/sdf.ts +227 -0
- package/src/internal/splice.ts +25 -0
- package/src/internal/window.ts +36 -5
- package/src/maps/anchors.ts +307 -1
- package/src/maps/controller.ts +116 -31
- package/src/maps/gl/buckets.ts +5 -1
- package/src/maps/gl/build-worker.ts +13 -2
- package/src/maps/gl/cover.ts +79 -44
- package/src/maps/gl/placement.ts +221 -69
- package/src/maps/gl/renderer.ts +104 -20
- package/src/maps/gl/shaders.ts +35 -32
- package/src/maps/gl/store.ts +66 -17
- package/src/maps/gl/text.ts +444 -71
- package/src/maps/gl/view.ts +361 -96
- package/src/maps/icons.ts +183 -0
- package/src/maps/index.ts +4 -0
- package/src/maps/labels.ts +269 -49
- package/src/maps/node.ts +82 -49
- package/src/maps/proj.ts +87 -0
- package/src/maps/sources.ts +5 -3
- package/src/maps/style.ts +32 -0
- package/src/maps/styles.ts +215 -4
- package/src/maps/tiles.ts +64 -31
- package/src/maps/types.ts +28 -4
- package/src/markdown/index.ts +149 -67
- package/src/markdown/spans.ts +5 -0
- package/src/qml/containers.tsx +6 -1
- package/src/qml/interaction.tsx +42 -5
- package/src/qml/layouts.tsx +21 -6
- package/src/qml/qtquick.tsx +9 -2
- package/src/qml/react.tsx +7 -5
- package/src/reorder/clock.ts +27 -0
- package/src/reorder/index.ts +100 -27
- package/src/rich-text-editor/index.ts +34 -1
- package/src/rich-text-editor/keymap.ts +1 -1
- package/src/rich-text-editor/keys.ts +135 -26
- package/src/rich-text-editor/marks.ts +101 -0
- package/src/rich-text-editor/render.ts +49 -12
- package/src/rich-text-editor/toolbar.ts +1 -1
- package/src/rich-text-editor/virtual.ts +25 -0
- package/src/richtext/node.ts +235 -14
- package/src/richtext/runs.ts +23 -12
- package/src/table/index.ts +29 -0
- package/src/table/rows.ts +18 -8
- package/src/terminal/index.ts +4 -0
- package/src/terminal/vt/pty.ts +16 -2
- package/src/terminal-output/index.ts +6 -1
- package/src/three/canvas.ts +102 -8
- package/src/three/events.ts +49 -9
- package/src/tree/index.ts +19 -0
package/dist/code-editor/node.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
177
|
-
this.
|
|
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
|
-
|
|
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.
|
|
212
|
-
this.
|
|
213
|
-
this.
|
|
214
|
-
|
|
215
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
448
|
-
const disp =
|
|
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
|
-
/**
|
|
508
|
-
*
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
594
|
-
|
|
595
|
-
this.
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
|
|
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
|
-
|
|
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
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
624
|
-
|
|
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.
|
|
1600
|
+
const step = this._history[this._historyIndex++];
|
|
1601
|
+
this._applyStep(step.from, step.before.length, step.after, step);
|
|
632
1602
|
}
|
|
633
|
-
|
|
1603
|
+
_applyStep(from, removed, lines, selection) {
|
|
1604
|
+
const view = this._viewBefore();
|
|
634
1605
|
this._undoRun = null;
|
|
635
1606
|
this._goalX = null;
|
|
636
|
-
this.
|
|
637
|
-
this._caret = clampPos(this._lines,
|
|
638
|
-
this._anchor = clampPos(this._lines,
|
|
639
|
-
this._afterEdit(
|
|
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
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
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
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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(
|
|
744
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
879
|
-
const
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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
|
-
|
|
1143
|
-
|
|
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.
|
|
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
|
|
1299
|
-
nextProps.
|
|
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
|
-
|
|
1348
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
1448
|
-
|
|
1449
|
-
(
|
|
1450
|
-
|
|
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
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
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
|
-
|
|
1475
|
-
|
|
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
|
-
|
|
1481
|
-
|
|
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
|
-
|
|
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
|
}
|