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