@react-x11/components 0.1.0 → 0.2.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 +979 -21
- package/dist/ansi/cast.d.ts +48 -0
- package/dist/ansi/cast.d.ts.map +1 -0
- package/dist/ansi/cast.js +135 -0
- package/dist/ansi/cast.js.map +1 -0
- package/dist/ansi/index.d.ts +10 -0
- package/dist/ansi/index.d.ts.map +1 -0
- package/dist/ansi/index.js +27 -0
- package/dist/ansi/index.js.map +1 -0
- package/dist/ansi/palette.d.ts +79 -0
- package/dist/ansi/palette.d.ts.map +1 -0
- package/dist/ansi/palette.js +155 -0
- package/dist/ansi/palette.js.map +1 -0
- package/dist/ansi/parse.d.ts +114 -0
- package/dist/ansi/parse.d.ts.map +1 -0
- package/dist/ansi/parse.js +687 -0
- package/dist/ansi/parse.js.map +1 -0
- package/dist/ansi/sgr.d.ts +87 -0
- package/dist/ansi/sgr.d.ts.map +1 -0
- package/dist/ansi/sgr.js +302 -0
- package/dist/ansi/sgr.js.map +1 -0
- package/dist/ansi/utf8.d.ts +15 -0
- package/dist/ansi/utf8.d.ts.map +1 -0
- package/dist/ansi/utf8.js +82 -0
- package/dist/ansi/utf8.js.map +1 -0
- package/dist/calendar/Calendar.d.ts +136 -0
- package/dist/calendar/Calendar.d.ts.map +1 -0
- package/dist/calendar/Calendar.js +460 -0
- package/dist/calendar/Calendar.js.map +1 -0
- package/dist/calendar/DatePicker.d.ts +52 -0
- package/dist/calendar/DatePicker.d.ts.map +1 -0
- package/dist/calendar/DatePicker.js +257 -0
- package/dist/calendar/DatePicker.js.map +1 -0
- package/dist/calendar/dates.d.ts +90 -0
- package/dist/calendar/dates.d.ts.map +1 -0
- package/dist/calendar/dates.js +236 -0
- package/dist/calendar/dates.js.map +1 -0
- package/dist/calendar/index.d.ts +8 -0
- package/dist/calendar/index.d.ts.map +1 -0
- package/dist/calendar/index.js +15 -0
- package/dist/calendar/index.js.map +1 -0
- package/dist/charts/components.d.ts +141 -0
- package/dist/charts/components.d.ts.map +1 -0
- package/dist/charts/components.js +580 -0
- package/dist/charts/components.js.map +1 -0
- package/dist/charts/data.d.ts +198 -0
- package/dist/charts/data.d.ts.map +1 -0
- package/dist/charts/data.js +616 -0
- package/dist/charts/data.js.map +1 -0
- package/dist/charts/hx.d.ts +21 -0
- package/dist/charts/hx.d.ts.map +1 -0
- package/dist/charts/hx.js +15 -0
- package/dist/charts/hx.js.map +1 -0
- package/dist/charts/index.d.ts +27 -0
- package/dist/charts/index.d.ts.map +1 -0
- package/dist/charts/index.js +38 -0
- package/dist/charts/index.js.map +1 -0
- package/dist/charts/node.d.ts +96 -0
- package/dist/charts/node.d.ts.map +1 -0
- package/dist/charts/node.js +720 -0
- package/dist/charts/node.js.map +1 -0
- package/dist/charts/render.d.ts +153 -0
- package/dist/charts/render.d.ts.map +1 -0
- package/dist/charts/render.js +762 -0
- package/dist/charts/render.js.map +1 -0
- package/dist/charts/scale.d.ts +42 -0
- package/dist/charts/scale.d.ts.map +1 -0
- package/dist/charts/scale.js +173 -0
- package/dist/charts/scale.js.map +1 -0
- package/dist/charts/spec.d.ts +91 -0
- package/dist/charts/spec.d.ts.map +1 -0
- package/dist/charts/spec.js +33 -0
- package/dist/charts/spec.js.map +1 -0
- package/dist/charts/timers.d.ts +6 -0
- package/dist/charts/timers.d.ts.map +1 -0
- package/dist/charts/timers.js +14 -0
- package/dist/charts/timers.js.map +1 -0
- package/dist/code/hx.d.ts +24 -0
- package/dist/code/hx.d.ts.map +1 -0
- package/dist/code/hx.js +25 -0
- package/dist/code/hx.js.map +1 -0
- package/dist/code/index.d.ts +45 -0
- package/dist/code/index.d.ts.map +1 -0
- package/dist/code/index.js +93 -0
- package/dist/code/index.js.map +1 -0
- package/dist/code-editor/completion.d.ts +26 -0
- package/dist/code-editor/completion.d.ts.map +1 -0
- package/dist/code-editor/completion.js +129 -0
- package/dist/code-editor/completion.js.map +1 -0
- package/dist/code-editor/doc.d.ts +35 -0
- package/dist/code-editor/doc.d.ts.map +1 -0
- package/dist/code-editor/doc.js +162 -0
- package/dist/code-editor/doc.js.map +1 -0
- package/dist/code-editor/hx.d.ts +13 -0
- package/dist/code-editor/hx.d.ts.map +1 -0
- package/dist/code-editor/hx.js +10 -0
- package/dist/code-editor/hx.js.map +1 -0
- package/dist/code-editor/index.d.ts +88 -0
- package/dist/code-editor/index.d.ts.map +1 -0
- package/dist/code-editor/index.js +401 -0
- package/dist/code-editor/index.js.map +1 -0
- package/dist/code-editor/node.d.ts +343 -0
- package/dist/code-editor/node.d.ts.map +1 -0
- package/dist/code-editor/node.js +1521 -0
- package/dist/code-editor/node.js.map +1 -0
- package/dist/code-language/hljs.d.ts +47 -0
- package/dist/code-language/hljs.d.ts.map +1 -0
- package/dist/code-language/hljs.js +211 -0
- package/dist/code-language/hljs.js.map +1 -0
- package/dist/code-language/index.d.ts +22 -0
- package/dist/code-language/index.d.ts.map +1 -0
- package/dist/code-language/index.js +20 -0
- package/dist/code-language/index.js.map +1 -0
- package/dist/code-language/languages/glsl.d.ts +4 -0
- package/dist/code-language/languages/glsl.d.ts.map +1 -0
- package/dist/code-language/languages/glsl.js +99 -0
- package/dist/code-language/languages/glsl.js.map +1 -0
- package/dist/code-language/languages/javascript.d.ts +10 -0
- package/dist/code-language/languages/javascript.d.ts.map +1 -0
- package/dist/code-language/languages/javascript.js +360 -0
- package/dist/code-language/languages/javascript.js.map +1 -0
- package/dist/code-language/languages/json.d.ts +4 -0
- package/dist/code-language/languages/json.d.ts.map +1 -0
- package/dist/code-language/languages/json.js +68 -0
- package/dist/code-language/languages/json.js.map +1 -0
- package/dist/code-language/languages/shell.d.ts +4 -0
- package/dist/code-language/languages/shell.d.ts.map +1 -0
- package/dist/code-language/languages/shell.js +189 -0
- package/dist/code-language/languages/shell.js.map +1 -0
- package/dist/code-language/languages/sql.d.ts +11 -0
- package/dist/code-language/languages/sql.d.ts.map +1 -0
- package/dist/code-language/languages/sql.js +133 -0
- package/dist/code-language/languages/sql.js.map +1 -0
- package/dist/code-language/lezer.d.ts +24 -0
- package/dist/code-language/lezer.d.ts.map +1 -0
- package/dist/code-language/lezer.js +148 -0
- package/dist/code-language/lezer.js.map +1 -0
- package/dist/code-language/registry.d.ts +19 -0
- package/dist/code-language/registry.d.ts.map +1 -0
- package/dist/code-language/registry.js +56 -0
- package/dist/code-language/registry.js.map +1 -0
- package/dist/code-language/runs.d.ts +35 -0
- package/dist/code-language/runs.d.ts.map +1 -0
- package/dist/code-language/runs.js +88 -0
- package/dist/code-language/runs.js.map +1 -0
- package/dist/code-language/stream.d.ts +70 -0
- package/dist/code-language/stream.d.ts.map +1 -0
- package/dist/code-language/stream.js +243 -0
- package/dist/code-language/stream.js.map +1 -0
- package/dist/code-language/textmate.d.ts +27 -0
- package/dist/code-language/textmate.d.ts.map +1 -0
- package/dist/code-language/textmate.js +86 -0
- package/dist/code-language/textmate.js.map +1 -0
- package/dist/code-language/theme.d.ts +26 -0
- package/dist/code-language/theme.d.ts.map +1 -0
- package/dist/code-language/theme.js +108 -0
- package/dist/code-language/theme.js.map +1 -0
- package/dist/code-language/timers.d.ts +7 -0
- package/dist/code-language/timers.d.ts.map +1 -0
- package/dist/code-language/timers.js +22 -0
- package/dist/code-language/timers.js.map +1 -0
- package/dist/code-language/types.d.ts +173 -0
- package/dist/code-language/types.d.ts.map +1 -0
- package/dist/code-language/types.js +41 -0
- package/dist/code-language/types.js.map +1 -0
- package/dist/codeblock/index.d.ts +69 -0
- package/dist/codeblock/index.d.ts.map +1 -0
- package/dist/codeblock/index.js +85 -0
- package/dist/codeblock/index.js.map +1 -0
- package/dist/color-picker/ColorField.d.ts +39 -0
- package/dist/color-picker/ColorField.d.ts.map +1 -0
- package/dist/color-picker/ColorField.js +239 -0
- package/dist/color-picker/ColorField.js.map +1 -0
- package/dist/color-picker/ColorPicker.d.ts +109 -0
- package/dist/color-picker/ColorPicker.d.ts.map +1 -0
- package/dist/color-picker/ColorPicker.js +757 -0
- package/dist/color-picker/ColorPicker.js.map +1 -0
- package/dist/color-picker/color.d.ts +105 -0
- package/dist/color-picker/color.d.ts.map +1 -0
- package/dist/color-picker/color.js +313 -0
- package/dist/color-picker/color.js.map +1 -0
- package/dist/color-picker/index.d.ts +8 -0
- package/dist/color-picker/index.d.ts.map +1 -0
- package/dist/color-picker/index.js +19 -0
- package/dist/color-picker/index.js.map +1 -0
- package/dist/desktop-calendar/eds.d.ts +102 -0
- package/dist/desktop-calendar/eds.d.ts.map +1 -0
- package/dist/desktop-calendar/eds.js +356 -0
- package/dist/desktop-calendar/eds.js.map +1 -0
- package/dist/desktop-calendar/ical.d.ts +63 -0
- package/dist/desktop-calendar/ical.d.ts.map +1 -0
- package/dist/desktop-calendar/ical.js +57 -0
- package/dist/desktop-calendar/ical.js.map +1 -0
- package/dist/desktop-calendar/index.d.ts +72 -0
- package/dist/desktop-calendar/index.d.ts.map +1 -0
- package/dist/desktop-calendar/index.js +213 -0
- package/dist/desktop-calendar/index.js.map +1 -0
- package/dist/embed/client.d.ts +121 -0
- package/dist/embed/client.d.ts.map +1 -0
- package/dist/embed/client.js +203 -0
- package/dist/embed/client.js.map +1 -0
- package/dist/embed/host.d.ts +96 -0
- package/dist/embed/host.d.ts.map +1 -0
- package/dist/embed/host.js +266 -0
- package/dist/embed/host.js.map +1 -0
- package/dist/embed/index.d.ts +5 -0
- package/dist/embed/index.d.ts.map +1 -0
- package/dist/embed/index.js +13 -0
- package/dist/embed/index.js.map +1 -0
- package/dist/embed/timers.d.ts +6 -0
- package/dist/embed/timers.d.ts.map +1 -0
- package/dist/embed/timers.js +16 -0
- package/dist/embed/timers.js.map +1 -0
- package/dist/flow/draw.d.ts +45 -0
- package/dist/flow/draw.d.ts.map +1 -0
- package/dist/flow/draw.js +295 -0
- package/dist/flow/draw.js.map +1 -0
- package/dist/flow/index.d.ts +64 -0
- package/dist/flow/index.d.ts.map +1 -0
- package/dist/flow/index.js +306 -0
- package/dist/flow/index.js.map +1 -0
- package/dist/flow/model.d.ts +158 -0
- package/dist/flow/model.d.ts.map +1 -0
- package/dist/flow/model.js +566 -0
- package/dist/flow/model.js.map +1 -0
- package/dist/flow/node.d.ts +429 -0
- package/dist/flow/node.d.ts.map +1 -0
- package/dist/flow/node.js +3025 -0
- package/dist/flow/node.js.map +1 -0
- package/dist/flow/paths.d.ts +43 -0
- package/dist/flow/paths.d.ts.map +1 -0
- package/dist/flow/paths.js +327 -0
- package/dist/flow/paths.js.map +1 -0
- package/dist/flow/types.d.ts +561 -0
- package/dist/flow/types.d.ts.map +1 -0
- package/dist/flow/types.js +2 -0
- package/dist/flow/types.js.map +1 -0
- package/dist/formula/hx.d.ts +24 -0
- package/dist/formula/hx.d.ts.map +1 -0
- package/dist/formula/hx.js +25 -0
- package/dist/formula/hx.js.map +1 -0
- package/dist/formula/index.d.ts +74 -0
- package/dist/formula/index.d.ts.map +1 -0
- package/dist/formula/index.js +155 -0
- package/dist/formula/index.js.map +1 -0
- package/dist/formula/katex.d.ts +78 -0
- package/dist/formula/katex.d.ts.map +1 -0
- package/dist/formula/katex.js +130 -0
- package/dist/formula/katex.js.map +1 -0
- package/dist/formula/layout.d.ts +103 -0
- package/dist/formula/layout.d.ts.map +1 -0
- package/dist/formula/layout.js +716 -0
- package/dist/formula/layout.js.map +1 -0
- package/dist/formula/node.d.ts +50 -0
- package/dist/formula/node.d.ts.map +1 -0
- package/dist/formula/node.js +351 -0
- package/dist/formula/node.js.map +1 -0
- package/dist/html/controls.d.ts +48 -0
- package/dist/html/controls.d.ts.map +1 -0
- package/dist/html/controls.js +195 -0
- package/dist/html/controls.js.map +1 -0
- package/dist/html/css/cascade.d.ts +51 -0
- package/dist/html/css/cascade.d.ts.map +1 -0
- package/dist/html/css/cascade.js +552 -0
- package/dist/html/css/cascade.js.map +1 -0
- package/dist/html/css/parse.d.ts +74 -0
- package/dist/html/css/parse.d.ts.map +1 -0
- package/dist/html/css/parse.js +464 -0
- package/dist/html/css/parse.js.map +1 -0
- package/dist/html/css/style.d.ts +142 -0
- package/dist/html/css/style.d.ts.map +1 -0
- package/dist/html/css/style.js +1145 -0
- package/dist/html/css/style.js.map +1 -0
- package/dist/html/css/ua.d.ts +5 -0
- package/dist/html/css/ua.d.ts.map +1 -0
- package/dist/html/css/ua.js +171 -0
- package/dist/html/css/ua.js.map +1 -0
- package/dist/html/css/values.d.ts +78 -0
- package/dist/html/css/values.d.ts.map +1 -0
- package/dist/html/css/values.js +330 -0
- package/dist/html/css/values.js.map +1 -0
- package/dist/html/dom.d.ts +112 -0
- package/dist/html/dom.d.ts.map +1 -0
- package/dist/html/dom.js +374 -0
- package/dist/html/dom.js.map +1 -0
- package/dist/html/hx.d.ts +24 -0
- package/dist/html/hx.d.ts.map +1 -0
- package/dist/html/hx.js +29 -0
- package/dist/html/hx.js.map +1 -0
- package/dist/html/index.d.ts +137 -0
- package/dist/html/index.d.ts.map +1 -0
- package/dist/html/index.js +339 -0
- package/dist/html/index.js.map +1 -0
- package/dist/html/layout/block.d.ts +58 -0
- package/dist/html/layout/block.d.ts.map +1 -0
- package/dist/html/layout/block.js +724 -0
- package/dist/html/layout/block.js.map +1 -0
- package/dist/html/layout/boxes.d.ts +261 -0
- package/dist/html/layout/boxes.d.ts.map +1 -0
- package/dist/html/layout/boxes.js +725 -0
- package/dist/html/layout/boxes.js.map +1 -0
- package/dist/html/layout/flex.d.ts +16 -0
- package/dist/html/layout/flex.d.ts.map +1 -0
- package/dist/html/layout/flex.js +242 -0
- package/dist/html/layout/flex.js.map +1 -0
- package/dist/html/layout/floats.d.ts +50 -0
- package/dist/html/layout/floats.d.ts.map +1 -0
- package/dist/html/layout/floats.js +110 -0
- package/dist/html/layout/floats.js.map +1 -0
- package/dist/html/layout/inline.d.ts +44 -0
- package/dist/html/layout/inline.d.ts.map +1 -0
- package/dist/html/layout/inline.js +700 -0
- package/dist/html/layout/inline.js.map +1 -0
- package/dist/html/layout/table.d.ts +5 -0
- package/dist/html/layout/table.d.ts.map +1 -0
- package/dist/html/layout/table.js +340 -0
- package/dist/html/layout/table.js.map +1 -0
- package/dist/html/node.d.ts +149 -0
- package/dist/html/node.d.ts.map +1 -0
- package/dist/html/node.js +861 -0
- package/dist/html/node.js.map +1 -0
- package/dist/html/paint.d.ts +48 -0
- package/dist/html/paint.d.ts.map +1 -0
- package/dist/html/paint.js +495 -0
- package/dist/html/paint.js.map +1 -0
- package/dist/html/resources.d.ts +48 -0
- package/dist/html/resources.d.ts.map +1 -0
- package/dist/html/resources.js +156 -0
- package/dist/html/resources.js.map +1 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +92 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/heights.d.ts +81 -0
- package/dist/internal/heights.d.ts.map +1 -0
- package/dist/internal/heights.js +193 -0
- package/dist/internal/heights.js.map +1 -0
- package/dist/internal/hx.d.ts +24 -0
- package/dist/internal/hx.d.ts.map +1 -0
- package/dist/internal/hx.js +27 -0
- package/dist/internal/hx.js.map +1 -0
- package/dist/internal/text.d.ts +6 -0
- package/dist/internal/text.d.ts.map +1 -0
- package/dist/internal/text.js +44 -0
- package/dist/internal/text.js.map +1 -0
- package/dist/internal/timers.d.ts +8 -0
- package/dist/internal/timers.d.ts.map +1 -0
- package/dist/internal/timers.js +22 -0
- package/dist/internal/timers.js.map +1 -0
- package/dist/internal/widget.d.ts +40 -0
- package/dist/internal/widget.d.ts.map +1 -0
- package/dist/internal/widget.js +44 -0
- package/dist/internal/widget.js.map +1 -0
- package/dist/markdown/ast.d.ts +126 -0
- package/dist/markdown/ast.d.ts.map +1 -0
- package/dist/markdown/ast.js +8 -0
- package/dist/markdown/ast.js.map +1 -0
- package/dist/markdown/hx.d.ts +24 -0
- package/dist/markdown/hx.d.ts.map +1 -0
- package/dist/markdown/hx.js +25 -0
- package/dist/markdown/hx.js.map +1 -0
- package/dist/markdown/index.d.ts +90 -0
- package/dist/markdown/index.d.ts.map +1 -0
- package/dist/markdown/index.js +413 -0
- package/dist/markdown/index.js.map +1 -0
- package/dist/markdown/parse.d.ts +8 -0
- package/dist/markdown/parse.d.ts.map +1 -0
- package/dist/markdown/parse.js +911 -0
- package/dist/markdown/parse.js.map +1 -0
- package/dist/markdown/spans.d.ts +20 -0
- package/dist/markdown/spans.d.ts.map +1 -0
- package/dist/markdown/spans.js +96 -0
- package/dist/markdown/spans.js.map +1 -0
- package/dist/media-player/backends.d.ts +64 -0
- package/dist/media-player/backends.d.ts.map +1 -0
- package/dist/media-player/backends.js +110 -0
- package/dist/media-player/backends.js.map +1 -0
- package/dist/media-player/control.d.ts +40 -0
- package/dist/media-player/control.d.ts.map +1 -0
- package/dist/media-player/control.js +44 -0
- package/dist/media-player/control.js.map +1 -0
- package/dist/media-player/hx.d.ts +24 -0
- package/dist/media-player/hx.d.ts.map +1 -0
- package/dist/media-player/hx.js +25 -0
- package/dist/media-player/hx.js.map +1 -0
- package/dist/media-player/index.d.ts +107 -0
- package/dist/media-player/index.d.ts.map +1 -0
- package/dist/media-player/index.js +219 -0
- package/dist/media-player/index.js.map +1 -0
- package/dist/media-player/mpv.d.ts +4 -0
- package/dist/media-player/mpv.d.ts.map +1 -0
- package/dist/media-player/mpv.js +120 -0
- package/dist/media-player/mpv.js.map +1 -0
- package/dist/media-player/vlc.d.ts +8 -0
- package/dist/media-player/vlc.d.ts.map +1 -0
- package/dist/media-player/vlc.js +83 -0
- package/dist/media-player/vlc.js.map +1 -0
- package/dist/richtext/index.d.ts +19 -0
- package/dist/richtext/index.d.ts.map +1 -0
- package/dist/richtext/index.js +4 -0
- package/dist/richtext/index.js.map +1 -0
- package/dist/richtext/links.d.ts +12 -0
- package/dist/richtext/links.d.ts.map +1 -0
- package/dist/richtext/links.js +64 -0
- package/dist/richtext/links.js.map +1 -0
- package/dist/richtext/menu.d.ts +11 -0
- package/dist/richtext/menu.d.ts.map +1 -0
- package/dist/richtext/menu.js +43 -0
- package/dist/richtext/menu.js.map +1 -0
- package/dist/richtext/node.d.ts +151 -0
- package/dist/richtext/node.d.ts.map +1 -0
- package/dist/richtext/node.js +255 -0
- package/dist/richtext/node.js.map +1 -0
- package/dist/richtext/runs.d.ts +92 -0
- package/dist/richtext/runs.d.ts.map +1 -0
- package/dist/richtext/runs.js +158 -0
- package/dist/richtext/runs.js.map +1 -0
- package/dist/table/hx.d.ts +24 -0
- package/dist/table/hx.d.ts.map +1 -0
- package/dist/table/hx.js +25 -0
- package/dist/table/hx.js.map +1 -0
- package/dist/table/index.d.ts +169 -0
- package/dist/table/index.d.ts.map +1 -0
- package/dist/table/index.js +844 -0
- package/dist/table/index.js.map +1 -0
- package/dist/table/rows.d.ts +136 -0
- package/dist/table/rows.d.ts.map +1 -0
- package/dist/table/rows.js +146 -0
- package/dist/table/rows.js.map +1 -0
- package/dist/terminal/backends.d.ts +105 -0
- package/dist/terminal/backends.d.ts.map +1 -0
- package/dist/terminal/backends.js +185 -0
- package/dist/terminal/backends.js.map +1 -0
- package/dist/terminal/hx.d.ts +24 -0
- package/dist/terminal/hx.d.ts.map +1 -0
- package/dist/terminal/hx.js +25 -0
- package/dist/terminal/hx.js.map +1 -0
- package/dist/terminal/index.d.ts +160 -0
- package/dist/terminal/index.d.ts.map +1 -0
- package/dist/terminal/index.js +344 -0
- package/dist/terminal/index.js.map +1 -0
- package/dist/terminal/title.d.ts +24 -0
- package/dist/terminal/title.d.ts.map +1 -0
- package/dist/terminal/title.js +129 -0
- package/dist/terminal/title.js.map +1 -0
- package/dist/terminal/vt/colors.d.ts +63 -0
- package/dist/terminal/vt/colors.d.ts.map +1 -0
- package/dist/terminal/vt/colors.js +203 -0
- package/dist/terminal/vt/colors.js.map +1 -0
- package/dist/terminal/vt/diff.d.ts +117 -0
- package/dist/terminal/vt/diff.d.ts.map +1 -0
- package/dist/terminal/vt/diff.js +358 -0
- package/dist/terminal/vt/diff.js.map +1 -0
- package/dist/terminal/vt/fonts.d.ts +92 -0
- package/dist/terminal/vt/fonts.d.ts.map +1 -0
- package/dist/terminal/vt/fonts.js +178 -0
- package/dist/terminal/vt/fonts.js.map +1 -0
- package/dist/terminal/vt/index.d.ts +78 -0
- package/dist/terminal/vt/index.d.ts.map +1 -0
- package/dist/terminal/vt/index.js +312 -0
- package/dist/terminal/vt/index.js.map +1 -0
- package/dist/terminal/vt/keys.d.ts +74 -0
- package/dist/terminal/vt/keys.d.ts.map +1 -0
- package/dist/terminal/vt/keys.js +309 -0
- package/dist/terminal/vt/keys.js.map +1 -0
- package/dist/terminal/vt/mouse.d.ts +44 -0
- package/dist/terminal/vt/mouse.d.ts.map +1 -0
- package/dist/terminal/vt/mouse.js +106 -0
- package/dist/terminal/vt/mouse.js.map +1 -0
- package/dist/terminal/vt/node.d.ts +246 -0
- package/dist/terminal/vt/node.d.ts.map +1 -0
- package/dist/terminal/vt/node.js +1068 -0
- package/dist/terminal/vt/node.js.map +1 -0
- package/dist/terminal/vt/pty.d.ts +81 -0
- package/dist/terminal/vt/pty.d.ts.map +1 -0
- package/dist/terminal/vt/pty.js +201 -0
- package/dist/terminal/vt/pty.js.map +1 -0
- package/dist/terminal/vt/renderer.d.ts +179 -0
- package/dist/terminal/vt/renderer.d.ts.map +1 -0
- package/dist/terminal/vt/renderer.js +357 -0
- package/dist/terminal/vt/renderer.js.map +1 -0
- package/dist/terminal/vt/timers.d.ts +6 -0
- package/dist/terminal/vt/timers.d.ts.map +1 -0
- package/dist/terminal/vt/timers.js +16 -0
- package/dist/terminal/vt/timers.js.map +1 -0
- package/dist/terminal/vt/xterm.d.ts +142 -0
- package/dist/terminal/vt/xterm.d.ts.map +1 -0
- package/dist/terminal/vt/xterm.js +32 -0
- package/dist/terminal/vt/xterm.js.map +1 -0
- package/dist/terminal-output/hx.d.ts +24 -0
- package/dist/terminal-output/hx.d.ts.map +1 -0
- package/dist/terminal-output/hx.js +25 -0
- package/dist/terminal-output/hx.js.map +1 -0
- package/dist/terminal-output/index.d.ts +92 -0
- package/dist/terminal-output/index.d.ts.map +1 -0
- package/dist/terminal-output/index.js +179 -0
- package/dist/terminal-output/index.js.map +1 -0
- package/dist/terminal-output/runs.d.ts +27 -0
- package/dist/terminal-output/runs.d.ts.map +1 -0
- package/dist/terminal-output/runs.js +66 -0
- package/dist/terminal-output/runs.js.map +1 -0
- package/dist/three/canvas.d.ts +46 -0
- package/dist/three/canvas.d.ts.map +1 -0
- package/dist/three/canvas.js +305 -0
- package/dist/three/canvas.js.map +1 -0
- package/dist/three/events.d.ts +64 -0
- package/dist/three/events.d.ts.map +1 -0
- package/dist/three/events.js +146 -0
- package/dist/three/events.js.map +1 -0
- package/dist/three/geometries.d.ts +89 -0
- package/dist/three/geometries.d.ts.map +1 -0
- package/dist/three/geometries.js +345 -0
- package/dist/three/geometries.js.map +1 -0
- package/dist/three/globals.d.ts +7 -0
- package/dist/three/globals.d.ts.map +1 -0
- package/dist/three/globals.js +20 -0
- package/dist/three/globals.js.map +1 -0
- package/dist/three/hx.d.ts +9 -0
- package/dist/three/hx.d.ts.map +1 -0
- package/dist/three/hx.js +10 -0
- package/dist/three/hx.js.map +1 -0
- package/dist/three/index.d.ts +22 -0
- package/dist/three/index.d.ts.map +1 -0
- package/dist/three/index.js +24 -0
- package/dist/three/index.js.map +1 -0
- package/dist/three/jsx-dev-runtime.d.ts +19 -0
- package/dist/three/jsx-dev-runtime.d.ts.map +1 -0
- package/dist/three/jsx-dev-runtime.js +2 -0
- package/dist/three/jsx-dev-runtime.js.map +1 -0
- package/dist/three/jsx-runtime.d.ts +19 -0
- package/dist/three/jsx-runtime.d.ts.map +1 -0
- package/dist/three/jsx-runtime.js +2 -0
- package/dist/three/jsx-runtime.js.map +1 -0
- package/dist/three/jsx.d.ts +198 -0
- package/dist/three/jsx.d.ts.map +1 -0
- package/dist/three/jsx.js +2 -0
- package/dist/three/jsx.js.map +1 -0
- package/dist/three/materials.d.ts +125 -0
- package/dist/three/materials.d.ts.map +1 -0
- package/dist/three/materials.js +229 -0
- package/dist/three/materials.js.map +1 -0
- package/dist/three/math.d.ts +146 -0
- package/dist/three/math.d.ts.map +1 -0
- package/dist/three/math.js +464 -0
- package/dist/three/math.js.map +1 -0
- package/dist/three/objects.d.ts +185 -0
- package/dist/three/objects.d.ts.map +1 -0
- package/dist/three/objects.js +309 -0
- package/dist/three/objects.js.map +1 -0
- package/dist/three/passes.d.ts +54 -0
- package/dist/three/passes.d.ts.map +1 -0
- package/dist/three/passes.js +78 -0
- package/dist/three/passes.js.map +1 -0
- package/dist/three/postprocess.d.ts +85 -0
- package/dist/three/postprocess.d.ts.map +1 -0
- package/dist/three/postprocess.js +503 -0
- package/dist/three/postprocess.js.map +1 -0
- package/dist/three/raycast.d.ts +24 -0
- package/dist/three/raycast.d.ts.map +1 -0
- package/dist/three/raycast.js +138 -0
- package/dist/three/raycast.js.map +1 -0
- package/dist/three/reconciler.d.ts +48 -0
- package/dist/three/reconciler.d.ts.map +1 -0
- package/dist/three/reconciler.js +416 -0
- package/dist/three/reconciler.js.map +1 -0
- package/dist/three/renderer-direct.d.ts +102 -0
- package/dist/three/renderer-direct.d.ts.map +1 -0
- package/dist/three/renderer-direct.js +782 -0
- package/dist/three/renderer-direct.js.map +1 -0
- package/dist/three/renderer-indirect.d.ts +68 -0
- package/dist/three/renderer-indirect.d.ts.map +1 -0
- package/dist/three/renderer-indirect.js +438 -0
- package/dist/three/renderer-indirect.js.map +1 -0
- package/dist/three/store.d.ts +81 -0
- package/dist/three/store.d.ts.map +1 -0
- package/dist/three/store.js +110 -0
- package/dist/three/store.js.map +1 -0
- package/dist/timeline/hx.d.ts +18 -0
- package/dist/timeline/hx.d.ts.map +1 -0
- package/dist/timeline/hx.js +10 -0
- package/dist/timeline/hx.js.map +1 -0
- package/dist/timeline/index.d.ts +152 -0
- package/dist/timeline/index.d.ts.map +1 -0
- package/dist/timeline/index.js +383 -0
- package/dist/timeline/index.js.map +1 -0
- package/dist/tray-host/hx.d.ts +24 -0
- package/dist/tray-host/hx.d.ts.map +1 -0
- package/dist/tray-host/hx.js +25 -0
- package/dist/tray-host/hx.js.map +1 -0
- package/dist/tray-host/index.d.ts +110 -0
- package/dist/tray-host/index.d.ts.map +1 -0
- package/dist/tray-host/index.js +191 -0
- package/dist/tray-host/index.js.map +1 -0
- package/dist/tray-host/manager.d.ts +137 -0
- package/dist/tray-host/manager.d.ts.map +1 -0
- package/dist/tray-host/manager.js +462 -0
- package/dist/tray-host/manager.js.map +1 -0
- package/dist/tray-host/notify.d.ts +22 -0
- package/dist/tray-host/notify.d.ts.map +1 -0
- package/dist/tray-host/notify.js +74 -0
- package/dist/tray-host/notify.js.map +1 -0
- package/dist/tray-host/protocol.d.ts +106 -0
- package/dist/tray-host/protocol.d.ts.map +1 -0
- package/dist/tray-host/protocol.js +191 -0
- package/dist/tray-host/protocol.js.map +1 -0
- package/dist/tree/hx.d.ts +24 -0
- package/dist/tree/hx.d.ts.map +1 -0
- package/dist/tree/hx.js +25 -0
- package/dist/tree/hx.js.map +1 -0
- package/dist/tree/index.d.ts +221 -0
- package/dist/tree/index.d.ts.map +1 -0
- package/dist/tree/index.js +716 -0
- package/dist/tree/index.js.map +1 -0
- package/dist/tree/internal.d.ts +18 -0
- package/dist/tree/internal.d.ts.map +1 -0
- package/dist/tree/internal.js +66 -0
- package/dist/tree/internal.js.map +1 -0
- package/dist/tree/rows.d.ts +177 -0
- package/dist/tree/rows.d.ts.map +1 -0
- package/dist/tree/rows.js +179 -0
- package/dist/tree/rows.js.map +1 -0
- package/package.json +166 -12
- package/src/ansi/cast.ts +159 -0
- package/src/ansi/index.ts +53 -0
- package/src/ansi/palette.ts +226 -0
- package/src/ansi/parse.ts +780 -0
- package/src/ansi/sgr.ts +380 -0
- package/src/ansi/utf8.ts +84 -0
- package/src/calendar/Calendar.ts +744 -0
- package/src/calendar/DatePicker.ts +368 -0
- package/src/calendar/dates.ts +307 -0
- package/src/calendar/index.ts +51 -0
- package/src/charts/components.ts +864 -0
- package/src/charts/data.ts +776 -0
- package/src/charts/hx.ts +39 -0
- package/src/charts/index.ts +99 -0
- package/src/charts/node.ts +932 -0
- package/src/charts/render.ts +982 -0
- package/src/charts/scale.ts +214 -0
- package/src/charts/spec.ts +122 -0
- package/src/charts/timers.ts +24 -0
- package/src/code/hx.ts +51 -0
- package/src/code/index.ts +174 -0
- package/src/code-editor/completion.ts +161 -0
- package/src/code-editor/doc.ts +198 -0
- package/src/code-editor/hx.ts +26 -0
- package/src/code-editor/index.ts +615 -0
- package/src/code-editor/node.ts +1926 -0
- package/src/code-language/hljs.ts +302 -0
- package/src/code-language/index.ts +51 -0
- package/src/code-language/languages/glsl.ts +120 -0
- package/src/code-language/languages/javascript.ts +419 -0
- package/src/code-language/languages/json.ts +72 -0
- package/src/code-language/languages/shell.ts +221 -0
- package/src/code-language/languages/sql.ts +165 -0
- package/src/code-language/lezer.ts +190 -0
- package/src/code-language/registry.ts +66 -0
- package/src/code-language/runs.ts +128 -0
- package/src/code-language/stream.ts +306 -0
- package/src/code-language/textmate.ts +134 -0
- package/src/code-language/theme.ts +124 -0
- package/src/code-language/timers.ts +36 -0
- package/src/code-language/types.ts +252 -0
- package/src/codeblock/index.ts +146 -0
- package/src/color-picker/ColorField.ts +333 -0
- package/src/color-picker/ColorPicker.ts +1069 -0
- package/src/color-picker/color.ts +399 -0
- package/src/color-picker/index.ts +50 -0
- package/src/desktop-calendar/eds.ts +484 -0
- package/src/desktop-calendar/ical.ts +119 -0
- package/src/desktop-calendar/index.ts +287 -0
- package/src/embed/client.ts +311 -0
- package/src/embed/host.ts +432 -0
- package/src/embed/index.ts +33 -0
- package/src/embed/timers.ts +32 -0
- package/src/flow/draw.ts +418 -0
- package/src/flow/index.ts +473 -0
- package/src/flow/model.ts +687 -0
- package/src/flow/node.ts +3694 -0
- package/src/flow/paths.ts +403 -0
- package/src/flow/types.ts +634 -0
- package/src/formula/hx.ts +51 -0
- package/src/formula/index.ts +243 -0
- package/src/formula/katex.ts +209 -0
- package/src/formula/layout.ts +876 -0
- package/src/formula/node.ts +467 -0
- package/src/html/controls.ts +244 -0
- package/src/html/css/cascade.ts +606 -0
- package/src/html/css/parse.ts +500 -0
- package/src/html/css/style.ts +1371 -0
- package/src/html/css/ua.ts +174 -0
- package/src/html/css/values.ts +358 -0
- package/src/html/dom.ts +410 -0
- package/src/html/hx.ts +55 -0
- package/src/html/index.ts +545 -0
- package/src/html/layout/block.ts +876 -0
- package/src/html/layout/boxes.ts +981 -0
- package/src/html/layout/flex.ts +297 -0
- package/src/html/layout/floats.ts +125 -0
- package/src/html/layout/inline.ts +870 -0
- package/src/html/layout/table.ts +380 -0
- package/src/html/node.ts +967 -0
- package/src/html/paint.ts +605 -0
- package/src/html/resources.ts +214 -0
- package/src/index.ts +638 -0
- package/src/internal/heights.ts +211 -0
- package/src/internal/hx.ts +53 -0
- package/src/internal/text.ts +44 -0
- package/src/internal/timers.ts +44 -0
- package/src/internal/widget.ts +92 -0
- package/src/markdown/ast.ts +163 -0
- package/src/markdown/hx.ts +51 -0
- package/src/markdown/index.ts +686 -0
- package/src/markdown/parse.ts +1037 -0
- package/src/markdown/spans.ts +143 -0
- package/src/media-player/backends.ts +155 -0
- package/src/media-player/control.ts +70 -0
- package/src/media-player/hx.ts +51 -0
- package/src/media-player/index.ts +378 -0
- package/src/media-player/mpv.ts +146 -0
- package/src/media-player/vlc.ts +97 -0
- package/src/richtext/index.ts +42 -0
- package/src/richtext/links.ts +78 -0
- package/src/richtext/menu.ts +49 -0
- package/src/richtext/node.ts +406 -0
- package/src/richtext/runs.ts +253 -0
- package/src/table/hx.ts +51 -0
- package/src/table/index.ts +1219 -0
- package/src/table/rows.ts +275 -0
- package/src/terminal/backends.ts +242 -0
- package/src/terminal/hx.ts +51 -0
- package/src/terminal/index.ts +574 -0
- package/src/terminal/title.ts +172 -0
- package/src/terminal/vt/colors.ts +247 -0
- package/src/terminal/vt/diff.ts +467 -0
- package/src/terminal/vt/fonts.ts +233 -0
- package/src/terminal/vt/index.ts +443 -0
- package/src/terminal/vt/keys.ts +369 -0
- package/src/terminal/vt/mouse.ts +144 -0
- package/src/terminal/vt/node.ts +1242 -0
- package/src/terminal/vt/pty.ts +334 -0
- package/src/terminal/vt/renderer.ts +555 -0
- package/src/terminal/vt/timers.ts +31 -0
- package/src/terminal/vt/xterm.ts +193 -0
- package/src/terminal-output/hx.ts +51 -0
- package/src/terminal-output/index.ts +340 -0
- package/src/terminal-output/runs.ts +79 -0
- package/src/three/canvas.ts +451 -0
- package/src/three/events.ts +197 -0
- package/src/three/geometries.ts +429 -0
- package/src/three/globals.ts +34 -0
- package/src/three/hx.ts +22 -0
- package/src/three/index.ts +98 -0
- package/src/three/jsx-dev-runtime.ts +27 -0
- package/src/three/jsx-runtime.ts +38 -0
- package/src/three/jsx.ts +275 -0
- package/src/three/materials.ts +270 -0
- package/src/three/math.ts +585 -0
- package/src/three/objects.ts +397 -0
- package/src/three/passes.ts +90 -0
- package/src/three/postprocess.ts +677 -0
- package/src/three/raycast.ts +189 -0
- package/src/three/react-reconciler.d.ts +50 -0
- package/src/three/reconciler.ts +624 -0
- package/src/three/renderer-direct.ts +1055 -0
- package/src/three/renderer-indirect.ts +570 -0
- package/src/three/store.ts +207 -0
- package/src/timeline/hx.ts +31 -0
- package/src/timeline/index.ts +603 -0
- package/src/tray-host/hx.ts +51 -0
- package/src/tray-host/index.ts +331 -0
- package/src/tray-host/manager.ts +639 -0
- package/src/tray-host/notify.ts +102 -0
- package/src/tray-host/protocol.ts +235 -0
- package/src/tree/hx.ts +51 -0
- package/src/tree/index.ts +1087 -0
- package/src/tree/internal.ts +87 -0
- package/src/tree/rows.ts +334 -0
- package/src/index.d.ts +0 -2
- package/src/index.js +0 -9
- package/src/sparkline/index.d.ts +0 -29
- package/src/sparkline/index.js +0 -50
- package/src/sparkline/node.js +0 -72
|
@@ -0,0 +1,1926 @@
|
|
|
1
|
+
// The retained node behind `<codeeditor>`: the text model (lines, caret,
|
|
2
|
+
// selection, undo), scrolling, and all painting (gutter, tokens, selection,
|
|
3
|
+
// caret, bracket match, diagnostics). `./index.ts` registers it and wires
|
|
4
|
+
// input to it.
|
|
5
|
+
//
|
|
6
|
+
// **How input reaches this node.** Through the default-action seam
|
|
7
|
+
// react-x11#266 made public: `defaultKeyDown`, `defaultMouseDown`,
|
|
8
|
+
// `defaultMouseDrag`, `defaultMouseUp`, `defaultFocus` and `defaultBlur`
|
|
9
|
+
// are methods the EventManager calls on the focused or pressed node, after
|
|
10
|
+
// the application's own `onKeyDown`/`onMouseDown` handlers and not at all
|
|
11
|
+
// if one of them called `preventDefault()`. So a bare `<codeeditor>` in
|
|
12
|
+
// JSX edits, selects and blinks the way `<textinput>` does, with no
|
|
13
|
+
// wrapping component required — and an app still vetoes or extends any of
|
|
14
|
+
// it without knowing how the element is built.
|
|
15
|
+
//
|
|
16
|
+
// `<CodeEditor>` is now only the completion popup: it is an ordinary
|
|
17
|
+
// `onKeyDown` handler that consumes the keys the popup owns (arrows,
|
|
18
|
+
// Return, Tab-accept, Escape-close) and, by consuming them, keeps this
|
|
19
|
+
// node's default action from also acting on them.
|
|
20
|
+
import { CARET_BLINK_MS, Node } from 'react-x11/node';
|
|
21
|
+
import type {
|
|
22
|
+
Context2D,
|
|
23
|
+
MeasureConstraints,
|
|
24
|
+
MeasuredSize,
|
|
25
|
+
} from 'react-x11/node';
|
|
26
|
+
import type { Style } from 'react-x11/style';
|
|
27
|
+
import {
|
|
28
|
+
ctrlChordLetter,
|
|
29
|
+
XK_BACKSPACE,
|
|
30
|
+
XK_DELETE,
|
|
31
|
+
XK_DOWN,
|
|
32
|
+
XK_END,
|
|
33
|
+
XK_ESCAPE,
|
|
34
|
+
XK_HOME,
|
|
35
|
+
XK_KP_ENTER,
|
|
36
|
+
XK_LEFT,
|
|
37
|
+
XK_PAGE_DOWN,
|
|
38
|
+
XK_PAGE_UP,
|
|
39
|
+
XK_RETURN,
|
|
40
|
+
XK_RIGHT,
|
|
41
|
+
XK_TAB,
|
|
42
|
+
XK_UP,
|
|
43
|
+
} from 'react-x11/keysyms';
|
|
44
|
+
|
|
45
|
+
import { startInterval, stopInterval } from '../code-language/timers.js';
|
|
46
|
+
import type { TimerId } from '../code-language/timers.js';
|
|
47
|
+
import {
|
|
48
|
+
clampPos,
|
|
49
|
+
comparePos,
|
|
50
|
+
maxPos,
|
|
51
|
+
minPos,
|
|
52
|
+
movePos,
|
|
53
|
+
posEqual,
|
|
54
|
+
tabMap,
|
|
55
|
+
wordBoundary,
|
|
56
|
+
wordRangeAt,
|
|
57
|
+
} from './doc.js';
|
|
58
|
+
import type { TabMap } from './doc.js';
|
|
59
|
+
import {
|
|
60
|
+
DARK_TOKEN_STYLES,
|
|
61
|
+
isDarkBackground,
|
|
62
|
+
LIGHT_TOKEN_STYLES,
|
|
63
|
+
tokenStyleFor,
|
|
64
|
+
} from '../code-language/theme.js';
|
|
65
|
+
import type {
|
|
66
|
+
Diagnostic,
|
|
67
|
+
Language,
|
|
68
|
+
Position,
|
|
69
|
+
Selection,
|
|
70
|
+
Token,
|
|
71
|
+
TokenStyles,
|
|
72
|
+
Tokenizer,
|
|
73
|
+
} from '../code-language/types.js';
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The element name — registration key, `kind`, and JSX tag, one string.
|
|
77
|
+
* react-x11 rejects a node whose `kind` is not the name it was registered
|
|
78
|
+
* under, because `kind` is what paint order, the test queries and the DEV
|
|
79
|
+
* assertion all match on. One constant, used in all three places.
|
|
80
|
+
*/
|
|
81
|
+
export const ELEMENT = 'codeeditor';
|
|
82
|
+
|
|
83
|
+
const UNDO_LIMIT = 200;
|
|
84
|
+
const DEFAULT_ROWS = 6;
|
|
85
|
+
const PREFERRED_WIDTH = 360;
|
|
86
|
+
const GUTTER_PAD = 8; // each side of the line numbers
|
|
87
|
+
const CARET_MARGIN = 24; // keep this many px visible beside the caret
|
|
88
|
+
const MATCH_SCAN_LIMIT = 20000; // chars examined looking for a bracket match
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The editor's imperative surface — what `ev.target` and the component's
|
|
92
|
+
* `ref` are typed as. An interface rather than the node class on purpose:
|
|
93
|
+
* the class has private state, which makes its type nominal, and a nominal
|
|
94
|
+
* type inside the exported props would stop `src/` and `dist/` declarations
|
|
95
|
+
* (both live in this repo's typecheck program) merging their JSX
|
|
96
|
+
* augmentations. The full retained node is underneath — cast to
|
|
97
|
+
* {@link CodeEditorNode} for `abs`, `style` and the rest of the `Node`
|
|
98
|
+
* surface.
|
|
99
|
+
*/
|
|
100
|
+
export interface CodeEditorHandle {
|
|
101
|
+
/** Read the text; assign to set it *without* an `onChange`, the
|
|
102
|
+
* DOM-input contract form libraries rely on. */
|
|
103
|
+
value: string;
|
|
104
|
+
readonly name: string | undefined;
|
|
105
|
+
readonly selection: Selection;
|
|
106
|
+
readonly lines: readonly string[];
|
|
107
|
+
readonly language: Language | null;
|
|
108
|
+
readonly canUndo: boolean;
|
|
109
|
+
readonly canRedo: boolean;
|
|
110
|
+
selectedText(): string;
|
|
111
|
+
replaceRange(from: Position, to: Position, text: string): void;
|
|
112
|
+
insertText(text: string): void;
|
|
113
|
+
moveCaret(pos: Position, extend: boolean): void;
|
|
114
|
+
select(anchor: Position, head: Position): void;
|
|
115
|
+
selectAll(): void;
|
|
116
|
+
undo(): void;
|
|
117
|
+
redo(): void;
|
|
118
|
+
indentSelection(dir: 1 | -1): void;
|
|
119
|
+
toggleLineComment(): void;
|
|
120
|
+
copySelection(selection?: string): void;
|
|
121
|
+
pasteFrom(selection?: string): void;
|
|
122
|
+
scrollBy(dx: number, dy: number): boolean;
|
|
123
|
+
caretRect(): { x: number; y: number; height: number };
|
|
124
|
+
focus(): void;
|
|
125
|
+
blur(): void;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The value/submit/selection event the component's callbacks receive —
|
|
129
|
+
* the same shape core's `<textinput>` events have, minus dispatch. */
|
|
130
|
+
export interface CodeEditorEvent {
|
|
131
|
+
type: 'change' | 'submit' | 'selectionchange';
|
|
132
|
+
value: string;
|
|
133
|
+
name: string | undefined;
|
|
134
|
+
selection: Selection;
|
|
135
|
+
target: CodeEditorHandle;
|
|
136
|
+
currentTarget: CodeEditorHandle;
|
|
137
|
+
nativeEvent: unknown;
|
|
138
|
+
defaultPrevented: boolean;
|
|
139
|
+
preventDefault(): void;
|
|
140
|
+
stopPropagation(): void;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The props `<codeeditor>` takes. The component re-exports this. */
|
|
144
|
+
export interface CodeEditorProps {
|
|
145
|
+
/** Controlled text (with `onChange`). */
|
|
146
|
+
value?: string;
|
|
147
|
+
/** Uncontrolled initial text. */
|
|
148
|
+
defaultValue?: string;
|
|
149
|
+
onChange?: (ev: CodeEditorEvent) => void;
|
|
150
|
+
/** Ctrl+Enter. */
|
|
151
|
+
onSubmit?: (ev: CodeEditorEvent) => void;
|
|
152
|
+
/** Caret or selection moved — what completion UIs track. */
|
|
153
|
+
onSelectionChange?: (ev: CodeEditorEvent) => void;
|
|
154
|
+
/** Field name, echoed on events. */
|
|
155
|
+
name?: string;
|
|
156
|
+
/** The language seam. `null`/absent paints plain text. */
|
|
157
|
+
language?: Language | null;
|
|
158
|
+
/** Token type → colour/weight/italic. Default {@link LIGHT_TOKEN_STYLES}. */
|
|
159
|
+
tokenStyles?: TokenStyles;
|
|
160
|
+
/** Ranges to underline (LSP-shaped). */
|
|
161
|
+
diagnostics?: readonly Diagnostic[];
|
|
162
|
+
readOnly?: boolean;
|
|
163
|
+
/** Inert: no default action runs, and the component also stops making it
|
|
164
|
+
* focusable. `readOnly` still navigates and copies; this does neither. */
|
|
165
|
+
disabled?: boolean;
|
|
166
|
+
placeholder?: string;
|
|
167
|
+
placeholderColor?: string;
|
|
168
|
+
/** Preferred height in text lines (default 6). */
|
|
169
|
+
rows?: number;
|
|
170
|
+
/** Tab display width and indent size (default 4). */
|
|
171
|
+
tabSize?: number;
|
|
172
|
+
/** Indent with spaces (default) or a real tab. */
|
|
173
|
+
insertSpaces?: boolean;
|
|
174
|
+
lineNumbers?: boolean;
|
|
175
|
+
/** Tint the caret's line. */
|
|
176
|
+
activeLine?: boolean;
|
|
177
|
+
/** Highlight the bracket pair around the caret (default true). */
|
|
178
|
+
matchBrackets?: boolean;
|
|
179
|
+
selectionColor?: string;
|
|
180
|
+
caretColor?: string;
|
|
181
|
+
gutterColor?: string;
|
|
182
|
+
gutterBackground?: string;
|
|
183
|
+
activeLineColor?: string;
|
|
184
|
+
matchingBracketColor?: string;
|
|
185
|
+
style?: Style | Style[];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// --- structural types for runtime surfaces that have no public declarations
|
|
189
|
+
//
|
|
190
|
+
// Same policy as `desktop-calendar/ical.ts`: describe what is used, locally,
|
|
191
|
+
// rather than reaching into another package's internal type graph.
|
|
192
|
+
|
|
193
|
+
/** The slice of ntk's TextLayout the editor reads. */
|
|
194
|
+
interface LayoutLike {
|
|
195
|
+
width: number;
|
|
196
|
+
height: number;
|
|
197
|
+
draw(ctx: unknown, x: number, y: number): void;
|
|
198
|
+
caretPosition(cp: number): { x: number; y: number; height: number };
|
|
199
|
+
indexAt(x: number, y: number): number;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
interface FontsLike {
|
|
203
|
+
layout(
|
|
204
|
+
content: string | Array<Record<string, unknown>>,
|
|
205
|
+
style: Record<string, unknown>,
|
|
206
|
+
options?: Record<string, unknown>,
|
|
207
|
+
): LayoutLike;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** ntk's clipboard, reached through the public `app` — write/read are what
|
|
211
|
+
* core's own fields call. Absent on older ntk; every use is optional. */
|
|
212
|
+
interface ClipboardLike {
|
|
213
|
+
write(text: string, options?: { selection?: string }): Promise<unknown>;
|
|
214
|
+
read(options?: { selection?: string }): Promise<unknown>;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
interface HistoryEntry {
|
|
218
|
+
value: string;
|
|
219
|
+
caret: Position;
|
|
220
|
+
anchor: Position;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const BRACKETS: Record<string, { match: string; dir: 1 | -1 }> = {
|
|
224
|
+
'(': { match: ')', dir: 1 },
|
|
225
|
+
'[': { match: ']', dir: 1 },
|
|
226
|
+
'{': { match: '}', dir: 1 },
|
|
227
|
+
')': { match: '(', dir: -1 },
|
|
228
|
+
']': { match: '[', dir: -1 },
|
|
229
|
+
'}': { match: '{', dir: -1 },
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/** Token types the bracket matcher must not look inside. */
|
|
233
|
+
const OPAQUE_TOKENS = new Set(['string', 'string2', 'comment', 'docComment']);
|
|
234
|
+
|
|
235
|
+
const SEVERITY_COLORS: Record<string, string> = {
|
|
236
|
+
error: '#e5484d',
|
|
237
|
+
warning: '#d5a021',
|
|
238
|
+
information: '#3b82f6',
|
|
239
|
+
hint: '#8b8f98',
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
function cpToUtf16(text: string, cp: number): number {
|
|
243
|
+
let i = 0;
|
|
244
|
+
for (let n = 0; n < cp && i < text.length; n++) {
|
|
245
|
+
const code = text.codePointAt(i)!;
|
|
246
|
+
i += code > 0xffff ? 2 : 1;
|
|
247
|
+
}
|
|
248
|
+
return i;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function utf16ToCp(text: string, index: number): number {
|
|
252
|
+
let cp = 0;
|
|
253
|
+
for (let i = 0; i < index && i < text.length;) {
|
|
254
|
+
const code = text.codePointAt(i)!;
|
|
255
|
+
i += code > 0xffff ? 2 : 1;
|
|
256
|
+
cp++;
|
|
257
|
+
}
|
|
258
|
+
return cp;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** What the component's handlers pass in — the public synthetic event
|
|
262
|
+
* surface (docs/events.md), structurally. */
|
|
263
|
+
export interface EditorKeyEvent {
|
|
264
|
+
keysym?: number;
|
|
265
|
+
codepoint?: number;
|
|
266
|
+
key?: string;
|
|
267
|
+
shiftKey: boolean;
|
|
268
|
+
ctrlKey: boolean;
|
|
269
|
+
nativeEvent?: unknown;
|
|
270
|
+
defaultPrevented?: boolean;
|
|
271
|
+
/** Consumes the key: what it suppresses is the default action *after*
|
|
272
|
+
* this one, which for Tab is the focus cycle. */
|
|
273
|
+
preventDefault?(): void;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export interface EditorMouseEvent {
|
|
277
|
+
x: number;
|
|
278
|
+
y: number;
|
|
279
|
+
button?: number;
|
|
280
|
+
detail?: number;
|
|
281
|
+
shiftKey: boolean;
|
|
282
|
+
capturePointer?(): void;
|
|
283
|
+
releasePointer?(): void;
|
|
284
|
+
preventDefault?(): void;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
interface LineCacheEntry {
|
|
288
|
+
display: string;
|
|
289
|
+
tokens: readonly Token[];
|
|
290
|
+
styleKey: string;
|
|
291
|
+
layout: LayoutLike | null;
|
|
292
|
+
map: TabMap;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export class CodeEditorNode extends Node implements CodeEditorHandle {
|
|
296
|
+
private _lines: string[];
|
|
297
|
+
/** What `_lines` was last built from — how a controlled `value` that the
|
|
298
|
+
* parent refused to update snaps back on the next read (see `_syncProps`). */
|
|
299
|
+
private _synced: string;
|
|
300
|
+
private _caret: Position = { line: 0, ch: 0 };
|
|
301
|
+
private _anchor: Position = { line: 0, ch: 0 };
|
|
302
|
+
private _scrollX = 0;
|
|
303
|
+
private _scrollY = 0;
|
|
304
|
+
private _focused = false;
|
|
305
|
+
private _caretOn = false;
|
|
306
|
+
private _blinkTimer: TimerId = null;
|
|
307
|
+
private _goalX: number | null = null;
|
|
308
|
+
private _drag: 'char' | 'word' | 'line' | null = null;
|
|
309
|
+
private _dragOrigin: Position = { line: 0, ch: 0 };
|
|
310
|
+
/** Escape was the last key, so the next Tab leaves instead of indenting —
|
|
311
|
+
* the editor's way of not being a keyboard trap (see `defaultKeyDown`). */
|
|
312
|
+
private _tabEscapes = false;
|
|
313
|
+
private _history: HistoryEntry[];
|
|
314
|
+
private _historyIndex = 0;
|
|
315
|
+
private _undoRun: string | null = null;
|
|
316
|
+
private _pendingValue: string | null = null;
|
|
317
|
+
private _keyNative: unknown = null;
|
|
318
|
+
private _tok: Tokenizer | null = null;
|
|
319
|
+
private _tokLanguage: Language | null | undefined;
|
|
320
|
+
private _lineCache = new Map<number, LineCacheEntry>();
|
|
321
|
+
private _metricsKey = '';
|
|
322
|
+
private _lineH = 0;
|
|
323
|
+
private _charW = 0;
|
|
324
|
+
private _widest = 0;
|
|
325
|
+
|
|
326
|
+
constructor(props: Record<string, unknown>, app: unknown) {
|
|
327
|
+
super(ELEMENT, props, app as ConstructorParameters<typeof Node>[2]);
|
|
328
|
+
const initial =
|
|
329
|
+
props.value != null
|
|
330
|
+
? String(props.value)
|
|
331
|
+
: props.defaultValue != null
|
|
332
|
+
? String(props.defaultValue)
|
|
333
|
+
: '';
|
|
334
|
+
this._lines = initial.split('\n');
|
|
335
|
+
this._synced = initial;
|
|
336
|
+
this._history = [
|
|
337
|
+
{ value: initial, caret: this._caret, anchor: this._anchor },
|
|
338
|
+
];
|
|
339
|
+
// Without this nothing focuses the editor and no key ever reaches it —
|
|
340
|
+
// an app's `focusable`/`tabIndex` prop still overrides either way, which
|
|
341
|
+
// is how `<CodeEditor disabled>` stays out of the tab order.
|
|
342
|
+
this.focusableByDefault = true;
|
|
343
|
+
this.defaultCursor = 'text';
|
|
344
|
+
// The editor's selection is its own (react-x11#291). A `selectable`
|
|
345
|
+
// document around it — a `<Markdown>` with a live editor in it, a form
|
|
346
|
+
// on a selectable pane — skips this subtree whole rather than lighting
|
|
347
|
+
// up half the text somebody is typing in, and a press here never
|
|
348
|
+
// reaches the surface above. That is the same rule `<textinput>` keeps,
|
|
349
|
+
// and the reason the `default*` handlers below can own the gesture
|
|
350
|
+
// without calling `super`.
|
|
351
|
+
this.hasOwnSelection = true;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* The editor's own size: a comfortable width, never wider than what is on
|
|
356
|
+
* offer, and `rows` text lines tall. An unbounded axis arrives as
|
|
357
|
+
* `Infinity`, so the `Math.min` is the whole rule. The height comes from a
|
|
358
|
+
* prop rather than from the style, which is what makes the
|
|
359
|
+
* `invalidateMeasure` in `applyProps` necessary.
|
|
360
|
+
*/
|
|
361
|
+
override measureContent({ width }: MeasureConstraints): MeasuredSize {
|
|
362
|
+
const rows = Math.max(1, Number(this.props.rows ?? DEFAULT_ROWS));
|
|
363
|
+
return {
|
|
364
|
+
width: Math.min(PREFERRED_WIDTH, width),
|
|
365
|
+
height: Math.ceil(this._lineHeight() * rows),
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// --- value & props sync --------------------------------------------------
|
|
370
|
+
|
|
371
|
+
get value(): string {
|
|
372
|
+
if (this._pendingValue !== null) return this._pendingValue;
|
|
373
|
+
if (this.props.value != null) return String(this.props.value);
|
|
374
|
+
return this._lines.join('\n');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Programmatic set, no `onChange` — the DOM-input contract form libraries
|
|
378
|
+
* rely on. On a controlled editor `props.value` wins the next render. */
|
|
379
|
+
set value(next: string) {
|
|
380
|
+
const text = next == null ? '' : String(next);
|
|
381
|
+
if (text === this._lines.join('\n')) return;
|
|
382
|
+
this._setText(text);
|
|
383
|
+
this._noteExternal();
|
|
384
|
+
this._repaint();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
get name(): string | undefined {
|
|
388
|
+
return this.props.name as string | undefined;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
get selection(): Selection {
|
|
392
|
+
return { anchor: { ...this._anchor }, head: { ...this._caret } };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
get lines(): readonly string[] {
|
|
396
|
+
this._syncProps();
|
|
397
|
+
return this._lines;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
get language(): Language | null {
|
|
401
|
+
return (this.props.language as Language | null | undefined) ?? null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Pull a controlled `value` into the model. Called at the top of every
|
|
406
|
+
* command and paint: a parent that ignored an `onChange` never re-renders,
|
|
407
|
+
* so there is no `applyProps` moment to catch the divergence — the next
|
|
408
|
+
* read is that moment.
|
|
409
|
+
*/
|
|
410
|
+
private _syncProps(): void {
|
|
411
|
+
const v = this.props.value;
|
|
412
|
+
if (v == null) return;
|
|
413
|
+
const text = String(v);
|
|
414
|
+
if (text === this._synced) return;
|
|
415
|
+
this._setText(text);
|
|
416
|
+
this._noteExternal();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private _setText(text: string): void {
|
|
420
|
+
this._lines = text.split('\n');
|
|
421
|
+
this._synced = text;
|
|
422
|
+
this._caret = clampPos(this._lines, this._caret);
|
|
423
|
+
this._anchor = clampPos(this._lines, this._anchor);
|
|
424
|
+
this._lineCache.clear();
|
|
425
|
+
this._widest = 0;
|
|
426
|
+
this._tokenizer()?.setLines(this._lines);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// --- tokenizer -----------------------------------------------------------
|
|
430
|
+
|
|
431
|
+
private _tokenizer(): Tokenizer | null {
|
|
432
|
+
const lang = this.language;
|
|
433
|
+
if (lang !== this._tokLanguage) {
|
|
434
|
+
this._tok?.dispose?.();
|
|
435
|
+
this._tokLanguage = lang;
|
|
436
|
+
this._tok = lang
|
|
437
|
+
? lang.createTokenizer({
|
|
438
|
+
invalidate: (fromLine) => {
|
|
439
|
+
for (const key of this._lineCache.keys()) {
|
|
440
|
+
if (key >= fromLine) this._lineCache.delete(key);
|
|
441
|
+
}
|
|
442
|
+
this._repaint();
|
|
443
|
+
},
|
|
444
|
+
})
|
|
445
|
+
: null;
|
|
446
|
+
this._tok?.setLines(this._lines);
|
|
447
|
+
this._lineCache.clear();
|
|
448
|
+
}
|
|
449
|
+
return this._tok;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// --- metrics & style -----------------------------------------------------
|
|
453
|
+
|
|
454
|
+
private _textStyle(): Record<string, unknown> {
|
|
455
|
+
const s = this.style as Record<string, unknown>;
|
|
456
|
+
return {
|
|
457
|
+
family: (s.fontFamily as string) ?? 'monospace',
|
|
458
|
+
size: (s.fontSize as number) ?? 13,
|
|
459
|
+
weight: (s.fontWeight as number) ?? 400,
|
|
460
|
+
style: (s.fontStyle as string) ?? 'normal',
|
|
461
|
+
// theme text before hardcoded ink: on a dark desktop the palette's
|
|
462
|
+
// `text` is light, and a near-black fallback would vanish — the
|
|
463
|
+
// component's default style also asks for `color: '$text'`, so this
|
|
464
|
+
// branch is for a bare <codeeditor> under a themeless tree
|
|
465
|
+
color: (s.color as string) ?? this._themeColor('text') ?? '#24292f',
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private _themeColor(token: string): string | undefined {
|
|
470
|
+
const theme = this.theme as Record<string, unknown> | null;
|
|
471
|
+
const v = theme?.[token];
|
|
472
|
+
return typeof v === 'string' ? v : undefined;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
private _fonts(): FontsLike | null {
|
|
476
|
+
return (
|
|
477
|
+
(this.app as { fonts?: FontsLike } | null | undefined)?.fonts ?? null
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
private _metrics(): void {
|
|
482
|
+
const s = this._textStyle();
|
|
483
|
+
const key = `${s.family}|${s.size}|${s.weight}`;
|
|
484
|
+
if (key === this._metricsKey && this._lineH > 0) return;
|
|
485
|
+
const fonts = this._fonts();
|
|
486
|
+
this._metricsKey = key;
|
|
487
|
+
if (!fonts) {
|
|
488
|
+
this._lineH = Number(s.size) * 1.4;
|
|
489
|
+
this._charW = Number(s.size) * 0.6;
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
const probe = fonts.layout('Mg', s);
|
|
493
|
+
this._lineH = probe.height || Number(s.size) * 1.4;
|
|
494
|
+
this._charW = fonts.layout('0', s).width || Number(s.size) * 0.6;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
private _lineHeight(): number {
|
|
498
|
+
this._metrics();
|
|
499
|
+
return this._lineH;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* The content box: `abs` inset by border and padding, computed from the
|
|
504
|
+
* flattened style. [react-x11 gap] built-in fields read an internal
|
|
505
|
+
* `contentBox()`; a public equivalent would remove this arithmetic —
|
|
506
|
+
* filed as react-x11#254.
|
|
507
|
+
*/
|
|
508
|
+
private _contentRect(): {
|
|
509
|
+
x: number;
|
|
510
|
+
y: number;
|
|
511
|
+
width: number;
|
|
512
|
+
height: number;
|
|
513
|
+
} {
|
|
514
|
+
const s = this.style as Record<string, unknown>;
|
|
515
|
+
const num = (v: unknown): number => (typeof v === 'number' ? v : 0);
|
|
516
|
+
const border = num(s.borderWidth);
|
|
517
|
+
const pad = num(s.padding);
|
|
518
|
+
const top = num(s.borderTopWidth ?? border) + num(s.paddingTop ?? pad);
|
|
519
|
+
const right =
|
|
520
|
+
num(s.borderRightWidth ?? border) + num(s.paddingRight ?? pad);
|
|
521
|
+
const bottom =
|
|
522
|
+
num(s.borderBottomWidth ?? border) + num(s.paddingBottom ?? pad);
|
|
523
|
+
const left = num(s.borderLeftWidth ?? border) + num(s.paddingLeft ?? pad);
|
|
524
|
+
const { x, y, width, height } = this.abs;
|
|
525
|
+
return {
|
|
526
|
+
x: x + left,
|
|
527
|
+
y: y + top,
|
|
528
|
+
width: Math.max(0, width - left - right),
|
|
529
|
+
height: Math.max(0, height - top - bottom),
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
private _gutterWidth(): number {
|
|
534
|
+
if (!this.props.lineNumbers) return 0;
|
|
535
|
+
this._metrics();
|
|
536
|
+
const digits = Math.max(2, String(this._lines.length).length);
|
|
537
|
+
return Math.ceil(digits * this._charW) + GUTTER_PAD * 2;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
private _tabSize(): number {
|
|
541
|
+
const n = Number(this.props.tabSize ?? 4);
|
|
542
|
+
return Number.isFinite(n) && n >= 1 ? Math.floor(n) : 4;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// --- per-line layout -----------------------------------------------------
|
|
546
|
+
|
|
547
|
+
private _resolveColor(color: string | undefined): string | undefined {
|
|
548
|
+
if (!color) return color;
|
|
549
|
+
if (color.startsWith('$')) return this._themeColor(color.slice(1));
|
|
550
|
+
return color;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* The token palette in force: the prop when given, else whichever
|
|
555
|
+
* built-in set reads on this editor's background — the resolved
|
|
556
|
+
* `backgroundColor` when it is a judgeable colour, the theme's
|
|
557
|
+
* `background` otherwise. This is what makes one `<CodeEditor>` legible
|
|
558
|
+
* on a light *and* a dark desktop with no props at all.
|
|
559
|
+
*/
|
|
560
|
+
private _tokenStyles(): TokenStyles {
|
|
561
|
+
const explicit = this.props.tokenStyles as TokenStyles | undefined;
|
|
562
|
+
if (explicit) return explicit;
|
|
563
|
+
const s = this.style as Record<string, unknown>;
|
|
564
|
+
const own = isDarkBackground(s.backgroundColor as string | undefined);
|
|
565
|
+
const dark = own ?? isDarkBackground(this._themeColor('background'));
|
|
566
|
+
return dark ? DARK_TOKEN_STYLES : LIGHT_TOKEN_STYLES;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/** Shared by the paint defaults that need a light/dark answer. */
|
|
570
|
+
private _onDark(): boolean {
|
|
571
|
+
const s = this.style as Record<string, unknown>;
|
|
572
|
+
return (
|
|
573
|
+
(isDarkBackground(s.backgroundColor as string | undefined) ??
|
|
574
|
+
isDarkBackground(this._themeColor('background'))) === true
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
private _lineEntry(line: number): LineCacheEntry {
|
|
579
|
+
const raw = this._lines[line];
|
|
580
|
+
const map = tabMap(raw, this._tabSize());
|
|
581
|
+
const tokens = this._tokenizer()?.lineTokens(line) ?? [];
|
|
582
|
+
const styleKey = this._metricsKey;
|
|
583
|
+
const cached = this._lineCache.get(line);
|
|
584
|
+
if (
|
|
585
|
+
cached &&
|
|
586
|
+
cached.display === map.display &&
|
|
587
|
+
cached.tokens === tokens &&
|
|
588
|
+
cached.styleKey === styleKey
|
|
589
|
+
) {
|
|
590
|
+
return cached;
|
|
591
|
+
}
|
|
592
|
+
const fonts = this._fonts();
|
|
593
|
+
const base = this._textStyle();
|
|
594
|
+
let layout: LayoutLike | null = null;
|
|
595
|
+
if (fonts) {
|
|
596
|
+
const styles = this._tokenStyles();
|
|
597
|
+
const spans: Array<Record<string, unknown>> = [];
|
|
598
|
+
let cursor = 0;
|
|
599
|
+
for (const t of tokens) {
|
|
600
|
+
const from = map.toDisplay(t.from);
|
|
601
|
+
const to = map.toDisplay(t.to);
|
|
602
|
+
if (from > cursor) {
|
|
603
|
+
spans.push({ text: map.display.slice(cursor, from) });
|
|
604
|
+
}
|
|
605
|
+
if (to > from) {
|
|
606
|
+
const st = tokenStyleFor(styles, t.type);
|
|
607
|
+
const span: Record<string, unknown> = {
|
|
608
|
+
text: map.display.slice(from, to),
|
|
609
|
+
};
|
|
610
|
+
if (st) {
|
|
611
|
+
const color = this._resolveColor(st.color);
|
|
612
|
+
if (color) span.color = color;
|
|
613
|
+
if (st.weight) span.weight = st.weight;
|
|
614
|
+
if (st.fontStyle) span.style = st.fontStyle;
|
|
615
|
+
}
|
|
616
|
+
spans.push(span);
|
|
617
|
+
}
|
|
618
|
+
cursor = Math.max(cursor, to);
|
|
619
|
+
}
|
|
620
|
+
if (cursor < map.display.length) {
|
|
621
|
+
spans.push({ text: map.display.slice(cursor) });
|
|
622
|
+
}
|
|
623
|
+
layout = fonts.layout(spans.length > 0 ? spans : [{ text: '' }], base);
|
|
624
|
+
}
|
|
625
|
+
const entry: LineCacheEntry = {
|
|
626
|
+
display: map.display,
|
|
627
|
+
tokens,
|
|
628
|
+
styleKey,
|
|
629
|
+
layout,
|
|
630
|
+
map,
|
|
631
|
+
};
|
|
632
|
+
this._lineCache.set(line, entry);
|
|
633
|
+
if (layout && layout.width > this._widest) this._widest = layout.width;
|
|
634
|
+
return entry;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/** Caret x (display pixels from the text origin) for a position. */
|
|
638
|
+
private _caretX(pos: Position): number {
|
|
639
|
+
const entry = this._lineEntry(pos.line);
|
|
640
|
+
if (!entry.layout) return 0;
|
|
641
|
+
const disp = entry.map.toDisplay(pos.ch);
|
|
642
|
+
return entry.layout.caretPosition(utf16ToCp(entry.display, disp)).x;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/** Window coordinates → document position. */
|
|
646
|
+
private _posAt(x: number, y: number): Position {
|
|
647
|
+
this._metrics();
|
|
648
|
+
const content = this._contentRect();
|
|
649
|
+
const textX = content.x + this._gutterWidth();
|
|
650
|
+
const line = Math.max(
|
|
651
|
+
0,
|
|
652
|
+
Math.min(
|
|
653
|
+
this._lines.length - 1,
|
|
654
|
+
Math.floor((y - content.y + this._scrollY) / this._lineH),
|
|
655
|
+
),
|
|
656
|
+
);
|
|
657
|
+
const entry = this._lineEntry(line);
|
|
658
|
+
if (!entry.layout) return { line, ch: 0 };
|
|
659
|
+
const cp = entry.layout.indexAt(x - textX + this._scrollX, 0);
|
|
660
|
+
const disp = cpToUtf16(entry.display, cp);
|
|
661
|
+
return clampPos(this._lines, { line, ch: entry.map.toRaw(disp) });
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/** Font facts the completion popup sizes itself with. */
|
|
665
|
+
metrics(): {
|
|
666
|
+
lineHeight: number;
|
|
667
|
+
charWidth: number;
|
|
668
|
+
family: string;
|
|
669
|
+
size: number;
|
|
670
|
+
} {
|
|
671
|
+
this._metrics();
|
|
672
|
+
const s = this._textStyle();
|
|
673
|
+
return {
|
|
674
|
+
lineHeight: this._lineH,
|
|
675
|
+
charWidth: this._charW,
|
|
676
|
+
family: String(s.family),
|
|
677
|
+
size: Number(s.size),
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/** Text width in this editor's font. The built-in completion popup no
|
|
682
|
+
* longer needs it — a `<popup anchor>` measures its own rows — but an app
|
|
683
|
+
* laying out its own overlay against the text still does. */
|
|
684
|
+
measureText(text: string): number {
|
|
685
|
+
const fonts = this._fonts();
|
|
686
|
+
if (!fonts) return text.length * this._charW;
|
|
687
|
+
return fonts.layout(text, this._textStyle()).width;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/** Caret geometry relative to this node's origin — node coordinates, which
|
|
691
|
+
* is exactly what a `<popup anchor={{at}}>` takes, so the completion popup
|
|
692
|
+
* hands this straight through and nothing here has to know where the
|
|
693
|
+
* editor sits on screen. */
|
|
694
|
+
caretRect(): { x: number; y: number; height: number } {
|
|
695
|
+
this._syncProps();
|
|
696
|
+
this._metrics();
|
|
697
|
+
const content = this._contentRect();
|
|
698
|
+
const textX = content.x + this._gutterWidth() - this.abs.x;
|
|
699
|
+
return {
|
|
700
|
+
x: textX + this._caretX(this._caret) - this._scrollX,
|
|
701
|
+
y:
|
|
702
|
+
content.y - this.abs.y + this._caret.line * this._lineH - this._scrollY,
|
|
703
|
+
height: this._lineH,
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// --- scrolling -----------------------------------------------------------
|
|
708
|
+
|
|
709
|
+
private _maxScrollY(): number {
|
|
710
|
+
const content = this._contentRect();
|
|
711
|
+
return Math.max(
|
|
712
|
+
0,
|
|
713
|
+
this._lines.length * this._lineHeight() - content.height,
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
private _maxScrollX(): number {
|
|
718
|
+
const content = this._contentRect();
|
|
719
|
+
const textW = content.width - this._gutterWidth();
|
|
720
|
+
return Math.max(0, this._widest + CARET_MARGIN - textW);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/** Wheel notches (deltaX/deltaY arrive as ±1 per notch) → pixels. */
|
|
724
|
+
handleWheel(ev: { deltaX: number; deltaY: number }): boolean {
|
|
725
|
+
const step = this._lineHeight() * 3;
|
|
726
|
+
return this.scrollBy(ev.deltaX * step, ev.deltaY * step);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/** Returns true when the offset moved (the component consumes the wheel
|
|
730
|
+
* event exactly then, so an unscrollable editor lets the page scroll). */
|
|
731
|
+
scrollBy(dx: number, dy: number): boolean {
|
|
732
|
+
const nx = Math.max(0, Math.min(this._scrollX + dx, this._maxScrollX()));
|
|
733
|
+
const ny = Math.max(0, Math.min(this._scrollY + dy, this._maxScrollY()));
|
|
734
|
+
if (nx === this._scrollX && ny === this._scrollY) return false;
|
|
735
|
+
this._scrollX = nx;
|
|
736
|
+
this._scrollY = ny;
|
|
737
|
+
this.root?.invalidate(false, this.abs, 'scroll');
|
|
738
|
+
return true;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
private _ensureCaretVisible(): void {
|
|
742
|
+
this._metrics();
|
|
743
|
+
const content = this._contentRect();
|
|
744
|
+
const lineTop = this._caret.line * this._lineH;
|
|
745
|
+
if (lineTop < this._scrollY) this._scrollY = lineTop;
|
|
746
|
+
const lineBottom = lineTop + this._lineH;
|
|
747
|
+
if (lineBottom > this._scrollY + content.height) {
|
|
748
|
+
this._scrollY = lineBottom - content.height;
|
|
749
|
+
}
|
|
750
|
+
const textW = content.width - this._gutterWidth();
|
|
751
|
+
const x = this._caretX(this._caret);
|
|
752
|
+
if (x < this._scrollX + CARET_MARGIN) {
|
|
753
|
+
this._scrollX = Math.max(0, x - CARET_MARGIN);
|
|
754
|
+
}
|
|
755
|
+
if (x > this._scrollX + textW - CARET_MARGIN) {
|
|
756
|
+
this._scrollX = x - textW + CARET_MARGIN;
|
|
757
|
+
}
|
|
758
|
+
this._scrollY = Math.max(0, Math.min(this._scrollY, this._maxScrollY()));
|
|
759
|
+
this._scrollX = Math.max(0, this._scrollX);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// --- events out ----------------------------------------------------------
|
|
763
|
+
|
|
764
|
+
private _makeEvent(type: CodeEditorEvent['type']): CodeEditorEvent {
|
|
765
|
+
const self = this;
|
|
766
|
+
const ev: CodeEditorEvent = {
|
|
767
|
+
type,
|
|
768
|
+
value: this.value,
|
|
769
|
+
name: this.name,
|
|
770
|
+
selection: this.selection,
|
|
771
|
+
target: self,
|
|
772
|
+
currentTarget: self,
|
|
773
|
+
nativeEvent: this._keyNative ?? null,
|
|
774
|
+
defaultPrevented: false,
|
|
775
|
+
preventDefault() {
|
|
776
|
+
ev.defaultPrevented = true;
|
|
777
|
+
},
|
|
778
|
+
stopPropagation() {},
|
|
779
|
+
};
|
|
780
|
+
return ev;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
private _fire(
|
|
784
|
+
prop: 'onChange' | 'onSubmit' | 'onSelectionChange',
|
|
785
|
+
value: string,
|
|
786
|
+
): void {
|
|
787
|
+
const handler = this.props[prop] as
|
|
788
|
+
((ev: CodeEditorEvent) => void) | undefined;
|
|
789
|
+
if (!handler) return;
|
|
790
|
+
const previous = this._pendingValue;
|
|
791
|
+
this._pendingValue = value;
|
|
792
|
+
try {
|
|
793
|
+
handler(
|
|
794
|
+
this._makeEvent(
|
|
795
|
+
prop === 'onSubmit'
|
|
796
|
+
? 'submit'
|
|
797
|
+
: prop === 'onSelectionChange'
|
|
798
|
+
? 'selectionchange'
|
|
799
|
+
: 'change',
|
|
800
|
+
),
|
|
801
|
+
);
|
|
802
|
+
} finally {
|
|
803
|
+
this._pendingValue = previous;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
private _repaint(): void {
|
|
808
|
+
this._caretOn = true;
|
|
809
|
+
this.root?.invalidate(false, this.abs, 'text');
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// --- history -------------------------------------------------------------
|
|
813
|
+
|
|
814
|
+
private _noteExternal(): void {
|
|
815
|
+
this._undoRun = null;
|
|
816
|
+
const value = this._lines.join('\n');
|
|
817
|
+
const top = this._history[this._historyIndex];
|
|
818
|
+
if (top && top.value === value) return;
|
|
819
|
+
this._pushHistory({ value, caret: this._caret, anchor: this._anchor });
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
private _pushHistory(entry: HistoryEntry): void {
|
|
823
|
+
this._history.length = this._historyIndex + 1;
|
|
824
|
+
this._history.push(entry);
|
|
825
|
+
if (this._history.length > UNDO_LIMIT) {
|
|
826
|
+
this._history.splice(0, this._history.length - UNDO_LIMIT);
|
|
827
|
+
}
|
|
828
|
+
this._historyIndex = this._history.length - 1;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
breakUndoRun(): void {
|
|
832
|
+
this._undoRun = null;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
get canUndo(): boolean {
|
|
836
|
+
return this._historyIndex > 0;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
get canRedo(): boolean {
|
|
840
|
+
return this._historyIndex < this._history.length - 1;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
undo(): void {
|
|
844
|
+
this._syncProps();
|
|
845
|
+
if (!this.canUndo) return;
|
|
846
|
+
this._historyIndex--;
|
|
847
|
+
this._applyHistory(this._history[this._historyIndex]);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
redo(): void {
|
|
851
|
+
this._syncProps();
|
|
852
|
+
if (!this.canRedo) return;
|
|
853
|
+
this._historyIndex++;
|
|
854
|
+
this._applyHistory(this._history[this._historyIndex]);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
private _applyHistory(entry: HistoryEntry): void {
|
|
858
|
+
this._undoRun = null;
|
|
859
|
+
this._goalX = null;
|
|
860
|
+
this._applyLines(entry.value.split('\n'), null);
|
|
861
|
+
this._caret = clampPos(this._lines, entry.caret);
|
|
862
|
+
this._anchor = clampPos(this._lines, entry.anchor);
|
|
863
|
+
this._afterEdit(entry.value);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// --- editing core --------------------------------------------------------
|
|
867
|
+
|
|
868
|
+
private _selectionRange(): [Position, Position] {
|
|
869
|
+
return [
|
|
870
|
+
minPos(this._anchor, this._caret),
|
|
871
|
+
maxPos(this._anchor, this._caret),
|
|
872
|
+
];
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
private _hasSelection(): boolean {
|
|
876
|
+
return !posEqual(this._anchor, this._caret);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
override selectedText(): string {
|
|
880
|
+
this._syncProps();
|
|
881
|
+
const [a, b] = this._selectionRange();
|
|
882
|
+
if (posEqual(a, b)) return '';
|
|
883
|
+
if (a.line === b.line) return this._lines[a.line].slice(a.ch, b.ch);
|
|
884
|
+
const parts = [this._lines[a.line].slice(a.ch)];
|
|
885
|
+
for (let i = a.line + 1; i < b.line; i++) parts.push(this._lines[i]);
|
|
886
|
+
parts.push(this._lines[b.line].slice(0, b.ch));
|
|
887
|
+
return parts.join('\n');
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* The one place text changes. Replaces `[from, to)` with `text`, moves the
|
|
892
|
+
* caret to the end of the insertion, fires `onChange`, and manages undo —
|
|
893
|
+
* `run` names a coalescable edit kind (`'type'`, `'delete-back'`,
|
|
894
|
+
* `'delete-forward'`); anything else is its own undo step.
|
|
895
|
+
*/
|
|
896
|
+
replaceRange(
|
|
897
|
+
from: Position,
|
|
898
|
+
to: Position,
|
|
899
|
+
text: string,
|
|
900
|
+
run: string | null = null,
|
|
901
|
+
): void {
|
|
902
|
+
this._syncProps();
|
|
903
|
+
if (this.props.readOnly) return;
|
|
904
|
+
const a = clampPos(this._lines, minPos(from, to));
|
|
905
|
+
const b = clampPos(this._lines, maxPos(from, to));
|
|
906
|
+
const insert = String(text).replace(/\r\n?/g, '\n');
|
|
907
|
+
const before = this._lines[a.line].slice(0, a.ch);
|
|
908
|
+
const after = this._lines[b.line].slice(b.ch);
|
|
909
|
+
const inserted = insert.split('\n');
|
|
910
|
+
inserted[0] = before + inserted[0];
|
|
911
|
+
const caretCh = inserted[inserted.length - 1].length;
|
|
912
|
+
inserted[inserted.length - 1] += after;
|
|
913
|
+
const newLines = this._lines.slice();
|
|
914
|
+
newLines.splice(a.line, b.line - a.line + 1, ...inserted);
|
|
915
|
+
|
|
916
|
+
const coalesce =
|
|
917
|
+
run !== null &&
|
|
918
|
+
run === this._undoRun &&
|
|
919
|
+
this._historyIndex === this._history.length - 1;
|
|
920
|
+
this._applyLines(newLines, {
|
|
921
|
+
fromLine: a.line,
|
|
922
|
+
removed: b.line - a.line + 1,
|
|
923
|
+
inserted: inserted.length,
|
|
924
|
+
});
|
|
925
|
+
this._caret = clampPos(this._lines, {
|
|
926
|
+
line: a.line + inserted.length - 1,
|
|
927
|
+
ch: caretCh,
|
|
928
|
+
});
|
|
929
|
+
this._anchor = { ...this._caret };
|
|
930
|
+
this._goalX = null;
|
|
931
|
+
|
|
932
|
+
const value = this._lines.join('\n');
|
|
933
|
+
if (coalesce) {
|
|
934
|
+
this._history[this._historyIndex] = {
|
|
935
|
+
value,
|
|
936
|
+
caret: this._caret,
|
|
937
|
+
anchor: this._anchor,
|
|
938
|
+
};
|
|
939
|
+
} else {
|
|
940
|
+
this._pushHistory({ value, caret: this._caret, anchor: this._anchor });
|
|
941
|
+
}
|
|
942
|
+
this._undoRun = run;
|
|
943
|
+
this._afterEdit(value);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
private _applyLines(
|
|
947
|
+
newLines: string[],
|
|
948
|
+
edit: { fromLine: number; removed: number; inserted: number } | null,
|
|
949
|
+
): void {
|
|
950
|
+
if (edit) {
|
|
951
|
+
// A tokenizer keeps the array `setLines` handed it (that is the
|
|
952
|
+
// contract in `Tokenizer`) and is told about changes through `edit()`
|
|
953
|
+
// alone, so an incremental edit has to *mutate* the live array —
|
|
954
|
+
// swapping in a fresh one would leave every engine tokenizing the
|
|
955
|
+
// pre-edit text at the new line numbers, painting a split line with
|
|
956
|
+
// its neighbour's colours. Callers only touch the edited span, so
|
|
957
|
+
// splicing that span across reproduces `newLines` exactly.
|
|
958
|
+
this._lines.splice(
|
|
959
|
+
edit.fromLine,
|
|
960
|
+
edit.removed,
|
|
961
|
+
...newLines.slice(edit.fromLine, edit.fromLine + edit.inserted),
|
|
962
|
+
);
|
|
963
|
+
} else {
|
|
964
|
+
this._lines = newLines;
|
|
965
|
+
}
|
|
966
|
+
const tok = this._tokenizer();
|
|
967
|
+
if (edit && tok) {
|
|
968
|
+
tok.edit(edit);
|
|
969
|
+
// line indices at and below the edit moved or changed; drop them
|
|
970
|
+
for (const key of [...this._lineCache.keys()]) {
|
|
971
|
+
if (key >= edit.fromLine) this._lineCache.delete(key);
|
|
972
|
+
}
|
|
973
|
+
if (edit.removed !== edit.inserted) this._lineCache.clear();
|
|
974
|
+
} else {
|
|
975
|
+
tok?.setLines(this._lines);
|
|
976
|
+
this._lineCache.clear();
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
private _afterEdit(value: string): void {
|
|
981
|
+
this._synced = value;
|
|
982
|
+
this._ensureCaretVisible();
|
|
983
|
+
this._repaint();
|
|
984
|
+
this._fire('onChange', value);
|
|
985
|
+
this._fire('onSelectionChange', value);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
insertText(text: string, run: string | null = null): void {
|
|
989
|
+
const [a, b] = this._selectionRange();
|
|
990
|
+
this.replaceRange(a, b, text, this._hasSelection() ? null : run);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
moveCaret(pos: Position, extend: boolean): void {
|
|
994
|
+
this._syncProps();
|
|
995
|
+
this.breakUndoRun();
|
|
996
|
+
this._caret = clampPos(this._lines, pos);
|
|
997
|
+
if (!extend) this._anchor = { ...this._caret };
|
|
998
|
+
this._ensureCaretVisible();
|
|
999
|
+
this._repaint();
|
|
1000
|
+
this._fire('onSelectionChange', this.value);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
select(anchor: Position, head: Position): void {
|
|
1004
|
+
this._syncProps();
|
|
1005
|
+
this.breakUndoRun();
|
|
1006
|
+
this._anchor = clampPos(this._lines, anchor);
|
|
1007
|
+
this._caret = clampPos(this._lines, head);
|
|
1008
|
+
this._ensureCaretVisible();
|
|
1009
|
+
this._repaint();
|
|
1010
|
+
this._fire('onSelectionChange', this.value);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// `: this`, matching the base declaration core grew in #294 — a subclass
|
|
1014
|
+
// narrowing it to void stops being structurally a DrawnNode.
|
|
1015
|
+
override selectAll(): this {
|
|
1016
|
+
const last = this._lines.length - 1;
|
|
1017
|
+
this.select(
|
|
1018
|
+
{ line: 0, ch: 0 },
|
|
1019
|
+
{ line: last, ch: this._lines[last].length },
|
|
1020
|
+
);
|
|
1021
|
+
this._ownPrimary();
|
|
1022
|
+
return this;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// --- clipboard -----------------------------------------------------------
|
|
1026
|
+
|
|
1027
|
+
private _clipboard(): ClipboardLike | null {
|
|
1028
|
+
const app = this.app as { clipboard?: ClipboardLike } | null | undefined;
|
|
1029
|
+
return app?.clipboard ?? null;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
copySelection(selection = 'CLIPBOARD'): void {
|
|
1033
|
+
const text = this.selectedText();
|
|
1034
|
+
if (!text) return;
|
|
1035
|
+
this._clipboard()
|
|
1036
|
+
?.write(text, { selection })
|
|
1037
|
+
.catch(() => {});
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
private _ownPrimary(): void {
|
|
1041
|
+
if (this._hasSelection()) this.copySelection('PRIMARY');
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
pasteFrom(selection = 'CLIPBOARD'): void {
|
|
1045
|
+
this._clipboard()
|
|
1046
|
+
?.read({ selection })
|
|
1047
|
+
.then((text) => {
|
|
1048
|
+
if (!this.destroyed && typeof text === 'string' && text) {
|
|
1049
|
+
this.insertText(text);
|
|
1050
|
+
}
|
|
1051
|
+
})
|
|
1052
|
+
.catch(() => {});
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// --- indentation & comments ---------------------------------------------
|
|
1056
|
+
|
|
1057
|
+
private _indentUnit(): string {
|
|
1058
|
+
return this.props.insertSpaces === false
|
|
1059
|
+
? '\t'
|
|
1060
|
+
: ' '.repeat(this._tabSize());
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/** Tab with a multi-line selection (or dir < 0): shift the lines. */
|
|
1064
|
+
indentSelection(dir: 1 | -1): void {
|
|
1065
|
+
this._syncProps();
|
|
1066
|
+
if (this.props.readOnly) return;
|
|
1067
|
+
const [a, b] = this._selectionRange();
|
|
1068
|
+
const endLine = b.line > a.line && b.ch === 0 ? b.line - 1 : b.line;
|
|
1069
|
+
const unit = this._indentUnit();
|
|
1070
|
+
const size = this._tabSize();
|
|
1071
|
+
const newLines = this._lines.slice();
|
|
1072
|
+
for (let i = a.line; i <= endLine; i++) {
|
|
1073
|
+
const line = newLines[i];
|
|
1074
|
+
if (dir > 0) {
|
|
1075
|
+
if (line.length > 0) newLines[i] = unit + line;
|
|
1076
|
+
} else {
|
|
1077
|
+
if (line.startsWith('\t')) newLines[i] = line.slice(1);
|
|
1078
|
+
else {
|
|
1079
|
+
let strip = 0;
|
|
1080
|
+
while (strip < size && line.charAt(strip) === ' ') strip++;
|
|
1081
|
+
newLines[i] = line.slice(strip);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
this._replaceAllPreservingSelection(newLines, a.line, endLine);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
toggleLineComment(): void {
|
|
1089
|
+
this._syncProps();
|
|
1090
|
+
if (this.props.readOnly) return;
|
|
1091
|
+
const marker = this.language?.data?.lineComment;
|
|
1092
|
+
if (!marker) return;
|
|
1093
|
+
const [a, b] = this._selectionRange();
|
|
1094
|
+
const endLine = b.line > a.line && b.ch === 0 ? b.line - 1 : b.line;
|
|
1095
|
+
const targets: number[] = [];
|
|
1096
|
+
for (let i = a.line; i <= endLine; i++) {
|
|
1097
|
+
if (this._lines[i].trim().length > 0) targets.push(i);
|
|
1098
|
+
}
|
|
1099
|
+
if (targets.length === 0) return;
|
|
1100
|
+
const allCommented = targets.every((i) =>
|
|
1101
|
+
this._lines[i].trimStart().startsWith(marker),
|
|
1102
|
+
);
|
|
1103
|
+
const newLines = this._lines.slice();
|
|
1104
|
+
for (const i of targets) {
|
|
1105
|
+
const line = newLines[i];
|
|
1106
|
+
const indent = line.length - line.trimStart().length;
|
|
1107
|
+
if (allCommented) {
|
|
1108
|
+
const rest = line.slice(indent + marker.length);
|
|
1109
|
+
newLines[i] =
|
|
1110
|
+
line.slice(0, indent) + (rest.startsWith(' ') ? rest.slice(1) : rest);
|
|
1111
|
+
} else {
|
|
1112
|
+
newLines[i] = line.slice(0, indent) + marker + ' ' + line.slice(indent);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
this._replaceAllPreservingSelection(newLines, a.line, endLine);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/** Line-rewriting commands share this: one undo step, selection kept on
|
|
1119
|
+
* the same lines (clamped to their new lengths). */
|
|
1120
|
+
private _replaceAllPreservingSelection(
|
|
1121
|
+
newLines: string[],
|
|
1122
|
+
fromLine: number,
|
|
1123
|
+
toLine: number,
|
|
1124
|
+
): void {
|
|
1125
|
+
const caret = this._caret;
|
|
1126
|
+
const anchor = this._anchor;
|
|
1127
|
+
this._applyLines(newLines, {
|
|
1128
|
+
fromLine,
|
|
1129
|
+
removed: toLine - fromLine + 1,
|
|
1130
|
+
inserted: toLine - fromLine + 1,
|
|
1131
|
+
});
|
|
1132
|
+
this._caret = clampPos(this._lines, caret);
|
|
1133
|
+
this._anchor = clampPos(this._lines, anchor);
|
|
1134
|
+
this._undoRun = null;
|
|
1135
|
+
const value = this._lines.join('\n');
|
|
1136
|
+
this._pushHistory({ value, caret: this._caret, anchor: this._anchor });
|
|
1137
|
+
this._afterEdit(value);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// --- input: keyboard -----------------------------------------------------
|
|
1141
|
+
|
|
1142
|
+
/**
|
|
1143
|
+
* The editor's own key behaviour, run by the EventManager after the app's
|
|
1144
|
+
* `onKeyDown` handlers and not at all if one of them called
|
|
1145
|
+
* `preventDefault()` — so `<CodeEditor>`'s completion popup vetoes this
|
|
1146
|
+
* simply by consuming the key first, and so does an application handler.
|
|
1147
|
+
*
|
|
1148
|
+
* Tab is an ordinary key here, ahead of focus traversal, which is what
|
|
1149
|
+
* lets a bare `<codeeditor>` indent with it. Owning Tab means owning the
|
|
1150
|
+
* keyboard user's way out, hence the convention `docs/extending.md` asks
|
|
1151
|
+
* elements to converge on: **Escape arms one pass-through Tab**. Escape on
|
|
1152
|
+
* its own does nothing visible, the next Tab leaves, and the one after
|
|
1153
|
+
* that indents again.
|
|
1154
|
+
*/
|
|
1155
|
+
override defaultKeyDown(ev: EditorKeyEvent): void {
|
|
1156
|
+
if (this.props.disabled) return;
|
|
1157
|
+
const k = ev.keysym;
|
|
1158
|
+
if (k === XK_ESCAPE) {
|
|
1159
|
+
this._tabEscapes = true;
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
if (k === XK_TAB && this._tabEscapes) {
|
|
1163
|
+
this._tabEscapes = false;
|
|
1164
|
+
return; // this one belongs to the focus cycle
|
|
1165
|
+
}
|
|
1166
|
+
this._tabEscapes = false;
|
|
1167
|
+
if (this.handleKeyDown(ev)) ev.preventDefault?.();
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/**
|
|
1171
|
+
* The editor's key behaviour as a predicate — true when the key was
|
|
1172
|
+
* consumed. Kept apart from {@link defaultKeyDown} because the editing
|
|
1173
|
+
* model is also driven directly in tests and by an app that wires the raw
|
|
1174
|
+
* element itself; the default action is this plus the Tab/Escape bargain.
|
|
1175
|
+
*/
|
|
1176
|
+
handleKeyDown(ev: EditorKeyEvent): boolean {
|
|
1177
|
+
this._syncProps();
|
|
1178
|
+
const previousNative = this._keyNative;
|
|
1179
|
+
this._keyNative = ev.nativeEvent ?? null;
|
|
1180
|
+
try {
|
|
1181
|
+
return this._editKey(ev);
|
|
1182
|
+
} finally {
|
|
1183
|
+
this._keyNative = previousNative;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
private _editKey(ev: EditorKeyEvent): boolean {
|
|
1188
|
+
const k = ev.keysym;
|
|
1189
|
+
const readOnly = Boolean(this.props.readOnly);
|
|
1190
|
+
const [a, b] = this._selectionRange();
|
|
1191
|
+
const hasSelection = this._hasSelection();
|
|
1192
|
+
const wordChars = this.language?.data?.wordChars ?? '';
|
|
1193
|
+
|
|
1194
|
+
if (k === XK_RETURN || k === XK_KP_ENTER) {
|
|
1195
|
+
if (ev.ctrlKey) {
|
|
1196
|
+
this._fire('onSubmit', this.value);
|
|
1197
|
+
return true;
|
|
1198
|
+
}
|
|
1199
|
+
if (readOnly) return false;
|
|
1200
|
+
const lineText = this._lines[a.line];
|
|
1201
|
+
const indentMatch = /^[\t ]*/.exec(lineText)?.[0] ?? '';
|
|
1202
|
+
const indent = indentMatch.slice(0, Math.min(indentMatch.length, a.ch));
|
|
1203
|
+
const before = lineText.slice(0, a.ch);
|
|
1204
|
+
const rule = this.language?.data?.indentAfter;
|
|
1205
|
+
const deeper = rule ? rule.test(before) : false;
|
|
1206
|
+
const after = this._lines[b.line].slice(b.ch);
|
|
1207
|
+
if (deeper && /^[)\]}]/.test(after)) {
|
|
1208
|
+
// the caret sits between a fresh pair — `{|}` — so Enter opens it
|
|
1209
|
+
// the way every editor does: an indented line for the caret, the
|
|
1210
|
+
// closer on its own line at the old depth
|
|
1211
|
+
const unit = this._indentUnit();
|
|
1212
|
+
this.replaceRange(a, b, '\n' + indent + unit + '\n' + indent);
|
|
1213
|
+
this.moveCaret(
|
|
1214
|
+
{ line: a.line + 1, ch: indent.length + unit.length },
|
|
1215
|
+
false,
|
|
1216
|
+
);
|
|
1217
|
+
return true;
|
|
1218
|
+
}
|
|
1219
|
+
this.insertText('\n' + indent + (deeper ? this._indentUnit() : ''));
|
|
1220
|
+
return true;
|
|
1221
|
+
}
|
|
1222
|
+
if (k === XK_BACKSPACE) {
|
|
1223
|
+
if (readOnly) return true;
|
|
1224
|
+
if (hasSelection) this.replaceRange(a, b, '');
|
|
1225
|
+
else if (ev.ctrlKey) {
|
|
1226
|
+
this.replaceRange(wordBoundary(this._lines, a, -1, wordChars), a, '');
|
|
1227
|
+
} else {
|
|
1228
|
+
const from = movePos(this._lines, a, -1);
|
|
1229
|
+
if (!posEqual(from, a)) this.replaceRange(from, a, '', 'delete-back');
|
|
1230
|
+
}
|
|
1231
|
+
return true;
|
|
1232
|
+
}
|
|
1233
|
+
if (k === XK_DELETE) {
|
|
1234
|
+
if (readOnly) return true;
|
|
1235
|
+
if (hasSelection) this.replaceRange(a, b, '');
|
|
1236
|
+
else if (ev.ctrlKey) {
|
|
1237
|
+
this.replaceRange(a, wordBoundary(this._lines, a, 1, wordChars), '');
|
|
1238
|
+
} else {
|
|
1239
|
+
const to = movePos(this._lines, a, 1);
|
|
1240
|
+
if (!posEqual(to, a)) this.replaceRange(a, to, '', 'delete-forward');
|
|
1241
|
+
}
|
|
1242
|
+
return true;
|
|
1243
|
+
}
|
|
1244
|
+
if (k === XK_LEFT || k === XK_RIGHT) {
|
|
1245
|
+
const dir = k === XK_LEFT ? -1 : 1;
|
|
1246
|
+
let target: Position;
|
|
1247
|
+
if (ev.ctrlKey) {
|
|
1248
|
+
target = wordBoundary(this._lines, this._caret, dir, wordChars);
|
|
1249
|
+
} else if (!ev.shiftKey && hasSelection) {
|
|
1250
|
+
target = dir < 0 ? a : b;
|
|
1251
|
+
} else {
|
|
1252
|
+
target = movePos(this._lines, this._caret, dir);
|
|
1253
|
+
}
|
|
1254
|
+
this.moveCaret(target, ev.shiftKey);
|
|
1255
|
+
if (ev.shiftKey) this._ownPrimary();
|
|
1256
|
+
return true;
|
|
1257
|
+
}
|
|
1258
|
+
if (k === XK_UP || k === XK_DOWN) {
|
|
1259
|
+
this._verticalMove(k === XK_UP ? -1 : 1, ev.shiftKey);
|
|
1260
|
+
return true;
|
|
1261
|
+
}
|
|
1262
|
+
if (k === XK_PAGE_UP || k === XK_PAGE_DOWN) {
|
|
1263
|
+
const content = this._contentRect();
|
|
1264
|
+
const page = Math.max(
|
|
1265
|
+
1,
|
|
1266
|
+
Math.floor(content.height / this._lineHeight()) - 1,
|
|
1267
|
+
);
|
|
1268
|
+
this._verticalMove((k === XK_PAGE_UP ? -1 : 1) * page, ev.shiftKey);
|
|
1269
|
+
return true;
|
|
1270
|
+
}
|
|
1271
|
+
if (k === XK_HOME) {
|
|
1272
|
+
if (ev.ctrlKey) {
|
|
1273
|
+
this.moveCaret({ line: 0, ch: 0 }, ev.shiftKey);
|
|
1274
|
+
return true;
|
|
1275
|
+
}
|
|
1276
|
+
const text = this._lines[this._caret.line];
|
|
1277
|
+
const first = text.length - text.trimStart().length;
|
|
1278
|
+
const ch = this._caret.ch === first ? 0 : first;
|
|
1279
|
+
this.moveCaret({ line: this._caret.line, ch }, ev.shiftKey);
|
|
1280
|
+
return true;
|
|
1281
|
+
}
|
|
1282
|
+
if (k === XK_END) {
|
|
1283
|
+
if (ev.ctrlKey) {
|
|
1284
|
+
const last = this._lines.length - 1;
|
|
1285
|
+
this.moveCaret(
|
|
1286
|
+
{ line: last, ch: this._lines[last].length },
|
|
1287
|
+
ev.shiftKey,
|
|
1288
|
+
);
|
|
1289
|
+
return true;
|
|
1290
|
+
}
|
|
1291
|
+
this.moveCaret(
|
|
1292
|
+
{ line: this._caret.line, ch: this._lines[this._caret.line].length },
|
|
1293
|
+
ev.shiftKey,
|
|
1294
|
+
);
|
|
1295
|
+
return true;
|
|
1296
|
+
}
|
|
1297
|
+
if (k === XK_TAB) {
|
|
1298
|
+
if (readOnly) return false;
|
|
1299
|
+
if (ev.shiftKey) {
|
|
1300
|
+
this.indentSelection(-1);
|
|
1301
|
+
return true;
|
|
1302
|
+
}
|
|
1303
|
+
if (a.line !== b.line) {
|
|
1304
|
+
this.indentSelection(1);
|
|
1305
|
+
return true;
|
|
1306
|
+
}
|
|
1307
|
+
if (this.props.insertSpaces === false) {
|
|
1308
|
+
this.insertText('\t', 'type');
|
|
1309
|
+
return true;
|
|
1310
|
+
}
|
|
1311
|
+
// spaces to the next tab stop, measured in display columns
|
|
1312
|
+
const map = tabMap(this._lines[a.line].slice(0, a.ch), this._tabSize());
|
|
1313
|
+
const col = map.display.length;
|
|
1314
|
+
const size = this._tabSize();
|
|
1315
|
+
this.insertText(' '.repeat(size - (col % size)), 'type');
|
|
1316
|
+
return true;
|
|
1317
|
+
}
|
|
1318
|
+
if (k === XK_ESCAPE) {
|
|
1319
|
+
return false; // the component owns Escape (completion, then Tab-out)
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
if (ev.ctrlKey) {
|
|
1323
|
+
const letter = ctrlChordLetter(ev);
|
|
1324
|
+
if (letter === 0x61 /* a */) {
|
|
1325
|
+
this.selectAll();
|
|
1326
|
+
return true;
|
|
1327
|
+
}
|
|
1328
|
+
if (letter === 0x63 /* c */) {
|
|
1329
|
+
this.copySelection();
|
|
1330
|
+
return true;
|
|
1331
|
+
}
|
|
1332
|
+
if (letter === 0x78 /* x */) {
|
|
1333
|
+
this.copySelection();
|
|
1334
|
+
if (!readOnly && hasSelection) this.replaceRange(a, b, '');
|
|
1335
|
+
return true;
|
|
1336
|
+
}
|
|
1337
|
+
if (letter === 0x76 /* v */) {
|
|
1338
|
+
if (!readOnly) this.pasteFrom();
|
|
1339
|
+
return true;
|
|
1340
|
+
}
|
|
1341
|
+
if (letter === 0x7a /* z */) {
|
|
1342
|
+
if (!readOnly) {
|
|
1343
|
+
if (ev.shiftKey) this.redo();
|
|
1344
|
+
else this.undo();
|
|
1345
|
+
}
|
|
1346
|
+
return true;
|
|
1347
|
+
}
|
|
1348
|
+
if (letter === 0x79 /* y */) {
|
|
1349
|
+
if (!readOnly) this.redo();
|
|
1350
|
+
return true;
|
|
1351
|
+
}
|
|
1352
|
+
if (letter === 0x2f /* / */) {
|
|
1353
|
+
this.toggleLineComment();
|
|
1354
|
+
return true;
|
|
1355
|
+
}
|
|
1356
|
+
return false;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
if (
|
|
1360
|
+
!readOnly &&
|
|
1361
|
+
ev.codepoint != null &&
|
|
1362
|
+
ev.codepoint >= 0x20 &&
|
|
1363
|
+
ev.codepoint !== 0x7f
|
|
1364
|
+
) {
|
|
1365
|
+
const ch = String.fromCodePoint(ev.codepoint);
|
|
1366
|
+
this.insertText(ch, 'type');
|
|
1367
|
+
if (/\s/.test(ch)) this.breakUndoRun();
|
|
1368
|
+
return true;
|
|
1369
|
+
}
|
|
1370
|
+
return false;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
private _verticalMove(deltaLines: number, extend: boolean): void {
|
|
1374
|
+
if (this._goalX == null) this._goalX = this._caretX(this._caret);
|
|
1375
|
+
const line = Math.max(
|
|
1376
|
+
0,
|
|
1377
|
+
Math.min(this._lines.length - 1, this._caret.line + deltaLines),
|
|
1378
|
+
);
|
|
1379
|
+
if (line === this._caret.line) {
|
|
1380
|
+
// pressing against the document's edge: land on its corner, the way
|
|
1381
|
+
// every editor does
|
|
1382
|
+
const ch = deltaLines < 0 ? 0 : this._lines[line].length;
|
|
1383
|
+
this.moveCaret({ line, ch }, extend);
|
|
1384
|
+
if (extend) this._ownPrimary();
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
const entry = this._lineEntry(line);
|
|
1388
|
+
let ch = 0;
|
|
1389
|
+
if (entry.layout) {
|
|
1390
|
+
const cp = entry.layout.indexAt(this._goalX, 0);
|
|
1391
|
+
ch = entry.map.toRaw(cpToUtf16(entry.display, cp));
|
|
1392
|
+
}
|
|
1393
|
+
const goal = this._goalX; // moveCaret clears it; this move keeps it
|
|
1394
|
+
this.moveCaret({ line, ch }, extend);
|
|
1395
|
+
this._goalX = goal;
|
|
1396
|
+
if (extend) this._ownPrimary();
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
// --- input: mouse --------------------------------------------------------
|
|
1400
|
+
|
|
1401
|
+
/**
|
|
1402
|
+
* A press: place the caret, start a drag-selection, or paste PRIMARY on
|
|
1403
|
+
* the middle button. The drag and release below are the continuation of
|
|
1404
|
+
* *this* press — core vetoes a gesture once, at its press, so an editor
|
|
1405
|
+
* whose `defaultMouseDown` never ran hears neither, and needs no
|
|
1406
|
+
* `dragging` flag to discover that.
|
|
1407
|
+
*/
|
|
1408
|
+
override defaultMouseDown(ev: EditorMouseEvent): void {
|
|
1409
|
+
if (this.props.disabled) return;
|
|
1410
|
+
this.focus();
|
|
1411
|
+
if (this.handleMouseDown(ev)) ev.preventDefault?.();
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
override defaultMouseDrag(ev: EditorMouseEvent): void {
|
|
1415
|
+
if (this.handleMouseMove(ev)) ev.preventDefault?.();
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
override defaultMouseUp(): void {
|
|
1419
|
+
this.handleMouseUp();
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
handleMouseDown(ev: EditorMouseEvent): boolean {
|
|
1423
|
+
this._syncProps();
|
|
1424
|
+
this.breakUndoRun();
|
|
1425
|
+
const pos = this._posAt(ev.x, ev.y);
|
|
1426
|
+
if (ev.button === 3) {
|
|
1427
|
+
const [a, b] = this._selectionRange();
|
|
1428
|
+
if (comparePos(pos, a) < 0 || comparePos(pos, b) > 0) {
|
|
1429
|
+
this.moveCaret(pos, false);
|
|
1430
|
+
}
|
|
1431
|
+
return true;
|
|
1432
|
+
}
|
|
1433
|
+
if (ev.button === 2) {
|
|
1434
|
+
this.moveCaret(pos, false);
|
|
1435
|
+
if (!this.props.readOnly) this.pasteFrom('PRIMARY');
|
|
1436
|
+
return true;
|
|
1437
|
+
}
|
|
1438
|
+
const detail = ev.detail ?? 1;
|
|
1439
|
+
if (detail >= 3) {
|
|
1440
|
+
this._drag = 'line';
|
|
1441
|
+
this._dragOrigin = pos;
|
|
1442
|
+
this._selectLineRange(pos, pos);
|
|
1443
|
+
} else if (detail === 2) {
|
|
1444
|
+
this._drag = 'word';
|
|
1445
|
+
this._dragOrigin = pos;
|
|
1446
|
+
this._selectWordRange(pos, pos);
|
|
1447
|
+
} else {
|
|
1448
|
+
this._drag = 'char';
|
|
1449
|
+
this._dragOrigin = pos;
|
|
1450
|
+
this.moveCaret(pos, ev.shiftKey);
|
|
1451
|
+
}
|
|
1452
|
+
ev.capturePointer?.();
|
|
1453
|
+
return true;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
handleMouseMove(ev: EditorMouseEvent): boolean {
|
|
1457
|
+
if (!this._drag) return false;
|
|
1458
|
+
const pos = this._posAt(ev.x, ev.y);
|
|
1459
|
+
if (this._drag === 'char') this.moveCaret(pos, true);
|
|
1460
|
+
else if (this._drag === 'word')
|
|
1461
|
+
this._selectWordRange(this._dragOrigin, pos);
|
|
1462
|
+
else this._selectLineRange(this._dragOrigin, pos);
|
|
1463
|
+
return true;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
handleMouseUp(): boolean {
|
|
1467
|
+
if (!this._drag) return false;
|
|
1468
|
+
this._drag = null;
|
|
1469
|
+
this._ownPrimary();
|
|
1470
|
+
return true;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
private _selectWordRange(origin: Position, to: Position): void {
|
|
1474
|
+
const extra = this.language?.data?.wordChars ?? '';
|
|
1475
|
+
const w1 = wordRangeAt(this._lines[origin.line], origin.ch, extra);
|
|
1476
|
+
const w2 = wordRangeAt(this._lines[to.line], to.ch, extra);
|
|
1477
|
+
const start = { line: origin.line, ch: w1.from };
|
|
1478
|
+
const end = { line: to.line, ch: w2.to };
|
|
1479
|
+
if (comparePos(to, origin) < 0) {
|
|
1480
|
+
this.select(
|
|
1481
|
+
{ line: origin.line, ch: w1.to },
|
|
1482
|
+
{ line: to.line, ch: w2.from },
|
|
1483
|
+
);
|
|
1484
|
+
} else {
|
|
1485
|
+
this.select(start, end);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
private _selectLineRange(origin: Position, to: Position): void {
|
|
1490
|
+
const [first, last] =
|
|
1491
|
+
to.line < origin.line ? [to.line, origin.line] : [origin.line, to.line];
|
|
1492
|
+
if (last + 1 < this._lines.length) {
|
|
1493
|
+
this.select({ line: first, ch: 0 }, { line: last + 1, ch: 0 });
|
|
1494
|
+
} else {
|
|
1495
|
+
this.select(
|
|
1496
|
+
{ line: first, ch: 0 },
|
|
1497
|
+
{ line: last, ch: this._lines[last].length },
|
|
1498
|
+
);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
// --- focus ---------------------------------------------------------------
|
|
1503
|
+
|
|
1504
|
+
// `focus()`/`blur()` come from `Node` — declared as well as implemented
|
|
1505
|
+
// since react-x11#267, so the forwarding shims this file used to carry are
|
|
1506
|
+
// gone. `CodeEditorHandle` keeps promising `void`: the base returns `this`,
|
|
1507
|
+
// and widening the handle to the node type would drag the class's nominal
|
|
1508
|
+
// type into the exported props (see the note on `CodeEditorHandle`).
|
|
1509
|
+
|
|
1510
|
+
/** Focus is a state, not an event: `onFocus` is where the app hears about
|
|
1511
|
+
* it, and this is where the caret starts blinking. The timer is also
|
|
1512
|
+
* released in `destroySubtree`, because a node that unmounts while focused
|
|
1513
|
+
* is forgotten rather than blurred and this would never run. */
|
|
1514
|
+
override defaultFocus(): void {
|
|
1515
|
+
this.handleFocus();
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
override defaultBlur(): void {
|
|
1519
|
+
this.handleBlur();
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
handleFocus(): void {
|
|
1523
|
+
this._focused = true;
|
|
1524
|
+
this._caretOn = true;
|
|
1525
|
+
if (this._blinkTimer == null) {
|
|
1526
|
+
this._blinkTimer = startInterval(() => {
|
|
1527
|
+
this._caretOn = !this._caretOn;
|
|
1528
|
+
this.root?.invalidate(false, this.abs, 'text');
|
|
1529
|
+
}, CARET_BLINK_MS);
|
|
1530
|
+
}
|
|
1531
|
+
this._repaint();
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
handleBlur(): void {
|
|
1535
|
+
this._focused = false;
|
|
1536
|
+
this.breakUndoRun();
|
|
1537
|
+
stopInterval(this._blinkTimer);
|
|
1538
|
+
this._blinkTimer = null;
|
|
1539
|
+
this._repaint();
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
// --- lifecycle -----------------------------------------------------------
|
|
1543
|
+
|
|
1544
|
+
override applyProps(
|
|
1545
|
+
nextProps: Record<string, unknown>,
|
|
1546
|
+
prevProps: Record<string, unknown>,
|
|
1547
|
+
): void {
|
|
1548
|
+
const before = prevProps ?? this.props;
|
|
1549
|
+
super.applyProps(nextProps, prevProps);
|
|
1550
|
+
if (nextProps.value != null) this._syncProps();
|
|
1551
|
+
if (nextProps.language !== before.language) {
|
|
1552
|
+
this._tokenizer(); // re-resolves against the new props
|
|
1553
|
+
this._repaint();
|
|
1554
|
+
}
|
|
1555
|
+
if (nextProps.rows !== before.rows) this.invalidateMeasure('props');
|
|
1556
|
+
if (
|
|
1557
|
+
nextProps.tokenStyles !== before.tokenStyles ||
|
|
1558
|
+
nextProps.diagnostics !== before.diagnostics ||
|
|
1559
|
+
nextProps.tabSize !== before.tabSize ||
|
|
1560
|
+
nextProps.lineNumbers !== before.lineNumbers
|
|
1561
|
+
) {
|
|
1562
|
+
this._lineCache.clear();
|
|
1563
|
+
this._repaint();
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
override destroySubtree(): void {
|
|
1568
|
+
stopInterval(this._blinkTimer);
|
|
1569
|
+
this._blinkTimer = null;
|
|
1570
|
+
this._tok?.dispose?.();
|
|
1571
|
+
this._tok = null;
|
|
1572
|
+
super.destroySubtree();
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// --- painting ------------------------------------------------------------
|
|
1576
|
+
|
|
1577
|
+
override paint(ctx: Context2D): void {
|
|
1578
|
+
super.paint(ctx);
|
|
1579
|
+
this._syncProps();
|
|
1580
|
+
this._metrics();
|
|
1581
|
+
const c = ctx as {
|
|
1582
|
+
fillStyle?: unknown;
|
|
1583
|
+
fillRect?(x: number, y: number, w: number, h: number): void;
|
|
1584
|
+
save?(): void;
|
|
1585
|
+
restore?(): void;
|
|
1586
|
+
beginPath?(): void;
|
|
1587
|
+
rect?(x: number, y: number, w: number, h: number): void;
|
|
1588
|
+
clip?(): void;
|
|
1589
|
+
moveTo?(x: number, y: number): void;
|
|
1590
|
+
lineTo?(x: number, y: number): void;
|
|
1591
|
+
stroke?(): void;
|
|
1592
|
+
strokeStyle?: unknown;
|
|
1593
|
+
lineWidth?: number;
|
|
1594
|
+
};
|
|
1595
|
+
if (typeof c.fillRect !== 'function') return; // mock backend: geometry only
|
|
1596
|
+
const content = this._contentRect();
|
|
1597
|
+
if (content.width <= 0 || content.height <= 0) return;
|
|
1598
|
+
const lineH = this._lineH;
|
|
1599
|
+
const gutterW = this._gutterWidth();
|
|
1600
|
+
const textX = content.x + gutterW - this._scrollX;
|
|
1601
|
+
const props = this.props as unknown as CodeEditorProps;
|
|
1602
|
+
const styles = this._textStyle();
|
|
1603
|
+
const baseColor = styles.color as string;
|
|
1604
|
+
|
|
1605
|
+
const canClip =
|
|
1606
|
+
typeof c.save === 'function' &&
|
|
1607
|
+
typeof c.beginPath === 'function' &&
|
|
1608
|
+
typeof c.rect === 'function' &&
|
|
1609
|
+
typeof c.clip === 'function';
|
|
1610
|
+
const clipTo = (x: number, y: number, w: number, h: number): boolean => {
|
|
1611
|
+
if (!canClip) return false;
|
|
1612
|
+
c.save!();
|
|
1613
|
+
c.beginPath!();
|
|
1614
|
+
c.rect!(x, y, w, h);
|
|
1615
|
+
c.clip!();
|
|
1616
|
+
return true;
|
|
1617
|
+
};
|
|
1618
|
+
const dark = this._onDark();
|
|
1619
|
+
|
|
1620
|
+
const first = Math.max(0, Math.floor(this._scrollY / lineH));
|
|
1621
|
+
const last = Math.min(
|
|
1622
|
+
this._lines.length - 1,
|
|
1623
|
+
Math.ceil((this._scrollY + content.height) / lineH),
|
|
1624
|
+
);
|
|
1625
|
+
const lineY = (i: number): number => content.y + i * lineH - this._scrollY;
|
|
1626
|
+
|
|
1627
|
+
// Everything the editor draws — gutter included — stays inside the
|
|
1628
|
+
// content box. The first and last visible rows are partial when
|
|
1629
|
+
// scrolled, and without this their overhang (up to a line each way)
|
|
1630
|
+
// walks into the padding and past it.
|
|
1631
|
+
const outerClip = clipTo(
|
|
1632
|
+
content.x,
|
|
1633
|
+
content.y,
|
|
1634
|
+
content.width,
|
|
1635
|
+
content.height,
|
|
1636
|
+
);
|
|
1637
|
+
|
|
1638
|
+
// gutter background
|
|
1639
|
+
if (gutterW > 0 && props.gutterBackground) {
|
|
1640
|
+
c.fillStyle = this._resolveColor(props.gutterBackground) ?? '#00000008';
|
|
1641
|
+
c.fillRect(content.x, content.y, gutterW, content.height);
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
// the text region clips one step narrower, so a horizontally scrolled
|
|
1645
|
+
// line cannot run underneath the line numbers
|
|
1646
|
+
const textClip = clipTo(
|
|
1647
|
+
content.x + gutterW,
|
|
1648
|
+
content.y,
|
|
1649
|
+
content.width - gutterW,
|
|
1650
|
+
content.height,
|
|
1651
|
+
);
|
|
1652
|
+
|
|
1653
|
+
// active line
|
|
1654
|
+
if (
|
|
1655
|
+
props.activeLine &&
|
|
1656
|
+
this._focused &&
|
|
1657
|
+
!this._hasSelection() &&
|
|
1658
|
+
this._caret.line >= first &&
|
|
1659
|
+
this._caret.line <= last
|
|
1660
|
+
) {
|
|
1661
|
+
c.fillStyle =
|
|
1662
|
+
this._resolveColor(props.activeLineColor) ??
|
|
1663
|
+
(dark ? 'rgba(255,255,255,0.06)' : 'rgba(120,140,180,0.10)');
|
|
1664
|
+
c.fillRect(
|
|
1665
|
+
content.x + gutterW,
|
|
1666
|
+
lineY(this._caret.line),
|
|
1667
|
+
content.width - gutterW,
|
|
1668
|
+
lineH,
|
|
1669
|
+
);
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
// selection bands
|
|
1673
|
+
if (this._hasSelection()) {
|
|
1674
|
+
const [a, b] = this._selectionRange();
|
|
1675
|
+
c.fillStyle =
|
|
1676
|
+
this._resolveColor(props.selectionColor) ??
|
|
1677
|
+
(dark ? 'rgba(82,139,255,0.35)' : 'rgba(80,140,255,0.28)');
|
|
1678
|
+
for (let i = Math.max(a.line, first); i <= Math.min(b.line, last); i++) {
|
|
1679
|
+
const entry = this._lineEntry(i);
|
|
1680
|
+
const startX =
|
|
1681
|
+
i === a.line && entry.layout
|
|
1682
|
+
? this._caretX({ line: i, ch: a.ch })
|
|
1683
|
+
: 0;
|
|
1684
|
+
const endX =
|
|
1685
|
+
i === b.line
|
|
1686
|
+
? this._caretX({ line: i, ch: b.ch })
|
|
1687
|
+
: (entry.layout?.width ?? 0) + this._charW * 0.5; // the newline
|
|
1688
|
+
c.fillRect(textX + startX, lineY(i), Math.max(2, endX - startX), lineH);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
// matching brackets
|
|
1693
|
+
if (props.matchBrackets !== false && this._focused) {
|
|
1694
|
+
const match = this._findBracketMatch();
|
|
1695
|
+
if (match) {
|
|
1696
|
+
c.fillStyle =
|
|
1697
|
+
this._resolveColor(props.matchingBracketColor) ??
|
|
1698
|
+
(dark ? 'rgba(90,190,130,0.35)' : 'rgba(80,180,120,0.30)');
|
|
1699
|
+
for (const pos of match) {
|
|
1700
|
+
if (pos.line < first || pos.line > last) continue;
|
|
1701
|
+
const x0 = this._caretX(pos);
|
|
1702
|
+
const x1 = this._caretX({ line: pos.line, ch: pos.ch + 1 });
|
|
1703
|
+
c.fillRect(textX + x0, lineY(pos.line), Math.max(1, x1 - x0), lineH);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
// text
|
|
1709
|
+
const empty =
|
|
1710
|
+
this._lines.length === 1 &&
|
|
1711
|
+
this._lines[0].length === 0 &&
|
|
1712
|
+
props.placeholder;
|
|
1713
|
+
if (empty) {
|
|
1714
|
+
const fonts = this._fonts();
|
|
1715
|
+
if (fonts) {
|
|
1716
|
+
const layout = fonts.layout(
|
|
1717
|
+
[
|
|
1718
|
+
{
|
|
1719
|
+
text: String(props.placeholder),
|
|
1720
|
+
color:
|
|
1721
|
+
this._resolveColor(props.placeholderColor) ??
|
|
1722
|
+
this._themeColor('dim') ??
|
|
1723
|
+
'#9aa0a6',
|
|
1724
|
+
},
|
|
1725
|
+
],
|
|
1726
|
+
styles,
|
|
1727
|
+
);
|
|
1728
|
+
layout.draw(ctx, textX, lineY(0) + (lineH - layout.height) / 2);
|
|
1729
|
+
}
|
|
1730
|
+
} else {
|
|
1731
|
+
for (let i = first; i <= last; i++) {
|
|
1732
|
+
const entry = this._lineEntry(i);
|
|
1733
|
+
entry.layout?.draw(
|
|
1734
|
+
ctx,
|
|
1735
|
+
textX,
|
|
1736
|
+
lineY(i) + (lineH - (entry.layout.height || lineH)) / 2,
|
|
1737
|
+
);
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
// diagnostics
|
|
1742
|
+
const diagnostics = props.diagnostics ?? [];
|
|
1743
|
+
for (const d of diagnostics) {
|
|
1744
|
+
const color = SEVERITY_COLORS[d.severity ?? 'error'] ?? '#e5484d';
|
|
1745
|
+
const from = clampPos(this._lines, d.from);
|
|
1746
|
+
const to = clampPos(this._lines, maxPos(d.from, d.to));
|
|
1747
|
+
for (
|
|
1748
|
+
let i = Math.max(from.line, first);
|
|
1749
|
+
i <= Math.min(to.line, last);
|
|
1750
|
+
i++
|
|
1751
|
+
) {
|
|
1752
|
+
const x0 = i === from.line ? this._caretX({ line: i, ch: from.ch }) : 0;
|
|
1753
|
+
const lineText = this._lines[i];
|
|
1754
|
+
const x1 =
|
|
1755
|
+
i === to.line
|
|
1756
|
+
? this._caretX({ line: i, ch: to.ch })
|
|
1757
|
+
: this._caretX({ line: i, ch: lineText.length });
|
|
1758
|
+
// hug the glyphs, not the line box: the text is centred in its
|
|
1759
|
+
// band, so a squiggle at the band's bottom floats visibly below
|
|
1760
|
+
// short text — measure where the drawn layout actually ends
|
|
1761
|
+
const entry = this._lineEntry(i);
|
|
1762
|
+
const glyphBottom =
|
|
1763
|
+
lineY(i) +
|
|
1764
|
+
(lineH - (entry.layout?.height || lineH)) / 2 +
|
|
1765
|
+
(entry.layout?.height || lineH);
|
|
1766
|
+
const y = Math.min(lineY(i) + lineH - 2, glyphBottom - 1);
|
|
1767
|
+
const width = Math.max(this._charW * 0.6, x1 - x0);
|
|
1768
|
+
if (
|
|
1769
|
+
typeof c.beginPath === 'function' &&
|
|
1770
|
+
typeof c.moveTo === 'function' &&
|
|
1771
|
+
typeof c.lineTo === 'function' &&
|
|
1772
|
+
typeof c.stroke === 'function'
|
|
1773
|
+
) {
|
|
1774
|
+
c.beginPath();
|
|
1775
|
+
const step = 4;
|
|
1776
|
+
let up = true;
|
|
1777
|
+
for (let x = 0; x <= width; x += step) {
|
|
1778
|
+
const px = textX + x0 + x;
|
|
1779
|
+
const py = y + (up ? 0 : 2);
|
|
1780
|
+
if (x === 0) c.moveTo(px, py);
|
|
1781
|
+
else c.lineTo(px, py);
|
|
1782
|
+
up = !up;
|
|
1783
|
+
}
|
|
1784
|
+
c.strokeStyle = color;
|
|
1785
|
+
c.lineWidth = 1;
|
|
1786
|
+
c.stroke();
|
|
1787
|
+
} else {
|
|
1788
|
+
c.fillStyle = color;
|
|
1789
|
+
c.fillRect(textX + x0, y + 1, width, 1);
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
// caret — 2px, in the (theme-resolved) text colour
|
|
1795
|
+
if (this._focused && this._caretOn) {
|
|
1796
|
+
const x = this._caretX(this._caret);
|
|
1797
|
+
c.fillStyle = this._resolveColor(props.caretColor) ?? baseColor;
|
|
1798
|
+
c.fillRect(
|
|
1799
|
+
Math.round(textX + x),
|
|
1800
|
+
lineY(this._caret.line) + 1,
|
|
1801
|
+
2,
|
|
1802
|
+
lineH - 2,
|
|
1803
|
+
);
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
if (textClip) c.restore!();
|
|
1807
|
+
|
|
1808
|
+
// gutter numbers: inside the content clip, outside the text clip
|
|
1809
|
+
if (gutterW > 0) {
|
|
1810
|
+
const fonts = this._fonts();
|
|
1811
|
+
if (fonts) {
|
|
1812
|
+
const dim =
|
|
1813
|
+
this._resolveColor(props.gutterColor) ??
|
|
1814
|
+
this._themeColor('dim') ??
|
|
1815
|
+
'#9aa0a6';
|
|
1816
|
+
for (let i = first; i <= last; i++) {
|
|
1817
|
+
const active = i === this._caret.line;
|
|
1818
|
+
const layout = fonts.layout(
|
|
1819
|
+
[
|
|
1820
|
+
{
|
|
1821
|
+
text: String(i + 1),
|
|
1822
|
+
color: active ? baseColor : dim,
|
|
1823
|
+
},
|
|
1824
|
+
],
|
|
1825
|
+
styles,
|
|
1826
|
+
);
|
|
1827
|
+
layout.draw(
|
|
1828
|
+
ctx,
|
|
1829
|
+
content.x + gutterW - GUTTER_PAD - layout.width,
|
|
1830
|
+
lineY(i) + (lineH - layout.height) / 2,
|
|
1831
|
+
);
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
// scroll thumbs — minimal, panel-style
|
|
1837
|
+
const maxY = this._maxScrollY();
|
|
1838
|
+
if (maxY > 0) {
|
|
1839
|
+
const trackH = content.height;
|
|
1840
|
+
const thumbH = Math.max(
|
|
1841
|
+
24,
|
|
1842
|
+
(trackH * trackH) / (this._lines.length * lineH),
|
|
1843
|
+
);
|
|
1844
|
+
const yOff = (this._scrollY / maxY) * (trackH - thumbH);
|
|
1845
|
+
c.fillStyle = 'rgba(128,128,128,0.35)';
|
|
1846
|
+
c.fillRect(content.x + content.width - 4, content.y + yOff, 3, thumbH);
|
|
1847
|
+
}
|
|
1848
|
+
const maxX = this._maxScrollX();
|
|
1849
|
+
if (maxX > 0) {
|
|
1850
|
+
const textW = content.width - gutterW;
|
|
1851
|
+
const trackW = textW;
|
|
1852
|
+
const thumbW = Math.max(
|
|
1853
|
+
24,
|
|
1854
|
+
(trackW * trackW) / (this._widest + CARET_MARGIN),
|
|
1855
|
+
);
|
|
1856
|
+
const xOff = (this._scrollX / maxX) * (trackW - thumbW);
|
|
1857
|
+
c.fillStyle = 'rgba(128,128,128,0.35)';
|
|
1858
|
+
c.fillRect(
|
|
1859
|
+
content.x + gutterW + xOff,
|
|
1860
|
+
content.y + content.height - 4,
|
|
1861
|
+
thumbW,
|
|
1862
|
+
3,
|
|
1863
|
+
);
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
if (outerClip) c.restore!();
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
/** The bracket pair to highlight: the caret sits beside one of them. */
|
|
1870
|
+
private _findBracketMatch(): [Position, Position] | null {
|
|
1871
|
+
const pos = this._caret;
|
|
1872
|
+
const line = this._lines[pos.line];
|
|
1873
|
+
const before = pos.ch > 0 ? line.charAt(pos.ch - 1) : '';
|
|
1874
|
+
const after = pos.ch < line.length ? line.charAt(pos.ch) : '';
|
|
1875
|
+
let at: Position;
|
|
1876
|
+
let ch: string;
|
|
1877
|
+
if (after in BRACKETS) {
|
|
1878
|
+
at = { line: pos.line, ch: pos.ch };
|
|
1879
|
+
ch = after;
|
|
1880
|
+
} else if (before in BRACKETS) {
|
|
1881
|
+
at = { line: pos.line, ch: pos.ch - 1 };
|
|
1882
|
+
ch = before;
|
|
1883
|
+
} else {
|
|
1884
|
+
return null;
|
|
1885
|
+
}
|
|
1886
|
+
if (this._isOpaqueAt(at)) return null;
|
|
1887
|
+
const { match, dir } = BRACKETS[ch];
|
|
1888
|
+
let depth = 0;
|
|
1889
|
+
let scanned = 0;
|
|
1890
|
+
let l = at.line;
|
|
1891
|
+
let i = at.ch;
|
|
1892
|
+
while (l >= 0 && l < this._lines.length && scanned < MATCH_SCAN_LIMIT) {
|
|
1893
|
+
const text = this._lines[l];
|
|
1894
|
+
while (dir > 0 ? i < text.length : i >= 0) {
|
|
1895
|
+
const cur = text.charAt(i);
|
|
1896
|
+
if (
|
|
1897
|
+
(cur === ch || cur === match) &&
|
|
1898
|
+
!this._isOpaqueAt({ line: l, ch: i })
|
|
1899
|
+
) {
|
|
1900
|
+
if (cur === ch) depth++;
|
|
1901
|
+
else {
|
|
1902
|
+
depth--;
|
|
1903
|
+
if (depth === 0) return [at, { line: l, ch: i }];
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
i += dir;
|
|
1907
|
+
scanned++;
|
|
1908
|
+
}
|
|
1909
|
+
l += dir;
|
|
1910
|
+
if (l >= 0 && l < this._lines.length) {
|
|
1911
|
+
i = dir > 0 ? 0 : this._lines[l].length - 1;
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
return null;
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
private _isOpaqueAt(pos: Position): boolean {
|
|
1918
|
+
const tokens = this._tokenizer()?.lineTokens(pos.line);
|
|
1919
|
+
if (!tokens) return false;
|
|
1920
|
+
for (const t of tokens) {
|
|
1921
|
+
if (t.from <= pos.ch && pos.ch < t.to) return OPAQUE_TOKENS.has(t.type);
|
|
1922
|
+
if (t.from > pos.ch) break;
|
|
1923
|
+
}
|
|
1924
|
+
return false;
|
|
1925
|
+
}
|
|
1926
|
+
}
|