From 75fa2004e5497befb31c3c237d2a1b2ac59bbc43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E4=B8=B0?= <104338839+anjiazhuyouxing@users.noreply.github.com> Date: Tue, 3 Dec 2024 19:14:25 +0800 Subject: [PATCH] feat: add new component Json viewer (#2561) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --------- Co-authored-by: 田丰 Co-authored-by: 代强 Co-authored-by: zhangyumei.0319 Co-authored-by: point.halo --- .eslintrc.js | 2 + content/feedback/banner/index-en-US.md | 2 +- content/feedback/banner/index.md | 2 +- content/feedback/notification/index-en-US.md | 2 +- content/feedback/notification/index.md | 2 +- content/feedback/popconfirm/index-en-US.md | 2 +- content/feedback/popconfirm/index.md | 2 +- content/feedback/progress/index-en-US.md | 2 +- content/feedback/progress/index.md | 2 +- content/feedback/skeleton/index-en-US.md | 2 +- content/feedback/skeleton/index.md | 2 +- content/feedback/spin/index-en-US.md | 2 +- content/feedback/spin/index.md | 2 +- content/feedback/toast/index-en-US.md | 2 +- content/feedback/toast/index.md | 2 +- content/input/autocomplete/index-en-US.md | 2 +- content/input/autocomplete/index.md | 2 +- content/input/cascader/index-en-US.md | 2 +- content/input/cascader/index.md | 2 +- content/input/checkbox/index-en-US.md | 2 +- content/input/checkbox/index.md | 2 +- content/input/colorpicker/index-en-US.md | 2 +- content/input/colorpicker/index.md | 2 +- content/input/datepicker/index-en-US.md | 2 +- content/input/datepicker/index.md | 2 +- content/input/form/index-en-US.md | 2 +- content/input/form/index.md | 2 +- content/input/input/index-en-US.md | 2 +- content/input/input/index.md | 2 +- content/input/inputnumber/index-en-US.md | 2 +- content/input/inputnumber/index.md | 2 +- content/input/pincode/index-en-US.md | 2 +- content/input/pincode/index.md | 2 +- content/input/radio/index-en-US.md | 2 +- content/input/radio/index.md | 2 +- content/input/rating/index-en-US.md | 2 +- content/input/rating/index.md | 2 +- content/input/select/index-en-US.md | 2 +- content/input/select/index.md | 2 +- content/input/slider/index-en-US.md | 2 +- content/input/slider/index.md | 2 +- content/input/switch/index-en-US.md | 2 +- content/input/switch/index.md | 2 +- content/input/taginput/index-en-US.md | 2 +- content/input/taginput/index.md | 2 +- content/input/timepicker/index-en-US.md | 2 +- content/input/timepicker/index.md | 2 +- content/input/transfer/index-en-US.md | 2 +- content/input/transfer/index.md | 2 +- content/input/treeselect/index-en-US.md | 2 +- content/input/treeselect/index.md | 2 +- content/input/upload/index-en-US.md | 2 +- content/input/upload/index.md | 2 +- content/navigation/anchor/index-en-US.md | 2 +- content/navigation/anchor/index.md | 2 +- content/navigation/backtop/index-en-US.md | 2 +- content/navigation/backtop/index.md | 2 +- content/navigation/breadcrumb/index-en-US.md | 2 +- content/navigation/breadcrumb/index.md | 2 +- content/navigation/navigation/index-en-US.md | 2 +- content/navigation/navigation/index.md | 2 +- content/navigation/pagination/index-en-US.md | 2 +- content/navigation/pagination/index.md | 2 +- content/navigation/steps/index-en-US.md | 2 +- content/navigation/steps/index.md | 2 +- content/navigation/tabs/index-en-US.md | 2 +- content/navigation/tabs/index.md | 2 +- content/navigation/tree/index-en-US.md | 2 +- content/navigation/tree/index.md | 2 +- content/order.js | 4 +- content/other/configprovider/index-en-US.md | 2 +- content/other/configprovider/index.md | 2 +- content/other/locale/index-en-US.md | 2 +- content/other/locale/index.md | 2 +- content/plus/hotkeys/index-en-US.md | 2 +- content/plus/hotkeys/index.md | 2 +- content/plus/jsonviewer/index-en-US.md | 210 ++ content/plus/jsonviewer/index.md | 206 ++ content/plus/lottie/index-en-US.md | 2 +- content/plus/lottie/index.md | 2 +- content/show/avatar/index-en-US.md | 2 +- content/show/avatar/index.md | 2 +- content/show/badge/index-en-US.md | 2 +- content/show/badge/index.md | 2 +- content/show/calendar/index-en-US.md | 2 +- content/show/calendar/index.md | 2 +- content/show/card/index-en-US.md | 2 +- content/show/card/index.md | 2 +- content/show/carousel/index-en-US.md | 2 +- content/show/carousel/index.md | 2 +- content/show/chart/index-en-US.md | 2 +- content/show/chart/index.md | 2 +- content/show/collapse/index-en-US.md | 2 +- content/show/collapse/index.md | 2 +- content/show/collapsible/index-en-US.md | 2 +- content/show/collapsible/index.md | 2 +- content/show/descriptions/index-en-US.md | 2 +- content/show/descriptions/index.md | 2 +- content/show/dropdown/index-en-US.md | 2 +- content/show/dropdown/index.md | 2 +- content/show/empty/index-en-US.md | 2 +- content/show/empty/index.md | 2 +- content/show/highlight/index-en-US.md | 2 +- content/show/highlight/index.md | 2 +- content/show/image/index-en-US.md | 2 +- content/show/image/index.md | 2 +- content/show/list/index-en-US.md | 2 +- content/show/list/index.md | 2 +- content/show/modal/index-en-US.md | 2 +- content/show/modal/index.md | 2 +- content/show/overflowlist/index-en-US.md | 2 +- content/show/overflowlist/index.md | 2 +- content/show/popover/index-en-US.md | 2 +- content/show/popover/index.md | 2 +- content/show/scrolllist/index-en-US.md | 2 +- content/show/scrolllist/index.md | 2 +- content/show/sidesheet/index-en-US.md | 2 +- content/show/sidesheet/index.md | 2 +- content/show/table/index-en-US.md | 2 +- content/show/table/index.md | 2 +- content/show/tag/index-en-US.md | 2 +- content/show/tag/index.md | 2 +- content/show/timeline/index-en-US.md | 2 +- content/show/timeline/index.md | 2 +- content/show/tooltip/index-en-US.md | 2 +- content/show/tooltip/index.md | 2 +- content/start/changelog/index-en-US.md | 1 + content/start/changelog/index.md | 1 + gatsby-node.js | 6 +- jest.config.js | 1 + package.json | 12 +- .../semi-foundation/jsonViewer/constants.ts | 7 + .../semi-foundation/jsonViewer/foundation.ts | 72 + .../jsonViewer/jsonViewer.scss | 200 ++ .../jsonViewer/script/build.js | 51 + .../semi-foundation/jsonViewer/variables.scss | 15 + packages/semi-foundation/package.json | 2 + packages/semi-foundation/tsconfig.json | 2 +- packages/semi-json-viewer-core/package.json | 55 + .../script/compileLib.js | 50 + .../semi-json-viewer-core/src/common/async.ts | 15 + .../src/common/charCode.ts | 443 ++++ .../src/common/characterClassifier.ts | 81 + .../semi-json-viewer-core/src/common/dom.ts | 34 + .../src/common/emitter.ts | 62 + .../src/common/emitterEvents.ts | 51 + .../semi-json-viewer-core/src/common/map.ts | 480 ++++ .../semi-json-viewer-core/src/common/model.ts | 64 + .../src/common/nameSpace.ts | 9 + .../src/common/position.ts | 35 + .../semi-json-viewer-core/src/common/range.ts | 146 ++ .../src/common/stopWatch.ts | 41 + .../src/common/strings.ts | 140 ++ .../semi-json-viewer-core/src/common/uint.ts | 56 + .../semi-json-viewer-core/src/common/utils.ts | 7 + .../src/common/wordCharacterClassifier.ts | 113 + .../src/common/worker.ts | 6 + packages/semi-json-viewer-core/src/index.ts | 1 + .../src/json-viewer/jsonViewer.ts | 68 + .../src/model/command.ts | 113 + .../src/model/foldingModel.ts | 138 ++ .../semi-json-viewer-core/src/model/index.ts | 5 + .../src/model/jsonModel.ts | 326 +++ .../src/model/selectionModel.ts | 151 ++ .../src/model/textModelSearch.ts | 529 +++++ .../src/pieceTreeTextBuffer/index.ts | 2 + .../src/pieceTreeTextBuffer/pieceTreeBase.ts | 2028 +++++++++++++++++ .../pieceTreeTextBufferBuilder.ts | 166 ++ .../src/pieceTreeTextBuffer/rbTreeBase.ts | 421 ++++ .../src/service/completion.ts | 526 +++++ .../src/service/contribution.ts | 11 + .../src/service/getRange.ts | 55 + .../src/service/jsonService.ts | 32 + .../src/service/jsonTypes.ts | 236 ++ .../src/service/parse.ts | 518 +++++ .../semi-json-viewer-core/src/tokens/index.md | 43 + .../src/tokens/jsonModelToken.ts | 306 +++ .../src/tokens/offsetRange.ts | 238 ++ .../src/tokens/tokenizationJsonModelPart.ts | 114 + .../src/tokens/tokenize.ts | 282 +++ .../src/view/complete/completeWidget.ts | 206 ++ .../src/view/edit/editWidget.ts | 413 ++++ .../src/view/edit/getEnterAction.ts | 89 + .../src/view/fold/foldWidget.ts | 99 + .../src/view/hover/hoverWidget.ts | 121 + .../src/view/search/searchWidget.ts | 153 ++ .../semi-json-viewer-core/src/view/view.ts | 493 ++++ .../virtualized/CellSizeAndPositionManager.ts | 213 ++ .../ScalingCellSizeAndPositionManager.ts | 189 ++ .../src/view/virtualized/types.ts | 10 + .../src/worker/json.worker.ts | 40 + .../src/worker/jsonWorker.ts | 42 + .../src/worker/jsonWorkerManager.ts | 100 + packages/semi-ui/index.ts | 1 + .../jsonViewer/_story/jsonViewer.stories.jsx | 95 + .../jsonViewer/_story/jsonViewer.stories.tsx | 59 + packages/semi-ui/jsonViewer/_story/utils.ts | 61 + packages/semi-ui/jsonViewer/index.tsx | 302 +++ packages/semi-ui/package.json | 1 + src/images/docIcons/doc-jsonviewer.svg | 8 + yarn.lock | 252 +- 201 files changed, 11960 insertions(+), 220 deletions(-) create mode 100644 content/plus/jsonviewer/index-en-US.md create mode 100644 content/plus/jsonviewer/index.md create mode 100644 packages/semi-foundation/jsonViewer/constants.ts create mode 100644 packages/semi-foundation/jsonViewer/foundation.ts create mode 100644 packages/semi-foundation/jsonViewer/jsonViewer.scss create mode 100644 packages/semi-foundation/jsonViewer/script/build.js create mode 100644 packages/semi-foundation/jsonViewer/variables.scss create mode 100644 packages/semi-json-viewer-core/package.json create mode 100644 packages/semi-json-viewer-core/script/compileLib.js create mode 100644 packages/semi-json-viewer-core/src/common/async.ts create mode 100644 packages/semi-json-viewer-core/src/common/charCode.ts create mode 100644 packages/semi-json-viewer-core/src/common/characterClassifier.ts create mode 100644 packages/semi-json-viewer-core/src/common/dom.ts create mode 100644 packages/semi-json-viewer-core/src/common/emitter.ts create mode 100644 packages/semi-json-viewer-core/src/common/emitterEvents.ts create mode 100644 packages/semi-json-viewer-core/src/common/map.ts create mode 100644 packages/semi-json-viewer-core/src/common/model.ts create mode 100644 packages/semi-json-viewer-core/src/common/nameSpace.ts create mode 100644 packages/semi-json-viewer-core/src/common/position.ts create mode 100644 packages/semi-json-viewer-core/src/common/range.ts create mode 100644 packages/semi-json-viewer-core/src/common/stopWatch.ts create mode 100644 packages/semi-json-viewer-core/src/common/strings.ts create mode 100644 packages/semi-json-viewer-core/src/common/uint.ts create mode 100644 packages/semi-json-viewer-core/src/common/utils.ts create mode 100644 packages/semi-json-viewer-core/src/common/wordCharacterClassifier.ts create mode 100644 packages/semi-json-viewer-core/src/common/worker.ts create mode 100644 packages/semi-json-viewer-core/src/index.ts create mode 100644 packages/semi-json-viewer-core/src/json-viewer/jsonViewer.ts create mode 100644 packages/semi-json-viewer-core/src/model/command.ts create mode 100644 packages/semi-json-viewer-core/src/model/foldingModel.ts create mode 100644 packages/semi-json-viewer-core/src/model/index.ts create mode 100644 packages/semi-json-viewer-core/src/model/jsonModel.ts create mode 100644 packages/semi-json-viewer-core/src/model/selectionModel.ts create mode 100644 packages/semi-json-viewer-core/src/model/textModelSearch.ts create mode 100644 packages/semi-json-viewer-core/src/pieceTreeTextBuffer/index.ts create mode 100644 packages/semi-json-viewer-core/src/pieceTreeTextBuffer/pieceTreeBase.ts create mode 100644 packages/semi-json-viewer-core/src/pieceTreeTextBuffer/pieceTreeTextBufferBuilder.ts create mode 100644 packages/semi-json-viewer-core/src/pieceTreeTextBuffer/rbTreeBase.ts create mode 100644 packages/semi-json-viewer-core/src/service/completion.ts create mode 100644 packages/semi-json-viewer-core/src/service/contribution.ts create mode 100644 packages/semi-json-viewer-core/src/service/getRange.ts create mode 100644 packages/semi-json-viewer-core/src/service/jsonService.ts create mode 100644 packages/semi-json-viewer-core/src/service/jsonTypes.ts create mode 100644 packages/semi-json-viewer-core/src/service/parse.ts create mode 100644 packages/semi-json-viewer-core/src/tokens/index.md create mode 100644 packages/semi-json-viewer-core/src/tokens/jsonModelToken.ts create mode 100644 packages/semi-json-viewer-core/src/tokens/offsetRange.ts create mode 100644 packages/semi-json-viewer-core/src/tokens/tokenizationJsonModelPart.ts create mode 100644 packages/semi-json-viewer-core/src/tokens/tokenize.ts create mode 100644 packages/semi-json-viewer-core/src/view/complete/completeWidget.ts create mode 100644 packages/semi-json-viewer-core/src/view/edit/editWidget.ts create mode 100644 packages/semi-json-viewer-core/src/view/edit/getEnterAction.ts create mode 100644 packages/semi-json-viewer-core/src/view/fold/foldWidget.ts create mode 100644 packages/semi-json-viewer-core/src/view/hover/hoverWidget.ts create mode 100644 packages/semi-json-viewer-core/src/view/search/searchWidget.ts create mode 100644 packages/semi-json-viewer-core/src/view/view.ts create mode 100644 packages/semi-json-viewer-core/src/view/virtualized/CellSizeAndPositionManager.ts create mode 100644 packages/semi-json-viewer-core/src/view/virtualized/ScalingCellSizeAndPositionManager.ts create mode 100644 packages/semi-json-viewer-core/src/view/virtualized/types.ts create mode 100644 packages/semi-json-viewer-core/src/worker/json.worker.ts create mode 100644 packages/semi-json-viewer-core/src/worker/jsonWorker.ts create mode 100644 packages/semi-json-viewer-core/src/worker/jsonWorkerManager.ts create mode 100644 packages/semi-ui/jsonViewer/_story/jsonViewer.stories.jsx create mode 100644 packages/semi-ui/jsonViewer/_story/jsonViewer.stories.tsx create mode 100644 packages/semi-ui/jsonViewer/_story/utils.ts create mode 100644 packages/semi-ui/jsonViewer/index.tsx create mode 100644 src/images/docIcons/doc-jsonviewer.svg diff --git a/.eslintrc.js b/.eslintrc.js index d8dee5ec17..1e07a7c02b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -121,6 +121,8 @@ module.exports = { "space-infix-ops": ["error", { "int32Hint": false }], 'space-before-blocks': ['error', 'always'], "space-infix-ops": "error", + '@typescript-eslint/prefer-as-const': 'off', + '@typescript-eslint/no-namespace': 'off', "@typescript-eslint/type-annotation-spacing": ['error', {"after": true}], "@typescript-eslint/member-delimiter-style": [ "error", diff --git a/content/feedback/banner/index-en-US.md b/content/feedback/banner/index-en-US.md index 563e5c15c2..854498a3d0 100644 --- a/content/feedback/banner/index-en-US.md +++ b/content/feedback/banner/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 79 +order: 80 category: Feedback title: Banner subTitle: Banner diff --git a/content/feedback/banner/index.md b/content/feedback/banner/index.md index 8fbc037054..64cb1bbf03 100644 --- a/content/feedback/banner/index.md +++ b/content/feedback/banner/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 79 +order: 80 category: 反馈类 title: Banner 通知横幅 icon: doc-banner diff --git a/content/feedback/notification/index-en-US.md b/content/feedback/notification/index-en-US.md index 0efad3cece..5c95b20dec 100644 --- a/content/feedback/notification/index-en-US.md +++ b/content/feedback/notification/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 80 +order: 81 category: Feedback title: Notification subTitle: Notification diff --git a/content/feedback/notification/index.md b/content/feedback/notification/index.md index 206b734972..308a15b52e 100644 --- a/content/feedback/notification/index.md +++ b/content/feedback/notification/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 80 +order: 81 category: 反馈类 title: Notification 通知 icon: doc-notification diff --git a/content/feedback/popconfirm/index-en-US.md b/content/feedback/popconfirm/index-en-US.md index 7f2f92ffd4..1c48572e77 100644 --- a/content/feedback/popconfirm/index-en-US.md +++ b/content/feedback/popconfirm/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 81 +order: 82 category: Feedback title: Popconfirm subTitle: Popconfirm diff --git a/content/feedback/popconfirm/index.md b/content/feedback/popconfirm/index.md index 1c71244065..53df4b37e4 100644 --- a/content/feedback/popconfirm/index.md +++ b/content/feedback/popconfirm/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 81 +order: 82 category: 反馈类 title: Popconfirm 气泡确认框 icon: doc-popconfirm diff --git a/content/feedback/progress/index-en-US.md b/content/feedback/progress/index-en-US.md index a8bbc569e0..cb0edf5af1 100644 --- a/content/feedback/progress/index-en-US.md +++ b/content/feedback/progress/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 82 +order: 83 category: Feedback title: Progress subTitle: Progress diff --git a/content/feedback/progress/index.md b/content/feedback/progress/index.md index f56010ccb6..21c91cd6ea 100644 --- a/content/feedback/progress/index.md +++ b/content/feedback/progress/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 82 +order: 83 category: 反馈类 title: Progress 进度条 icon: doc-progress diff --git a/content/feedback/skeleton/index-en-US.md b/content/feedback/skeleton/index-en-US.md index 15d06a0743..0a00439862 100644 --- a/content/feedback/skeleton/index-en-US.md +++ b/content/feedback/skeleton/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 83 +order: 84 category: Feedback title: Skeleton subTitle: Skeleton diff --git a/content/feedback/skeleton/index.md b/content/feedback/skeleton/index.md index 50eca18b1a..987dfaccb4 100644 --- a/content/feedback/skeleton/index.md +++ b/content/feedback/skeleton/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 83 +order: 84 category: 反馈类 title: Skeleton 骨架屏 icon: doc-skeleton diff --git a/content/feedback/spin/index-en-US.md b/content/feedback/spin/index-en-US.md index cd6cea267e..f93f1f5d42 100644 --- a/content/feedback/spin/index-en-US.md +++ b/content/feedback/spin/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 84 +order: 85 category: Feedback title: Spin subTitle: Spin diff --git a/content/feedback/spin/index.md b/content/feedback/spin/index.md index 4ff8a25801..4c5d68fe17 100644 --- a/content/feedback/spin/index.md +++ b/content/feedback/spin/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 84 +order: 85 category: 反馈类 title: Spin 加载器 icon: doc-spin diff --git a/content/feedback/toast/index-en-US.md b/content/feedback/toast/index-en-US.md index fcdaa205c2..b4c5b34d5c 100644 --- a/content/feedback/toast/index-en-US.md +++ b/content/feedback/toast/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 85 +order: 86 category: Feedback title: Toast subTitle: Toast diff --git a/content/feedback/toast/index.md b/content/feedback/toast/index.md index 62b784a717..92d95266ab 100644 --- a/content/feedback/toast/index.md +++ b/content/feedback/toast/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 85 +order: 86 category: 反馈类 title: Toast 提示 icon: doc-toast diff --git a/content/input/autocomplete/index-en-US.md b/content/input/autocomplete/index-en-US.md index b38f86efdc..e9ce6e124a 100644 --- a/content/input/autocomplete/index-en-US.md +++ b/content/input/autocomplete/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 29 +order: 30 category: Input title: AutoComplete icon: doc-autocomplete diff --git a/content/input/autocomplete/index.md b/content/input/autocomplete/index.md index 0d385139a4..a3130c5b5a 100644 --- a/content/input/autocomplete/index.md +++ b/content/input/autocomplete/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 29 +order: 30 category: 输入类 title: AutoComplete 自动完成 icon: doc-autocomplete diff --git a/content/input/cascader/index-en-US.md b/content/input/cascader/index-en-US.md index 1fbbb7a661..4e833ba946 100644 --- a/content/input/cascader/index-en-US.md +++ b/content/input/cascader/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 30 +order: 31 category: Input title: Cascader subTitle: Cascade diff --git a/content/input/cascader/index.md b/content/input/cascader/index.md index b8ada4f534..2a34d86078 100644 --- a/content/input/cascader/index.md +++ b/content/input/cascader/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 30 +order: 31 category: 输入类 title: Cascader 级联选择 icon: doc-cascader diff --git a/content/input/checkbox/index-en-US.md b/content/input/checkbox/index-en-US.md index d2da67e070..773cac9e1b 100644 --- a/content/input/checkbox/index-en-US.md +++ b/content/input/checkbox/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 31 +order: 32 category: Input title: Checkbox subTitle: Checkbox diff --git a/content/input/checkbox/index.md b/content/input/checkbox/index.md index fc95512fba..5457f82da0 100644 --- a/content/input/checkbox/index.md +++ b/content/input/checkbox/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 31 +order: 32 category: 输入类 title: Checkbox 复选框 icon: doc-checkbox diff --git a/content/input/colorpicker/index-en-US.md b/content/input/colorpicker/index-en-US.md index f4240b17e1..4982b3edb0 100644 --- a/content/input/colorpicker/index-en-US.md +++ b/content/input/colorpicker/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 32 +order: 33 category: Input title: ColorPicker icon: doc-colorPlatteNew diff --git a/content/input/colorpicker/index.md b/content/input/colorpicker/index.md index f296b8383f..6f1a3a73ee 100644 --- a/content/input/colorpicker/index.md +++ b/content/input/colorpicker/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 32 +order: 33 category: 输入类 title: ColorPicker 颜色选择器 icon: doc-colorPlatteNew diff --git a/content/input/datepicker/index-en-US.md b/content/input/datepicker/index-en-US.md index 780237e1f1..e9302987a0 100644 --- a/content/input/datepicker/index-en-US.md +++ b/content/input/datepicker/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 33 +order: 34 category: Input title: DatePicker subTitle: Date Selector diff --git a/content/input/datepicker/index.md b/content/input/datepicker/index.md index 431d1d3015..7450b8411c 100644 --- a/content/input/datepicker/index.md +++ b/content/input/datepicker/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 33 +order: 34 category: 输入类 title: DatePicker 日期选择器 icon: doc-datepicker diff --git a/content/input/form/index-en-US.md b/content/input/form/index-en-US.md index 82c1a771ff..843edc04ae 100644 --- a/content/input/form/index-en-US.md +++ b/content/input/form/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 34 +order: 35 category: Input title: Form subTitle: Form diff --git a/content/input/form/index.md b/content/input/form/index.md index 5f07fa4222..316868cda2 100644 --- a/content/input/form/index.md +++ b/content/input/form/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 34 +order: 35 category: 输入类 title: Form 表单 icon: doc-form diff --git a/content/input/input/index-en-US.md b/content/input/input/index-en-US.md index 621d047dab..f27708f11a 100644 --- a/content/input/input/index-en-US.md +++ b/content/input/input/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 35 +order: 36 category: Input title: Input subTitle: Input diff --git a/content/input/input/index.md b/content/input/input/index.md index ebba3a4e31..963090d51b 100644 --- a/content/input/input/index.md +++ b/content/input/input/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 35 +order: 36 category: 输入类 title: Input 输入框 icon: doc-input diff --git a/content/input/inputnumber/index-en-US.md b/content/input/inputnumber/index-en-US.md index 374aa5c6d6..283d4bc1ae 100644 --- a/content/input/inputnumber/index-en-US.md +++ b/content/input/inputnumber/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 36 +order: 37 category: Input title: InputNumber subTitle: InputNumber diff --git a/content/input/inputnumber/index.md b/content/input/inputnumber/index.md index f5c7f105c2..1352f654b8 100644 --- a/content/input/inputnumber/index.md +++ b/content/input/inputnumber/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 36 +order: 37 category: 输入类 title: InputNumber 数字输入框 icon: doc-inputnumber diff --git a/content/input/pincode/index-en-US.md b/content/input/pincode/index-en-US.md index 3058e4a0e3..327b520611 100644 --- a/content/input/pincode/index-en-US.md +++ b/content/input/pincode/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 37 +order: 38 category: Input title: PinCode icon: doc-pincode diff --git a/content/input/pincode/index.md b/content/input/pincode/index.md index b645958e59..b6f2a12479 100644 --- a/content/input/pincode/index.md +++ b/content/input/pincode/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 37 +order: 38 category: 输入类 title: PinCode 验证码输入 icon: doc-pincode diff --git a/content/input/radio/index-en-US.md b/content/input/radio/index-en-US.md index 8999ec9060..d587cbe303 100644 --- a/content/input/radio/index-en-US.md +++ b/content/input/radio/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 38 +order: 39 category: Input title: Radio subTitle: Radio diff --git a/content/input/radio/index.md b/content/input/radio/index.md index 2f85e5eb89..49917ba3a1 100644 --- a/content/input/radio/index.md +++ b/content/input/radio/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 38 +order: 39 category: 输入类 title: Radio 单选框 icon: doc-radio diff --git a/content/input/rating/index-en-US.md b/content/input/rating/index-en-US.md index 473f41c6ca..2c3d6b7d17 100644 --- a/content/input/rating/index-en-US.md +++ b/content/input/rating/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 39 +order: 40 category: Input title: Rating subTitle: Rating diff --git a/content/input/rating/index.md b/content/input/rating/index.md index 228cdb8b29..5630d9fc81 100644 --- a/content/input/rating/index.md +++ b/content/input/rating/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 39 +order: 40 category: 输入类 title: Rating 评分 icon: doc-rating diff --git a/content/input/select/index-en-US.md b/content/input/select/index-en-US.md index 028d02ff92..50a24d5b91 100644 --- a/content/input/select/index-en-US.md +++ b/content/input/select/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 40 +order: 41 category: Input title: Select subTitle: Select diff --git a/content/input/select/index.md b/content/input/select/index.md index 3bc23a455d..41d54927b6 100644 --- a/content/input/select/index.md +++ b/content/input/select/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 40 +order: 41 category: 输入类 title: Select 选择器 icon: doc-select diff --git a/content/input/slider/index-en-US.md b/content/input/slider/index-en-US.md index 12fc231c19..b48285c52a 100644 --- a/content/input/slider/index-en-US.md +++ b/content/input/slider/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 41 +order: 42 category: Input title: Slider subTitle: Slider diff --git a/content/input/slider/index.md b/content/input/slider/index.md index c5e8a3a8a1..b57f280b64 100644 --- a/content/input/slider/index.md +++ b/content/input/slider/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 41 +order: 42 category: 输入类 title: Slider 滑动选择器 icon: doc-slider diff --git a/content/input/switch/index-en-US.md b/content/input/switch/index-en-US.md index 4fc822cdaf..e3ecd71de9 100644 --- a/content/input/switch/index-en-US.md +++ b/content/input/switch/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 42 +order: 43 category: Input title: Switch subTitle: Switch diff --git a/content/input/switch/index.md b/content/input/switch/index.md index 8636ce1a79..18698f5190 100644 --- a/content/input/switch/index.md +++ b/content/input/switch/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 42 +order: 43 category: 输入类 title: Switch 开关 icon: doc-switch diff --git a/content/input/taginput/index-en-US.md b/content/input/taginput/index-en-US.md index 593b272699..a57523e927 100644 --- a/content/input/taginput/index-en-US.md +++ b/content/input/taginput/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 43 +order: 44 category: Input title: TagInput subTitle: TagInput diff --git a/content/input/taginput/index.md b/content/input/taginput/index.md index d13f668eeb..5d92979c3e 100644 --- a/content/input/taginput/index.md +++ b/content/input/taginput/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 43 +order: 44 category: 输入类 title: TagInput 标签输入框 icon: doc-tagInput diff --git a/content/input/timepicker/index-en-US.md b/content/input/timepicker/index-en-US.md index 012ba4dba1..199198f54f 100644 --- a/content/input/timepicker/index-en-US.md +++ b/content/input/timepicker/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 44 +order: 45 category: Input title: TimePicker subTitle: TimePicker diff --git a/content/input/timepicker/index.md b/content/input/timepicker/index.md index ddeaad31da..015ee76ee4 100644 --- a/content/input/timepicker/index.md +++ b/content/input/timepicker/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 44 +order: 45 category: 输入类 title: TimePicker 时间选择器 icon: doc-timepicker diff --git a/content/input/transfer/index-en-US.md b/content/input/transfer/index-en-US.md index 6e2106c572..153509a678 100644 --- a/content/input/transfer/index-en-US.md +++ b/content/input/transfer/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 45 +order: 46 category: Input title: Transfer icon: doc-transfer diff --git a/content/input/transfer/index.md b/content/input/transfer/index.md index fcf49606cf..9f8438906b 100644 --- a/content/input/transfer/index.md +++ b/content/input/transfer/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 45 +order: 46 category: 输入类 title: Transfer 穿梭框 icon: doc-transfer diff --git a/content/input/treeselect/index-en-US.md b/content/input/treeselect/index-en-US.md index 0c900b7054..9805d5cb84 100644 --- a/content/input/treeselect/index-en-US.md +++ b/content/input/treeselect/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 46 +order: 47 category: Input title: TreeSelect subTitle: TreeSelect diff --git a/content/input/treeselect/index.md b/content/input/treeselect/index.md index 4bcf7e7b34..6ee2a5ef11 100644 --- a/content/input/treeselect/index.md +++ b/content/input/treeselect/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 46 +order: 47 category: 输入类 title: TreeSelect 树选择器 icon: doc-treeselect diff --git a/content/input/upload/index-en-US.md b/content/input/upload/index-en-US.md index be8df92243..9bdbb6b0d2 100644 --- a/content/input/upload/index-en-US.md +++ b/content/input/upload/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 47 +order: 48 category: Input title: Upload icon: doc-upload diff --git a/content/input/upload/index.md b/content/input/upload/index.md index 8aeddd2da1..c2c5757b4a 100644 --- a/content/input/upload/index.md +++ b/content/input/upload/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 47 +order: 48 category: 输入类 title: Upload 上传 icon: doc-upload diff --git a/content/navigation/anchor/index-en-US.md b/content/navigation/anchor/index-en-US.md index e4ade621ef..fb45a3d621 100644 --- a/content/navigation/anchor/index-en-US.md +++ b/content/navigation/anchor/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 48 +order: 49 category: Navigation title: Anchor subTitle: Anchor diff --git a/content/navigation/anchor/index.md b/content/navigation/anchor/index.md index 06984f0bc3..f156aad534 100644 --- a/content/navigation/anchor/index.md +++ b/content/navigation/anchor/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 48 +order: 49 category: 导航类 title: Anchor 锚点 icon: doc-anchor diff --git a/content/navigation/backtop/index-en-US.md b/content/navigation/backtop/index-en-US.md index 10561db189..951928ff4d 100644 --- a/content/navigation/backtop/index-en-US.md +++ b/content/navigation/backtop/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 49 +order: 50 category: Navigation title: BackTop subTitle: BackTop diff --git a/content/navigation/backtop/index.md b/content/navigation/backtop/index.md index 4a2d0ec310..eca909ef77 100644 --- a/content/navigation/backtop/index.md +++ b/content/navigation/backtop/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 49 +order: 50 category: 导航类 title: BackTop 回到顶部 icon: doc-backtop diff --git a/content/navigation/breadcrumb/index-en-US.md b/content/navigation/breadcrumb/index-en-US.md index 63ab070c7e..bc20c0cb55 100644 --- a/content/navigation/breadcrumb/index-en-US.md +++ b/content/navigation/breadcrumb/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 50 +order: 51 category: Navigation title: Breadcrumb subTitle: Breadcrumb diff --git a/content/navigation/breadcrumb/index.md b/content/navigation/breadcrumb/index.md index 50b2dee6c0..cd0ed2ed2c 100644 --- a/content/navigation/breadcrumb/index.md +++ b/content/navigation/breadcrumb/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 50 +order: 51 category: 导航类 title: Breadcrumb 面包屑 icon: doc-breadcrumb diff --git a/content/navigation/navigation/index-en-US.md b/content/navigation/navigation/index-en-US.md index 2c8e8b35ba..0401985b3e 100644 --- a/content/navigation/navigation/index-en-US.md +++ b/content/navigation/navigation/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 51 +order: 52 category: Navigation title: Navigation subTitle: Navigation diff --git a/content/navigation/navigation/index.md b/content/navigation/navigation/index.md index 1172a1f0db..c462ae8831 100644 --- a/content/navigation/navigation/index.md +++ b/content/navigation/navigation/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 51 +order: 52 category: 导航类 title: Navigation 导航 icon: doc-navigation diff --git a/content/navigation/pagination/index-en-US.md b/content/navigation/pagination/index-en-US.md index a0364c51ac..5389fcafda 100644 --- a/content/navigation/pagination/index-en-US.md +++ b/content/navigation/pagination/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 52 +order: 53 category: Navigation title: Pagination subTitle: Pagination diff --git a/content/navigation/pagination/index.md b/content/navigation/pagination/index.md index 20c47919e0..18c94b2578 100644 --- a/content/navigation/pagination/index.md +++ b/content/navigation/pagination/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 52 +order: 53 category: 导航类 title: Pagination 翻页器 icon: doc-pagination diff --git a/content/navigation/steps/index-en-US.md b/content/navigation/steps/index-en-US.md index 3c5b576f0b..2fe1d3b138 100644 --- a/content/navigation/steps/index-en-US.md +++ b/content/navigation/steps/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 53 +order: 54 category: Navigation title: Steps subTitle: Steps diff --git a/content/navigation/steps/index.md b/content/navigation/steps/index.md index 8930b57430..6d176855c0 100644 --- a/content/navigation/steps/index.md +++ b/content/navigation/steps/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 53 +order: 54 category: 导航类 title: Steps 步骤 icon: doc-steps diff --git a/content/navigation/tabs/index-en-US.md b/content/navigation/tabs/index-en-US.md index 80728ea7fa..a5d7971499 100644 --- a/content/navigation/tabs/index-en-US.md +++ b/content/navigation/tabs/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 54 +order: 55 category: Navigation title: Tabs subTitle: Tabs diff --git a/content/navigation/tabs/index.md b/content/navigation/tabs/index.md index 9e329c2002..23dfd4df24 100644 --- a/content/navigation/tabs/index.md +++ b/content/navigation/tabs/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 54 +order: 55 category: 导航类 title: Tabs 标签栏 icon: doc-tabs diff --git a/content/navigation/tree/index-en-US.md b/content/navigation/tree/index-en-US.md index 208e033e19..6e958384c7 100644 --- a/content/navigation/tree/index-en-US.md +++ b/content/navigation/tree/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 55 +order: 56 category: Navigation title: Tree subTitle: Tree diff --git a/content/navigation/tree/index.md b/content/navigation/tree/index.md index d046770e63..21716be45f 100644 --- a/content/navigation/tree/index.md +++ b/content/navigation/tree/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 55 +order: 56 category: 导航类 title: Tree 树形控件 icon: doc-tree diff --git a/content/order.js b/content/order.js index 66e49f3bcf..5a7c8126db 100644 --- a/content/order.js +++ b/content/order.js @@ -25,6 +25,7 @@ const order = [ 'codehighlight', "markdownrender", "dragMove", + "jsonviewer", 'hotkeys', "lottie", 'autocomplete', @@ -85,7 +86,8 @@ const order = [ 'spin', 'toast', 'configprovider', - 'locale' + 'locale', + 'jsonviewer', ]; let { exec } = require('child_process'); let fs = require('fs'); diff --git a/content/other/configprovider/index-en-US.md b/content/other/configprovider/index-en-US.md index caac38cbbb..491d267366 100644 --- a/content/other/configprovider/index-en-US.md +++ b/content/other/configprovider/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 86 +order: 87 category: Other title: ConfigProvider icon: doc-configprovider diff --git a/content/other/configprovider/index.md b/content/other/configprovider/index.md index 71277ea5f5..1d30a41aa0 100644 --- a/content/other/configprovider/index.md +++ b/content/other/configprovider/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 86 +order: 87 category: 其他 title: ConfigProvider 全局配置 icon: doc-configprovider diff --git a/content/other/locale/index-en-US.md b/content/other/locale/index-en-US.md index f887d131e0..bec9391f44 100644 --- a/content/other/locale/index-en-US.md +++ b/content/other/locale/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 87 +order: 88 category: Other title: LocaleProvider subTitle: LocaleProvider diff --git a/content/other/locale/index.md b/content/other/locale/index.md index 2edda95516..6065edb032 100644 --- a/content/other/locale/index.md +++ b/content/other/locale/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 87 +order: 88 category: 其他 title: LocaleProvider 多语言 icon: doc-i18n diff --git a/content/plus/hotkeys/index-en-US.md b/content/plus/hotkeys/index-en-US.md index 48d5c6cee6..077baf3959 100644 --- a/content/plus/hotkeys/index-en-US.md +++ b/content/plus/hotkeys/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 27 +order: 28 category: Plus title: HotKeys icon: doc-configprovider diff --git a/content/plus/hotkeys/index.md b/content/plus/hotkeys/index.md index 4facbd0053..0b08c75836 100644 --- a/content/plus/hotkeys/index.md +++ b/content/plus/hotkeys/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 27 +order: 28 category: Plus title: HotKeys 快捷键 icon: doc-configprovider diff --git a/content/plus/jsonviewer/index-en-US.md b/content/plus/jsonviewer/index-en-US.md new file mode 100644 index 0000000000..32e4b58ae3 --- /dev/null +++ b/content/plus/jsonviewer/index-en-US.md @@ -0,0 +1,210 @@ +--- +localeCode: en-US +order: 27 +category: Plus +title: JsonViewer +icon: doc-jsonviewer +dir: column +noInline: true +brief: Used for displaying and editing JSON data +showNew: true +--- + +## When to use + +The JsonViewer component can be used for the display and editing of JSON data. + +Semi mainly referred to the design concept of the `text-buffer` data structure of [VS Code](https://github.com/microsoft/vscode), reused some utilities and data type definitions (Token parsing, language services, etc.), and implemented the JsonViewer component in combination with our functional/style customization requirements. Visually, it will be more coordinated with other components within the Semi Design system, and it will be more convenient for customized rendering and customization of specific data types. + +Compared with directly using MonacoEditor, Semi JsonViewer has additional processing in engineering construction, is simpler to use, and there is no need to pay attention to complex configurations such as Webpack plugins and worker loaders. At the same time, since we only focus on the JSON data format, it is more lightweight. While being ready to use out of the box, it has a smaller size **(📦-96%)**, a more extreme loading speed **(🚀 -53.5%)**, and less memory occupation **(⬇️71.6% reduction)**. For data with five million lines and below, data loading and parsing can be completed within 1 second. + +Detailed comparison data can be referred to in the [Performance](#Performance) section. + +- If you only need to preview/edit JSON and don't need to modify other more complex programming languages, we recommend that you choose `JsonViewer`. +- If you also need to handle data/code files in other formats and the full capabilities of a code editor (syntax highlighting, code completion, error prompts, complex editing, etc.) are essential and the build product size is not a key concern, we recommend that you choose `Monaco Editor`. + +## Demos + +### How to import + +JsonViewer supported from v2.71.0 + +```jsx import +import { JsonViewer } from '@douyinfe/semi-ui'; +``` + +### Basic Usage + +Basic usage of JsonViewer. Pass in the `height` and `width` parameters to set the height, width and initial value of the component. Pass in the JSON string through the `value`. + +```jsx live=true dir="column" noInline=true +import React from 'react'; +import { JsonViewer } from '@douyinfe/semi-ui'; +const data = `{ + "name": "Semi", + "version": "0.0.0" +}`; +class SimpleJsonViewer extends React.Component { + render() { + return ( +
+ +
+ ); + } +} + +render(SimpleJsonViewer); +``` + +### Differrent lineHeight + +Configure the `lineHeight` parameter of `options` to set a fixed line height (unit: px, default 18). + +```jsx live=true dir="column" noInline=true +import React from 'react'; +import { JsonViewer, Space } from '@douyinfe/semi-ui'; +const data = `{ + "name": "Semi", + "version": "0.0.0" +}`; +class SimpleJsonViewerWithLineHeight extends React.Component { + render() { + return ( +
+
+ +
+
+ +
+
+ +
+
+ ); + } +} + +render(SimpleJsonViewerWithLineHeight); +``` + +### Autowrap + +Configure the `autoWrap` parameter of `options`. When it is set to `true`, the component will automatically wrap lines according to the length of the content. + +```jsx live=true dir="column" noInline=true +import React from 'react'; +import { JsonViewer } from '@douyinfe/semi-ui'; +const data = `{ + "name": "Semi", + "version": "0.0.0", + "description": "Semi Design is a design system that defines a set of mid_back design and front_end basic components." +}`; +class SimpleJsonViewerWithAutoWrap extends React.Component { + render() { + return ( +
+ +
+ ); + } +} + +render(SimpleJsonViewerWithAutoWrap); +``` + +### Format options + +Configure `options.formatOptions` to set the formatting configuration of the component. + +- tabSize: number,set the indent size to 4, which means each level of indentation is 4 spaces. +- insertSpaces: boolean,when it is true, it means using spaces for indentation, and when it is false, it means using tabs. +- eol: string,set the line break character, which can be `\n`,`\r\n`, + +```jsx live=true dir="column" noInline=true +import React, { useRef } from 'react'; +import { JsonViewer, Button } from '@douyinfe/semi-ui'; +const data = `{ + "name": "Semi", + "version": "0.0.0" +}`; +function FormatJsonComponent() { + const jsonviewerRef = useRef(); + return ( +
+ +
+ +
+
+ ); +} + +render(FormatJsonComponent); +``` + +## API Reference + +### JsonViewer + +| Attribute | Description | Type | Default | +| --- | --- | --- | --- | +| value | Display content | string | - | +| height | Height of wrapper DOM | number | - | +| width | Width of wrapper DOM | number | - | +| className | className of wrapper DOM | string | - | +| style | InlineStyle of wrapper DOM | object | - | +| options | Formatting configuration | JsonViewerOptions | - | +| onChange | Callback for content change | (value: string) => void | - | + +### JsonViewerOptions + +| Attribute | Description | Type | Default | +| ------------- | --------------------------------------- | ----------------- | ------- | +| lineHeight | Height of each line of content, unit:px | number | 20 | +| autoWrap | Whether to wrap lines automatically. | boolean | true | +| formatOptions | Content format setting | FormattingOptions | - | + +### FormattingOptions + +| Attribute | Description | Type | Default | +| ------------ | ------------------------------------- | ------- | ------- | +| tabSize | Indent size. Unit: px | number | 4 | +| insertSpaces | Whether to use spaces for indentation | boolean | true | +| eol | Line break character | string | '\n' | + +## Methods + +Methods bound to the component instance can be called via `ref` to achieve certain special interactions. + +| Method | Description | +| ---------- | ---------------------- | +| getValue() | Get current value | +| format() | Format current content | + +### Performance + +#### Bundle Size + +| Libs Name | Size | Size (Gzip) | +| ------------ | --------- | ----------- | +| JsonViewer | 203.14kb | 51.23kb | +| MonacoEditor | 5102.0 KB | 1322.7 KB | + +#### Time for rendering data of different magnitudes. + +> For details on the generation method of the test data, please refer to [URL](https://github.com/DouyinFE/semi-design/blob/main/packages/semi-ui/jsonViewer/_story/jsonViewer.stories.jsx) +> When the data volume exceeds 500,000 lines, ReactMonacoEditor turns off highlighting and other behaviors by default, and the data comparison does not follow the principle of a single variable. + +| Libs Name | 1k lines | 5k lines | 10 thousand lines | 100 thousand lines | 500 thousand lines | 1 million lines | 3 million lines | +| --- | --- | --- | --- | --- | --- | --- | --- | +| JsonViewer | 30.42ms | 30.66ms | 36.87ms | 52.73ms | 111.02ms | 178.81ms | 506.25ms | +| ReactMonacoEditor | 72.01ms | 73.76ms | 76.64ms | 97.89ms | 133.31ms | 202.79ms | 495.53ms | +| Performance improvement | 57.70% | 58.41% | 51.87% | 46.11% | - | - | - | diff --git a/content/plus/jsonviewer/index.md b/content/plus/jsonviewer/index.md new file mode 100644 index 0000000000..13a61119f0 --- /dev/null +++ b/content/plus/jsonviewer/index.md @@ -0,0 +1,206 @@ +--- +localeCode: zh-CN +order: 27 +category: Plus +title: JsonViewer Json编辑器 +icon: doc-jsonviewer +dir: column +noInline: true +brief: 用于展示和编辑 JSON 数据 +showNew: true +--- + +## 使用场景 +JsonViewer 组件可用于 JSON 数据的展示与编辑。 +Semi 重点参考了 [VS Code](https://github.com/microsoft/vscode)的 text-buffer 数据结构设计思路,复用了部分 utils与数据类型定义(Token解析,语言服务等),结合我们的功能/样式定制需求,实现了 JsonViewer 组件, 视觉上会与 Semi Design 体系内的其他组件更协调,对于特定数据类型的定制化渲染定制会更方便。 +相比于直接使用 MonacoEditor,Semi JsonViewer 在工程化构建上做了额外处理,使用更为简单,无需关注 Webpack插件、worker loader等复杂的配置。 +同时由于我们仅关注 Json 数据格式,更轻量化,在开箱即用的同时,拥有更小的体积**(📦 -96%)** ,更极致的加载速度**(🚀 -53.5%)** ,更少的内存占用**(⬇️ 71.6%)**。 +对于五百万行及以下的数据,均可以做到1s内完成数据加载与解析。 +详细的对比数据可查阅 [Performance](#Performance) 章节 +- 如果你仅需要对 Json 做预览/编辑,无需对更复杂的其他编程语言作修改,我们建议你选用 JsonViewer +- 如果你还需要处理其他格式的数据/代码文件,完整的代码编辑器能力(语法高亮、代码不全、错误提示、复杂编辑等)是刚需,构建产物体积不是关注重点,我们建议你选用 Monaco Editor + + +## 代码演示 + +### 如何引入 +JsonViewer 从 v2.71.0 开始支持 +```jsx import +import { JsonViewer } from '@douyinfe/semi-ui'; +``` + +### 基本用法 + +JsonViewer 的基本用法。传入 height 和 width 参数,设置组件的高度和宽度和初始值。通过 value 传入 Json 字符串 + +```jsx live=true dir="column" noInline=true +import React from 'react'; +import { JsonViewer } from '@douyinfe/semi-ui'; +const data = `{ + "name": "Semi", + "version": "0.0.0" +}`; +class SimpleJsonViewer extends React.Component { + render() { + return ( +
+ +
+ ); + } +} + +render(SimpleJsonViewer); +``` + +### 设置行高 + +配置 options 的 lineHeight 参数,设置固定行高(单位:px, 默认 18)。 + +```jsx live=true dir="column" noInline=true +import React from 'react'; +import { JsonViewer, Space } from '@douyinfe/semi-ui'; +const data = `{ + "name": "Semi", + "version": "0.0.0" +}`; +class SimpleJsonViewerWithLineHeight extends React.Component { + render() { + return ( +
+
+ +
+
+ +
+
+ +
+
+ ); + } +} + +render(SimpleJsonViewerWithLineHeight); +``` + +### 自动换行 + +配置 options 的 autoWrap 参数,设置为 true 时,组件会根据内容长度自动换行。 + +```jsx live=true dir="column" noInline=true +import React from 'react'; +import { JsonViewer } from '@douyinfe/semi-ui'; +const data = `{ + "name": "Semi", + "version": "0.0.0", + "description": "Semi Design is a design system that defines a set of mid_back design and front_end basic components." +}`; +class SimpleJsonViewerWithAutoWrap extends React.Component { + render() { + return ( +
+ +
+ ); + } +} + +render(SimpleJsonViewerWithAutoWrap); +``` + +### 格式化配置 + +配置 options 的 formatOptions 参数,设置组件的格式化配置。 + +- tabSize: number,设置缩进大小为4,表示每级缩进 4 个空格 +- insertSpaces: boolean,true 表示使用空格进行缩进,false 表示使用制表符(Tab) +- eol: string,设置换行符,可以是\n,\r\n, + +```jsx live=true dir="column" noInline=true +import React, { useRef } from 'react'; +import { JsonViewer, Button } from '@douyinfe/semi-ui'; +const data = `{ + "name": "Semi", + "version": "0.0.0" +}`; +function FormatJsonComponent() { + const jsonviewerRef = useRef(); + return ( +
+ +
+ +
+
+ ); +} + +render(FormatJsonComponent); +``` + + +## API 参考 + +### JsonViewer + +| 属性 | 说明 | 类型 | 默认值 | +|-------------------|------------------------------------------------|---------------------------------|--------------| +| value | 展示内容 | string | - | +| height | 高度 | number | - | +| width | 宽度 | number | - | +| className | 类名 | string | - | +| style | 内联样式 | object | - | +| options | 格式化配置 | JsonViewerOptions | - | +| onChange | 内容变化回调 | (value: string) => void | - | + +### JsonViewerOptions + +| 属性 | 说明 | 类型 | 默认值 | +|-------------------|------------------------------------------------|---------------------------------|-----------| +| lineHeight | 行高 | number | 20 | +| autoWrap | 是否自动换行 | boolean | true | +| formatOptions | 格式化配置 | FormattingOptions | - | + +### FormattingOptions + +| 属性 | 说明 | 类型 | 默认值 | +|-------------------|------------------------------------------------|---------------------------------|-----------| +| tabSize | 缩进大小 | number | 4 | +| insertSpaces | 是否使用空格进行缩进 | boolean | true | +| eol | 换行符 | string | '\n' | + +## Methods + +绑定在组件实例上的方法,可以通过 ref 调用实现某些特殊交互 + +| 名称 | 描述 | +|---------|--------| +| getValue() | 获取当前值 | +| format() | 格式化 | + + +### Performance +#### Bundle Size +| 组件 | 体积 | 体积(Gzip) | +| ------------ | --------- | ---------- | +| JsonViewer | 203.14kb | 51.23kb | +| MonacoEditor | 5102.0 KB | 1322.7 KB | + +#### 渲染不同量级数据耗时 +> 注: +> - 测试数据生成方式详情可查阅 [url](https://github.com/DouyinFE/semi-design/blob/main/packages/semi-ui/jsonViewer/_story/jsonViewer.stories.jsx) +> - 当数据量级超出50w行时,ReactMonacoEditor 默认关闭高亮等行为,数据对比不遵循单一变量原则 + +| 组件 | 1k行 | 5k行 | 1w行 | 10w行 | 50w行 | 100w行 | 300w行 | +| ----------------- | ------- | ------- | ------- | ------- | -------- | -------- | -------- | +| JsonViewer | 30.42ms | 30.66ms | 36.87ms | 52.73ms | 111.02ms | 178.81ms | 506.25ms | +| ReactMonacoEditor | 72.01ms | 73.76ms | 76.64ms | 97.89ms | 133.31ms | 202.79ms | 495.53ms | +| 性能提升 | 57.70% | 58.41% | 51.87% | 46.11% | - | - | - | \ No newline at end of file diff --git a/content/plus/lottie/index-en-US.md b/content/plus/lottie/index-en-US.md index b672adbef3..15144197e2 100644 --- a/content/plus/lottie/index-en-US.md +++ b/content/plus/lottie/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 28 +order: 29 category: Plus title: Lottie Animation icon: doc-lottie diff --git a/content/plus/lottie/index.md b/content/plus/lottie/index.md index a96c8c0b01..fd84d127f2 100644 --- a/content/plus/lottie/index.md +++ b/content/plus/lottie/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 28 +order: 29 category: Plus title: Lottie 动画 icon: doc-lottie diff --git a/content/show/avatar/index-en-US.md b/content/show/avatar/index-en-US.md index 7e12500e2d..c8ff1778ad 100644 --- a/content/show/avatar/index-en-US.md +++ b/content/show/avatar/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 56 +order: 57 category: Show title: Avatar subTitle: avatar diff --git a/content/show/avatar/index.md b/content/show/avatar/index.md index 93c604eaff..a7f375c184 100644 --- a/content/show/avatar/index.md +++ b/content/show/avatar/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 56 +order: 57 category: 展示类 title: Avatar 头像 icon: doc-avatar diff --git a/content/show/badge/index-en-US.md b/content/show/badge/index-en-US.md index 65f91c39b2..95d52ec535 100644 --- a/content/show/badge/index-en-US.md +++ b/content/show/badge/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 57 +order: 58 category: Show title: Badge subTitle: Badge diff --git a/content/show/badge/index.md b/content/show/badge/index.md index 5b9220b102..a3c2c339d0 100644 --- a/content/show/badge/index.md +++ b/content/show/badge/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 57 +order: 58 category: 展示类 title: Badge 徽章 icon: doc-badge diff --git a/content/show/calendar/index-en-US.md b/content/show/calendar/index-en-US.md index 436df963a5..efd38b1b07 100644 --- a/content/show/calendar/index-en-US.md +++ b/content/show/calendar/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 58 +order: 59 category: Show title: Calendar subTitle: Calendar diff --git a/content/show/calendar/index.md b/content/show/calendar/index.md index c3c9ee2e76..877a308fc7 100644 --- a/content/show/calendar/index.md +++ b/content/show/calendar/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 58 +order: 59 category: 展示类 title: Calendar 日历 icon: doc-calendar diff --git a/content/show/card/index-en-US.md b/content/show/card/index-en-US.md index 9358fac4b9..6e6b082f87 100644 --- a/content/show/card/index-en-US.md +++ b/content/show/card/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 59 +order: 60 category: Show title: Card subTitle: Card diff --git a/content/show/card/index.md b/content/show/card/index.md index f63216e2ce..3ca43a8b8e 100644 --- a/content/show/card/index.md +++ b/content/show/card/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 59 +order: 60 category: 展示类 title: Card 卡片 subTitle: 卡片 diff --git a/content/show/carousel/index-en-US.md b/content/show/carousel/index-en-US.md index 887ad2b00d..468792a14c 100644 --- a/content/show/carousel/index-en-US.md +++ b/content/show/carousel/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 60 +order: 61 category: Show title: Carousel subTitle: Carousel diff --git a/content/show/carousel/index.md b/content/show/carousel/index.md index 656d04b7ce..80cadbfc14 100644 --- a/content/show/carousel/index.md +++ b/content/show/carousel/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 60 +order: 61 category: 展示类 title: Carousel 轮播图 icon: doc-carousel diff --git a/content/show/chart/index-en-US.md b/content/show/chart/index-en-US.md index 9619c4f946..038c55303d 100644 --- a/content/show/chart/index-en-US.md +++ b/content/show/chart/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 78 +order: 79 category: Show title: Data Visualization icon: doc-vchart diff --git a/content/show/chart/index.md b/content/show/chart/index.md index 653139efad..673625ef7c 100644 --- a/content/show/chart/index.md +++ b/content/show/chart/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 78 +order: 79 category: 展示类 title: Data Visualization 数据可视化 icon: doc-vchart diff --git a/content/show/collapse/index-en-US.md b/content/show/collapse/index-en-US.md index 17d014026d..4804587127 100644 --- a/content/show/collapse/index-en-US.md +++ b/content/show/collapse/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 61 +order: 62 category: Show title: Collapse subTitle: Collapse diff --git a/content/show/collapse/index.md b/content/show/collapse/index.md index f779f8f4a1..9fc24da381 100644 --- a/content/show/collapse/index.md +++ b/content/show/collapse/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 61 +order: 62 category: 展示类 title: Collapse 折叠面板 icon: doc-accordion diff --git a/content/show/collapsible/index-en-US.md b/content/show/collapsible/index-en-US.md index 6b5e7497ec..213b12cc3a 100644 --- a/content/show/collapsible/index-en-US.md +++ b/content/show/collapsible/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 62 +order: 63 category: Show title: Collapsible subTitle: Collapsible diff --git a/content/show/collapsible/index.md b/content/show/collapsible/index.md index 96702c3e9a..73924c8c19 100644 --- a/content/show/collapsible/index.md +++ b/content/show/collapsible/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 62 +order: 63 category: 展示类 title: Collapsible 折叠 icon: doc-collapsible diff --git a/content/show/descriptions/index-en-US.md b/content/show/descriptions/index-en-US.md index 1acf928601..9e7c03cd00 100644 --- a/content/show/descriptions/index-en-US.md +++ b/content/show/descriptions/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 63 +order: 64 category: Show title: Description subTitle: Descriptions diff --git a/content/show/descriptions/index.md b/content/show/descriptions/index.md index 3612e24a48..577358717e 100644 --- a/content/show/descriptions/index.md +++ b/content/show/descriptions/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 63 +order: 64 category: 展示类 title: Descriptions 描述列表 icon: doc-descriptions diff --git a/content/show/dropdown/index-en-US.md b/content/show/dropdown/index-en-US.md index 428f647ba9..208eb1fa1b 100644 --- a/content/show/dropdown/index-en-US.md +++ b/content/show/dropdown/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 64 +order: 65 category: Show title: Dropdown subTitle: Dropdown diff --git a/content/show/dropdown/index.md b/content/show/dropdown/index.md index 520d5104d0..7af3b6cd6d 100644 --- a/content/show/dropdown/index.md +++ b/content/show/dropdown/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 64 +order: 65 category: 展示类 title: Dropdown 下拉框 icon: doc-dropdown diff --git a/content/show/empty/index-en-US.md b/content/show/empty/index-en-US.md index e3a49ccab3..e770794ded 100644 --- a/content/show/empty/index-en-US.md +++ b/content/show/empty/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 65 +order: 66 category: Show title: Empty subTitle: Empty diff --git a/content/show/empty/index.md b/content/show/empty/index.md index 633fe8ce46..3f9d10fa9b 100644 --- a/content/show/empty/index.md +++ b/content/show/empty/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 65 +order: 66 category: 展示类 title: Empty 空状态 icon: doc-empty diff --git a/content/show/highlight/index-en-US.md b/content/show/highlight/index-en-US.md index 1d341cfa5f..0b28d5b60d 100644 --- a/content/show/highlight/index-en-US.md +++ b/content/show/highlight/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 66 +order: 67 category: Show title: Highlight icon: doc-highlight diff --git a/content/show/highlight/index.md b/content/show/highlight/index.md index 4df0096aeb..29c0cf21ff 100644 --- a/content/show/highlight/index.md +++ b/content/show/highlight/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 66 +order: 67 category: 展示类 title: Highlight 高亮文本 icon: doc-highlight diff --git a/content/show/image/index-en-US.md b/content/show/image/index-en-US.md index 4f846f0032..3db4f06935 100644 --- a/content/show/image/index-en-US.md +++ b/content/show/image/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 67 +order: 68 category: Show title: Image icon: doc-image diff --git a/content/show/image/index.md b/content/show/image/index.md index 7de09e3438..5a3a6759d9 100644 --- a/content/show/image/index.md +++ b/content/show/image/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 67 +order: 68 category: 展示类 title: Image 图片 icon: doc-image diff --git a/content/show/list/index-en-US.md b/content/show/list/index-en-US.md index 4b5f79cd9e..09c9a85107 100644 --- a/content/show/list/index-en-US.md +++ b/content/show/list/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 68 +order: 69 category: Show title: List subTitle: List diff --git a/content/show/list/index.md b/content/show/list/index.md index 02a93f0c81..cce80aee1b 100644 --- a/content/show/list/index.md +++ b/content/show/list/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 68 +order: 69 category: 展示类 title: List 列表 icon: doc-list diff --git a/content/show/modal/index-en-US.md b/content/show/modal/index-en-US.md index cdcbf27b02..1044de8409 100644 --- a/content/show/modal/index-en-US.md +++ b/content/show/modal/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 69 +order: 70 category: Show title: Modal subTitle: Modal diff --git a/content/show/modal/index.md b/content/show/modal/index.md index 91cda8ed06..abea17cdab 100644 --- a/content/show/modal/index.md +++ b/content/show/modal/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 69 +order: 70 category: 展示类 title: Modal 模态对话框 icon: doc-modal diff --git a/content/show/overflowlist/index-en-US.md b/content/show/overflowlist/index-en-US.md index 699cb28b93..c9d8634adf 100644 --- a/content/show/overflowlist/index-en-US.md +++ b/content/show/overflowlist/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 70 +order: 71 category: Show title: OverflowList subTitle: OverflowList diff --git a/content/show/overflowlist/index.md b/content/show/overflowlist/index.md index c449c3f75b..56dd30e585 100644 --- a/content/show/overflowlist/index.md +++ b/content/show/overflowlist/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 70 +order: 71 category: 展示类 title: OverflowList 折叠列表 icon: doc-overflowList diff --git a/content/show/popover/index-en-US.md b/content/show/popover/index-en-US.md index 0f2ac631ad..03da78c3d9 100644 --- a/content/show/popover/index-en-US.md +++ b/content/show/popover/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 71 +order: 72 category: Show title: Popover subTitle: Popover diff --git a/content/show/popover/index.md b/content/show/popover/index.md index c575b99837..7cc3de7273 100644 --- a/content/show/popover/index.md +++ b/content/show/popover/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 71 +order: 72 category: 展示类 title: Popover 气泡卡片 icon: doc-popover diff --git a/content/show/scrolllist/index-en-US.md b/content/show/scrolllist/index-en-US.md index 2d7d3b3ba8..41ea1c751f 100644 --- a/content/show/scrolllist/index-en-US.md +++ b/content/show/scrolllist/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 72 +order: 73 category: Show title: ScrollList subTitle: ScrollList diff --git a/content/show/scrolllist/index.md b/content/show/scrolllist/index.md index a74bc54269..a37becd949 100644 --- a/content/show/scrolllist/index.md +++ b/content/show/scrolllist/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 72 +order: 73 category: 展示类 title: ScrollList 滚动列表 icon: doc-scrolllist diff --git a/content/show/sidesheet/index-en-US.md b/content/show/sidesheet/index-en-US.md index 334c71f349..dae578d7c8 100644 --- a/content/show/sidesheet/index-en-US.md +++ b/content/show/sidesheet/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 73 +order: 74 category: Show title: SideSheet subTitle: SideSheet diff --git a/content/show/sidesheet/index.md b/content/show/sidesheet/index.md index 4bff453bb4..4cbab1aeaf 100644 --- a/content/show/sidesheet/index.md +++ b/content/show/sidesheet/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 73 +order: 74 category: 展示类 title: SideSheet 滑动侧边栏 icon: doc-sidesheet diff --git a/content/show/table/index-en-US.md b/content/show/table/index-en-US.md index 8caa1057c0..8270c9de0a 100644 --- a/content/show/table/index-en-US.md +++ b/content/show/table/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 74 +order: 75 category: Show title: Table subTitle: Table diff --git a/content/show/table/index.md b/content/show/table/index.md index 6ed2f02b81..fcdf31b0f3 100644 --- a/content/show/table/index.md +++ b/content/show/table/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 74 +order: 75 category: 展示类 title: Table 表格 icon: doc-table diff --git a/content/show/tag/index-en-US.md b/content/show/tag/index-en-US.md index bd22dcd2e3..8d01ed8fd2 100644 --- a/content/show/tag/index-en-US.md +++ b/content/show/tag/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 75 +order: 76 category: Show title: Tag subTitle: Tag diff --git a/content/show/tag/index.md b/content/show/tag/index.md index 8f12709180..36f46ee9aa 100644 --- a/content/show/tag/index.md +++ b/content/show/tag/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 75 +order: 76 category: 展示类 title: Tag 标签 icon: doc-tag diff --git a/content/show/timeline/index-en-US.md b/content/show/timeline/index-en-US.md index 922564d543..9cd53102d1 100644 --- a/content/show/timeline/index-en-US.md +++ b/content/show/timeline/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 76 +order: 77 category: Show title: Timeline subTitle: Timeline diff --git a/content/show/timeline/index.md b/content/show/timeline/index.md index 3f75a7d050..920ba297e2 100644 --- a/content/show/timeline/index.md +++ b/content/show/timeline/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 76 +order: 77 category: 展示类 title: Timeline 时间轴 icon: doc-timeline diff --git a/content/show/tooltip/index-en-US.md b/content/show/tooltip/index-en-US.md index fc02f25b03..8a57adb687 100644 --- a/content/show/tooltip/index-en-US.md +++ b/content/show/tooltip/index-en-US.md @@ -1,6 +1,6 @@ --- localeCode: en-US -order: 77 +order: 78 category: Show title: Tooltip subTitle: Tooltip diff --git a/content/show/tooltip/index.md b/content/show/tooltip/index.md index 8271939e85..f2f7b146d9 100644 --- a/content/show/tooltip/index.md +++ b/content/show/tooltip/index.md @@ -1,6 +1,6 @@ --- localeCode: zh-CN -order: 77 +order: 78 category: 展示类 title: Tooltip 工具提示 icon: doc-tooltip diff --git a/content/start/changelog/index-en-US.md b/content/start/changelog/index-en-US.md index 893e7c6053..e49a744e8f 100644 --- a/content/start/changelog/index-en-US.md +++ b/content/start/changelog/index-en-US.md @@ -17,6 +17,7 @@ Version:Major.Minor.Patch (follow the **Semver** specification) --- #### 🎉 2.71.0-beta.0 (2024-12-02) + - 【New Component】 - Add `DragMove` Component,Change the positioning by dragging. [#2595](https://github.com/DouyinFE/semi-design/pull/2595) - Add `JsonViewer` Component,support the display and editing of JSON data at the million-line level. [#2561](https://github.com/DouyinFE/semi-design/pull/2561) diff --git a/content/start/changelog/index.md b/content/start/changelog/index.md index 907eb8980a..166e047cc6 100644 --- a/content/start/changelog/index.md +++ b/content/start/changelog/index.md @@ -15,6 +15,7 @@ Semi 版本号遵循 **Semver** 规范(主版本号-次版本号-修订版本 #### 🎉 2.71.0-beta.0 (2024-12-02) + - 【New Component】 - 新增 DragMove 组件,通过拖拽改变定位 [#2595](https://github.com/DouyinFE/semi-design/pull/2595) - 新增 JsonViewer 组件,支持百万行级 JSON 数据的展示与编辑 [#2561](https://github.com/DouyinFE/semi-design/pull/2561) diff --git a/gatsby-node.js b/gatsby-node.js index cd5547de83..970c7efe7d 100644 --- a/gatsby-node.js +++ b/gatsby-node.js @@ -112,6 +112,7 @@ exports.onCreateWebpackConfig = ({ stage, rules, loaders, plugins, actions }) => 'semi-site-header': process.env.SEMI_SITE_HEADER || '@douyinfe/semi-site-header', 'semi-site-banner': process.env.SEMI_SITE_BANNER || '@douyinfe/semi-site-banner', 'univers-webview': process.env.SEMI_SITE_UNIVERS_WEBVIEW || resolve('packages/semi-ui'), + '@douyinfe/semi-json-viewer-core': resolve('packages/semi-json-viewer-core'), '@douyinfe/semi-ui': resolve('packages/semi-ui'), '@douyinfe/semi-foundation': resolve('packages/semi-foundation'), '@douyinfe/semi-icons': resolve('packages/semi-icons/src/'), @@ -155,7 +156,7 @@ exports.onCreateWebpackConfig = ({ stage, rules, loaders, plugins, actions }) => }, { test: /\.m?js/, - include: [/micromark-util-sanitize-uri/, /mdast-util-from-markdown/, /micromark/, /mdast-util-to-markdown/, /semi-foundation\/node_modules\/@mdx-js/], + include: [/micromark-util-sanitize-uri/, /mdast-util-from-markdown/, /micromark/, /mdast-util-to-markdown/, /semi-foundation\/node_modules\/@mdx-js/, /jsonc-parser/], use: ["esbuild-loader"] }, { @@ -184,7 +185,8 @@ exports.onCreateWebpackConfig = ({ stage, rules, loaders, plugins, actions }) => test: /\.mjs$/, include: /node_modules/, type: "javascript/auto" - } + }, + { test: /\.worker\.ts$/, use: ['worker-loader', 'ts-loader'] } ], }, plugins: [plugins.extractText(), plugins.define({ diff --git a/jest.config.js b/jest.config.js index b9c285bcd7..adddcfcec8 100644 --- a/jest.config.js +++ b/jest.config.js @@ -64,6 +64,7 @@ let config = { '@douyinfe/semi-foundation(.*)$': '/packages/semi-foundation/$1', '@douyinfe/semi-illustrations(.*)$': '/packages/semi-illustrations/src/$1', '@douyinfe/semi-icons(.*)$': '/packages/semi-icons/src/$1', + '@douyinfe/semi-json-viewer-core(.*)$': '/packages/semi-json-viewer-core/src/$1', // 将semi-animation相关的直接指向它的cjs版本,这样不用再走一次babel-jest的编译 '@douyinfe/semi-animation-styled(.*)$': '/packages/semi-animation-styled', '@douyinfe/semi-animation-react(.*)$': '/packages/semi-animation-react', diff --git a/package.json b/package.json index 8c19694aab..06bfe33275 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,10 @@ "bootstrap": "lerna bootstrap -- --legacy-peer-deps", "docsite": "npm run develop", "pre-develop": "npm run scripts:changelog && node ./scripts/designToken.js ./static/designToken.json", - "develop": "npm run pre-develop && gatsby clean && lerna run build:lib --scope @douyinfe/semi-webpack-plugin --scope eslint-plugin-semi-design && gatsby develop -H 0.0.0.0 --port=3666 --verbose", + "develop": "npm run pre-develop && gatsby clean && lerna run build:lib --scope @douyinfe/semi-json-viewer-core --scope @douyinfe/semi-webpack-plugin --scope eslint-plugin-semi-design && gatsby develop -H 0.0.0.0 --port=3666 --verbose", "scripts:changelog": "node scripts/changelog.js", "start": "npm run story", - "pre-story": "lerna exec --scope=@douyinfe/semi-ui --scope=@douyinfe/semi-foundation -- rimraf ./lib && lerna run build:lib --scope @douyinfe/semi-webpack-plugin --scope eslint-plugin-semi-design", + "pre-story": "lerna exec --scope=@douyinfe/semi-ui --scope=@douyinfe/semi-foundation -- rimraf ./lib && lerna run build:lib --scope @douyinfe/semi-json-viewer-core --scope @douyinfe/semi-webpack-plugin --scope eslint-plugin-semi-design", "story": "npm run pre-story && sb dev -c ./.storybook/js/ -p 6006", "story:ts": "npm run pre-story && sb dev -c ./.storybook/ts/ -p 6007", "story:ani": "npm run pre-story && sb dev -c ./.storybook/animation/react -p 6008", @@ -35,7 +35,7 @@ "build:css": "lerna run build:css", "build-storybook": "sb build -c ./.storybook/js/ -o ./storybook && cp -r storybook storybook-static", "build-storybook-static": "sb build -c ./.storybook/js/", - "build:gatsbydoc": "lerna run build:lib --scope @douyinfe/semi-webpack-plugin --scope eslint-plugin-semi-design && cross-env NODE_ENV=production node --max_old_space_size=16384 ./node_modules/gatsby/cli.js build --prefix-paths --verbose && rimraf build && mv public build", + "build:gatsbydoc": "lerna run build:lib --scope --scope @douyinfe/semi-json-viewer-core --scope @douyinfe/semi-webpack-plugin --scope eslint-plugin-semi-design && cross-env NODE_ENV=production node --max_old_space_size=16384 ./node_modules/gatsby/cli.js build --prefix-paths --verbose && rimraf build && mv public build", "build:icon": "lerna run build:icon --scope='@douyinfe/semi-{icons,illustrations}'", "cypress:coverage": "npx wait-on http://127.0.0.1:6006 && ./node_modules/.bin/cypress run", "postcypress:coverage": "yarn coverage:merge", @@ -217,7 +217,8 @@ "webpack": "^5.77.0", "webpack-cli": "^5.1.4", "webpack-dev-server": "^3.11.2", - "webpackbar": "^5.0.0-3" + "webpackbar": "^5.0.0-3", + "worker-loader": "^3.0.8" }, "husky": { "hooks": { @@ -242,5 +243,6 @@ "stylelint" ] }, - "license": "MIT" + "license": "MIT", + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/packages/semi-foundation/jsonViewer/constants.ts b/packages/semi-foundation/jsonViewer/constants.ts new file mode 100644 index 0000000000..4642544722 --- /dev/null +++ b/packages/semi-foundation/jsonViewer/constants.ts @@ -0,0 +1,7 @@ +import { BASE_CLASS_PREFIX } from "../base/constants"; + +const cssClasses = { + PREFIX: `${BASE_CLASS_PREFIX}-json-viewer`, +} as const; + +export { cssClasses }; diff --git a/packages/semi-foundation/jsonViewer/foundation.ts b/packages/semi-foundation/jsonViewer/foundation.ts new file mode 100644 index 0000000000..0fad861339 --- /dev/null +++ b/packages/semi-foundation/jsonViewer/foundation.ts @@ -0,0 +1,72 @@ + +import { JsonViewer, JsonViewerOptions } from '@douyinfe/semi-json-viewer-core'; +import BaseFoundation, { DefaultAdapter, noopFunction } from '../base/foundation'; + +export type { JsonViewerOptions }; +export interface JsonViewerAdapter

, S = Record> extends DefaultAdapter { + getEditorRef: () => HTMLElement; + getSearchRef: () => HTMLInputElement; + notifyChange: (value: string) => void; + notifyHover: (value: string, el: HTMLElement) => HTMLElement | undefined; + setSearchOptions: (key: string) => void; + showSearchBar: () => void +} + +class JsonViewerFoundation extends BaseFoundation { + constructor(adapter: JsonViewerAdapter) { + super({ ...JsonViewerFoundation, ...adapter }); + } + + jsonViewer: JsonViewer | null = null; + + init() { + const props = this.getProps(); + const editorRef = this._adapter.getEditorRef(); + this.jsonViewer = new JsonViewer(editorRef, props.value, props.options); + this.jsonViewer.layout(); + this.jsonViewer.emitter.on('contentChanged', (e) => { + this._adapter.notifyChange(this.jsonViewer?.getModel().getValue()); + if (this.getState('showSearchBar')) { + this.search(this._adapter.getSearchRef().value); + } + }); + this.jsonViewer.emitter.on('hoverNode', (e) => { + const el = this._adapter.notifyHover(e.value, e.target); + if (el) { + this.jsonViewer.emitter.emit('renderHoverNode', { el }); + } + }); + } + + search(searchText: string) { + const state = this.getState('searchOptions'); + const { caseSensitive, wholeWord, regex } = state; + this.jsonViewer?.getSearchWidget().search(searchText, caseSensitive, wholeWord, regex); + } + + prevSearch() { + this.jsonViewer?.getSearchWidget().navigateResults(-1); + } + + nextSearch() { + this.jsonViewer?.getSearchWidget().navigateResults(1); + } + + replace(replaceText: string) { + this.jsonViewer?.getSearchWidget().replace(replaceText); + } + + replaceAll(replaceText: string) { + this.jsonViewer?.getSearchWidget().replaceAll(replaceText); + } + + setSearchOptions(key: string) { + this._adapter.setSearchOptions(key); + } + + showSearchBar() { + this._adapter.showSearchBar(); + } +} + +export default JsonViewerFoundation; \ No newline at end of file diff --git a/packages/semi-foundation/jsonViewer/jsonViewer.scss b/packages/semi-foundation/jsonViewer/jsonViewer.scss new file mode 100644 index 0000000000..0ec7c1bf5a --- /dev/null +++ b/packages/semi-foundation/jsonViewer/jsonViewer.scss @@ -0,0 +1,200 @@ +@import './variables.scss'; + +$module: #{$prefix}-json-viewer; + +.#{$module} { + &-background { + background-color: $color-json-viewer-background; + } + + &-string-key { + color: $color-json-viewer-key; + } + + &-string-value { + color: $color-json-viewer-value; + } + + &-keyword { + color: $color-json-viewer-keyword; + } + + &-number { + color: $color-json-viewer-number; + } + + &-delimiter-comma { + color: $color-json-viewer-delimiter-comma; + } + + &-delimiter-bracket-0 { + color: rgba(var(--semi-blue-7), 1); + } + &-delimiter-bracket-1 { + color: rgba(var(--semi-green-7), 1); + } + &-delimiter-bracket-2 { + color: rgba(var(--semi-orange-7), 1); + } + &-delimiter-array-0 { + color: rgba(var(--semi-blue-7), 1); + } + &-delimiter-array-1 { + color: rgba(var(--semi-green-7), 1); + } + &-delimiter-array-2 { + color: rgba(var(--semi-orange-7), 1); + } + + &-search-result { + background-color: $color-json-viewer-search-result-background; + } + + &-current-search-result { + background-color: $color-json-viewer-current-search-result-background !important; + } + + &-folding-icon { + opacity: 0.7; + transition: opacity 0.8s; + color: $color-json-viewer-folding-icon; + } + + &-view-line { + font-family: Menlo, Firecode, Monaco, 'Courier New', monospace; + font-weight: normal; + font-size: 12px; + font-feature-settings: 'liga' 0, 'calt' 0; + font-variation-settings: normal; + letter-spacing: 0px; + color: #237893; + word-wrap: break-word; + white-space: pre-wrap; + } + + &-line-number { + font-family: Menlo, Firecode, Monaco, 'Courier New', monospace; + font-weight: normal; + font-size: 12px; + font-feature-settings: 'liga' 0, 'calt' 0; + font-variation-settings: normal; + letter-spacing: 0px; + color: $color-json-viewer-line-number; + text-align: center; + width: 50px; + } + + &-content-container { + scrollbar-width: none; /* 隐藏滚动条(Firefox) */ + -ms-overflow-style: none; /* 隐藏滚动条(IE 和 Edge) */ + } + + &-content-container::-webkit-scrollbar { + display: none; /* 隐藏滚动条(Webkit 浏览器) */ + } + + &-search-bar-container { + width: 458px; + box-sizing: border-box; + border: 1px solid var(--semi-color-border); + border-radius: var(--semi-border-radius-small); + display: flex; + flex-direction: column; + padding: 8px; + gap: 8px; + background-color: var(--semi-color-bg-0); + } + + &-search-bar { + display: flex; + align-items: flex-start; + gap: 8px; + &-input { + width: 200px; + flex-shrink: 0; + } + .#{$prefix}-button-group { + flex-wrap: nowrap; + } + // next icon btn + .#{$prefix}-button:nth-of-type(1) { + width: 40px; + } + // prev icon btn + .#{$prefix}-button:nth-of-type(2) { + width: 40px; + } + } + + &-replace-bar { + display: flex; + align-items: flex-start; + gap: 8px; + &-input { + width: 261px; + } + // replace btn + .#{$prefix}-button:nth-of-type(1) { + width: 52px; + } + // all replace btn + .#{$prefix}-button:nth-of-type(2) { + width: 80px; + } + } + + &-search-options { + display: flex; + align-items: center; + justify-content: center; + list-style: none; + padding-inline-start: 0; + margin-block-start: 0; + margin-block-end: 0; + gap: 8px; + } + + &-search-options-item { + min-width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + border-radius: var(--semi-border-radius-small); + color: var(--semi-color-text-2); + } + + &-search-options-item:hover { + background-color: var(--semi-color-default); + } + + &-search-options-item-active { + color: var(--semi-color-primary); + background-color: var(--semi-color-primary-light-default); + } + + &-complete-suggestions-container { + border-radius: var(--semi-border-radius-medium); + background-color: var(--semi-color-bg-3); + box-shadow: var(--semi-shadow-elevated); + z-index: 1000; + min-width: 200px; + max-width: 400px; + list-style: none; + padding: 4px 0; + } + + &-complete-container { + position: absolute; + z-index: 1000; + } + + &-complete-suggestions-item { + padding: 8px 16px; + color: var(--semi-color-text-0); + cursor: pointer; + } + + +} diff --git a/packages/semi-foundation/jsonViewer/script/build.js b/packages/semi-foundation/jsonViewer/script/build.js new file mode 100644 index 0000000000..15d505bca6 --- /dev/null +++ b/packages/semi-foundation/jsonViewer/script/build.js @@ -0,0 +1,51 @@ +const esbuild = require('esbuild'); +const path = require('path'); +const fs = require('fs'); + + + + +const compileWorker = async ()=>{ + const workerEntry = path.join(__dirname, "..", "core/src/worker/json.worker.ts"); + + const result = await esbuild.build({ + entryPoints: [workerEntry], + bundle: true, + write: false, + }); + return result.outputFiles[0].text; +}; + + +const buildMain = async ()=>{ + const mainEntry = path.join(__dirname, "..", "core/src/index.ts"); + + const result = await esbuild.build({ + entryPoints: [mainEntry], + bundle: true, + packages: 'external', + write: false, + format: 'esm' + }); + return result.outputFiles[0].text; + +}; + + + +const compile = async ()=>{ + const workerRaw = await compileWorker(); + + const mainRaw = await buildMain(); + + const finalRaw = mainRaw.replaceAll("%WORKER_RAW%", encodeURIComponent(workerRaw)); + + const saveDir = path.join(__dirname, "..", "core/lib"); + + if (!fs.existsSync(saveDir)) { + fs.mkdirSync(saveDir); + } + fs.writeFileSync(path.join(saveDir, "index.js"), finalRaw, 'utf8'); +}; + +compile(); diff --git a/packages/semi-foundation/jsonViewer/variables.scss b/packages/semi-foundation/jsonViewer/variables.scss new file mode 100644 index 0000000000..9620eef398 --- /dev/null +++ b/packages/semi-foundation/jsonViewer/variables.scss @@ -0,0 +1,15 @@ +$color-json-viewer-background: var(--semi-color-default); // JSON背景颜色 +$color-json-viewer-key: rgba(var(--semi-red-5), 1); // JSON key 颜色 +$color-json-viewer-value: rgba(var(--semi-blue-5), 1); +$color-json-viewer-number: rgba(var(--semi-green-5), 1); // JSON number 颜色 +$color-json-viewer-keyword: rgba(var(--semi-blue-5), 1); // JSON keyword 颜色 +$color-json-viewer-delimiter-comma: rgba(var(--semi-blue-6), 1); // JSON delimiter comma 颜色 + + +$color-json-viewer-search-result-background: rgba(var(--semi-green-2), 1); // JSON search result background 颜色 +$color-json-viewer-current-search-result-background: rgba(var(--semi-yellow-4), 1); // JSON current search result background 颜色 + +$color-json-viewer-folding-icon: rgba(var(--semi-blue-7), 1); // JSON folding icon 颜色 + + +$color-json-viewer-line-number: rgba(var(--semi-grey-5), 1); // JSON line number 颜色 diff --git a/packages/semi-foundation/package.json b/packages/semi-foundation/package.json index 146a38c6e5..d6b3c9a340 100644 --- a/packages/semi-foundation/package.json +++ b/packages/semi-foundation/package.json @@ -7,6 +7,7 @@ "prepublishOnly": "npm run build:lib" }, "dependencies": { + "@douyinfe/semi-json-viewer-core": "2.68.4", "@douyinfe/semi-animation": "2.70.1", "@mdx-js/mdx": "^3.0.1", "async-validator": "^3.5.0", @@ -30,6 +31,7 @@ ], "gitHead": "eb34a4f25f002bb4cbcfa51f3df93bed868c831a", "devDependencies": { + "esbuild": "0.24.0", "@babel/plugin-transform-runtime": "^7.15.8", "@babel/preset-env": "^7.15.8", "@types/lodash": "^4.14.176", diff --git a/packages/semi-foundation/tsconfig.json b/packages/semi-foundation/tsconfig.json index a6eed0762d..1b66204020 100644 --- a/packages/semi-foundation/tsconfig.json +++ b/packages/semi-foundation/tsconfig.json @@ -6,7 +6,7 @@ "sourceMap": true, "allowJs": true, "module": "es6", - "lib": ["es7", "dom", "es2017"], + "lib": ["esnext", "dom"], "moduleResolution": "node", "noImplicitAny": false, "forceConsistentCasingInFileNames": true, diff --git a/packages/semi-json-viewer-core/package.json b/packages/semi-json-viewer-core/package.json new file mode 100644 index 0000000000..7bd7a92a81 --- /dev/null +++ b/packages/semi-json-viewer-core/package.json @@ -0,0 +1,55 @@ +{ + "name": "@douyinfe/semi-json-viewer-core", + "version": "2.68.4", + "description": "", + "main": "lib/index.js", + "module": "lib/index.js", + "typings": "src/index.ts", + "scripts": { + "build:lib": "node ./script/compileLib.js" + }, + "files": [ + "dist/*", + "lib/*" + ], + "dependencies": { + "jsonc-parser": "^3.3.1" + }, + "devDependencies": { + "esbuild": "^0.24.0" + }, + "sideEffects": [ + "*.scss", + "*.css", + "lib/es/index.js", + "./index.ts" + ], + "keywords": [ + "bytedance douyin design system", + "semi design to any design", + "a11y react component library", + "design to code", + "code to design", + "3000+ design token", + "dark mode", + "semi design", + "design ops", + "modern design system", + "figma ui kit" + ], + "homepage": "https://semi.design", + "bugs": { + "url": "https://github.com/DouyinFE/semi-design/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/DouyinFE/semi-design" + }, + "_unpkg": true, + "unpkgFiles": [ + "dist/css", + "dist/umd/*.js" + ], + "author": "", + "license": "MIT" +} diff --git a/packages/semi-json-viewer-core/script/compileLib.js b/packages/semi-json-viewer-core/script/compileLib.js new file mode 100644 index 0000000000..c939c8bd3b --- /dev/null +++ b/packages/semi-json-viewer-core/script/compileLib.js @@ -0,0 +1,50 @@ +const esbuild = require('esbuild'); +const path = require('path'); +const fs = require('fs'); + + +const compileWorker = async ()=>{ + const workerEntry = path.join(__dirname, "..", "src/worker/json.worker.ts"); + + const result = await esbuild.build({ + entryPoints: [workerEntry], + bundle: true, + write: false, + minify: true, + }); + return result.outputFiles[0].text; +}; + + +const buildMain = async ()=>{ + const mainEntry = path.join(__dirname, "..", "src/index.ts"); + + const result = await esbuild.build({ + entryPoints: [mainEntry], + bundle: true, + packages: 'external', + write: false, + format: 'esm' + }); + return result.outputFiles[0].text; + +}; + + + +const compile = async ()=>{ + const workerRaw = await compileWorker(); + + const mainRaw = await buildMain(); + + const finalRaw = mainRaw.replaceAll("%WORKER_RAW%", encodeURIComponent(workerRaw)); + + const saveDir = path.join(__dirname, "..", "lib"); + + if (!fs.existsSync(saveDir)) { + fs.mkdirSync(saveDir); + } + fs.writeFileSync(path.join(saveDir, "index.js"), finalRaw, 'utf8'); +}; + +compile(); diff --git a/packages/semi-json-viewer-core/src/common/async.ts b/packages/semi-json-viewer-core/src/common/async.ts new file mode 100644 index 0000000000..9bafaa2dda --- /dev/null +++ b/packages/semi-json-viewer-core/src/common/async.ts @@ -0,0 +1,15 @@ +/** Based on https://github.com/microsoft/vscode-json-languageservice with modifications for custom requirements */ +export function runWhenGlobalIdle(callback: (idleDeadline: IdleDeadline) => void) { + const handler = window.requestIdleCallback(callback); + let disposed = false; + + return { + dispose: () => { + if (disposed) { + return; + } + disposed = true; + window.cancelIdleCallback(handler); + }, + }; +} diff --git a/packages/semi-json-viewer-core/src/common/charCode.ts b/packages/semi-json-viewer-core/src/common/charCode.ts new file mode 100644 index 0000000000..f0accf6f25 --- /dev/null +++ b/packages/semi-json-viewer-core/src/common/charCode.ts @@ -0,0 +1,443 @@ +/** reference from https://github.com/microsoft/vscode */ +/** + * An inlined enum containing useful character codes (to be used with String.charCodeAt). + * Please leave the const keyword such that it gets inlined when compiled to JavaScript! + */ +export const enum CharCode { + Null = 0, + /** + * The `\b` character. + */ + Backspace = 8, + /** + * The `\t` character. + */ + Tab = 9, + /** + * The `\n` character. + */ + LineFeed = 10, + /** + * The `\r` character. + */ + CarriageReturn = 13, + Space = 32, + /** + * The `!` character. + */ + ExclamationMark = 33, + /** + * The `"` character. + */ + DoubleQuote = 34, + /** + * The `#` character. + */ + Hash = 35, + /** + * The `$` character. + */ + DollarSign = 36, + /** + * The `%` character. + */ + PercentSign = 37, + /** + * The `&` character. + */ + Ampersand = 38, + /** + * The `'` character. + */ + SingleQuote = 39, + /** + * The `(` character. + */ + OpenParen = 40, + /** + * The `)` character. + */ + CloseParen = 41, + /** + * The `*` character. + */ + Asterisk = 42, + /** + * The `+` character. + */ + Plus = 43, + /** + * The `,` character. + */ + Comma = 44, + /** + * The `-` character. + */ + Dash = 45, + /** + * The `.` character. + */ + Period = 46, + /** + * The `/` character. + */ + Slash = 47, + + Digit0 = 48, + Digit1 = 49, + Digit2 = 50, + Digit3 = 51, + Digit4 = 52, + Digit5 = 53, + Digit6 = 54, + Digit7 = 55, + Digit8 = 56, + Digit9 = 57, + + /** + * The `:` character. + */ + Colon = 58, + /** + * The `;` character. + */ + Semicolon = 59, + /** + * The `<` character. + */ + LessThan = 60, + /** + * The `=` character. + */ + Equals = 61, + /** + * The `>` character. + */ + GreaterThan = 62, + /** + * The `?` character. + */ + QuestionMark = 63, + /** + * The `@` character. + */ + AtSign = 64, + + A = 65, + B = 66, + C = 67, + D = 68, + E = 69, + F = 70, + G = 71, + H = 72, + I = 73, + J = 74, + K = 75, + L = 76, + M = 77, + N = 78, + O = 79, + P = 80, + Q = 81, + R = 82, + S = 83, + T = 84, + U = 85, + V = 86, + W = 87, + X = 88, + Y = 89, + Z = 90, + + /** + * The `[` character. + */ + OpenSquareBracket = 91, + /** + * The `\` character. + */ + Backslash = 92, + /** + * The `]` character. + */ + CloseSquareBracket = 93, + /** + * The `^` character. + */ + Caret = 94, + /** + * The `_` character. + */ + Underline = 95, + /** + * The ``(`)`` character. + */ + BackTick = 96, + + a = 97, + b = 98, + c = 99, + d = 100, + e = 101, + f = 102, + g = 103, + h = 104, + i = 105, + j = 106, + k = 107, + l = 108, + m = 109, + n = 110, + o = 111, + p = 112, + q = 113, + r = 114, + s = 115, + t = 116, + u = 117, + v = 118, + w = 119, + x = 120, + y = 121, + z = 122, + + /** + * The `{` character. + */ + OpenCurlyBrace = 123, + /** + * The `|` character. + */ + Pipe = 124, + /** + * The `}` character. + */ + CloseCurlyBrace = 125, + /** + * The `~` character. + */ + Tilde = 126, + + /** + * The   (no-break space) character. + * Unicode Character 'NO-BREAK SPACE' (U+00A0) + */ + NoBreakSpace = 160, + + U_Combining_Grave_Accent = 0x0300, // U+0300 Combining Grave Accent + U_Combining_Acute_Accent = 0x0301, // U+0301 Combining Acute Accent + U_Combining_Circumflex_Accent = 0x0302, // U+0302 Combining Circumflex Accent + U_Combining_Tilde = 0x0303, // U+0303 Combining Tilde + U_Combining_Macron = 0x0304, // U+0304 Combining Macron + U_Combining_Overline = 0x0305, // U+0305 Combining Overline + U_Combining_Breve = 0x0306, // U+0306 Combining Breve + U_Combining_Dot_Above = 0x0307, // U+0307 Combining Dot Above + U_Combining_Diaeresis = 0x0308, // U+0308 Combining Diaeresis + U_Combining_Hook_Above = 0x0309, // U+0309 Combining Hook Above + U_Combining_Ring_Above = 0x030a, // U+030A Combining Ring Above + U_Combining_Double_Acute_Accent = 0x030b, // U+030B Combining Double Acute Accent + U_Combining_Caron = 0x030c, // U+030C Combining Caron + U_Combining_Vertical_Line_Above = 0x030d, // U+030D Combining Vertical Line Above + U_Combining_Double_Vertical_Line_Above = 0x030e, // U+030E Combining Double Vertical Line Above + U_Combining_Double_Grave_Accent = 0x030f, // U+030F Combining Double Grave Accent + U_Combining_Candrabindu = 0x0310, // U+0310 Combining Candrabindu + U_Combining_Inverted_Breve = 0x0311, // U+0311 Combining Inverted Breve + U_Combining_Turned_Comma_Above = 0x0312, // U+0312 Combining Turned Comma Above + U_Combining_Comma_Above = 0x0313, // U+0313 Combining Comma Above + U_Combining_Reversed_Comma_Above = 0x0314, // U+0314 Combining Reversed Comma Above + U_Combining_Comma_Above_Right = 0x0315, // U+0315 Combining Comma Above Right + U_Combining_Grave_Accent_Below = 0x0316, // U+0316 Combining Grave Accent Below + U_Combining_Acute_Accent_Below = 0x0317, // U+0317 Combining Acute Accent Below + U_Combining_Left_Tack_Below = 0x0318, // U+0318 Combining Left Tack Below + U_Combining_Right_Tack_Below = 0x0319, // U+0319 Combining Right Tack Below + U_Combining_Left_Angle_Above = 0x031a, // U+031A Combining Left Angle Above + U_Combining_Horn = 0x031b, // U+031B Combining Horn + U_Combining_Left_Half_Ring_Below = 0x031c, // U+031C Combining Left Half Ring Below + U_Combining_Up_Tack_Below = 0x031d, // U+031D Combining Up Tack Below + U_Combining_Down_Tack_Below = 0x031e, // U+031E Combining Down Tack Below + U_Combining_Plus_Sign_Below = 0x031f, // U+031F Combining Plus Sign Below + U_Combining_Minus_Sign_Below = 0x0320, // U+0320 Combining Minus Sign Below + U_Combining_Palatalized_Hook_Below = 0x0321, // U+0321 Combining Palatalized Hook Below + U_Combining_Retroflex_Hook_Below = 0x0322, // U+0322 Combining Retroflex Hook Below + U_Combining_Dot_Below = 0x0323, // U+0323 Combining Dot Below + U_Combining_Diaeresis_Below = 0x0324, // U+0324 Combining Diaeresis Below + U_Combining_Ring_Below = 0x0325, // U+0325 Combining Ring Below + U_Combining_Comma_Below = 0x0326, // U+0326 Combining Comma Below + U_Combining_Cedilla = 0x0327, // U+0327 Combining Cedilla + U_Combining_Ogonek = 0x0328, // U+0328 Combining Ogonek + U_Combining_Vertical_Line_Below = 0x0329, // U+0329 Combining Vertical Line Below + U_Combining_Bridge_Below = 0x032a, // U+032A Combining Bridge Below + U_Combining_Inverted_Double_Arch_Below = 0x032b, // U+032B Combining Inverted Double Arch Below + U_Combining_Caron_Below = 0x032c, // U+032C Combining Caron Below + U_Combining_Circumflex_Accent_Below = 0x032d, // U+032D Combining Circumflex Accent Below + U_Combining_Breve_Below = 0x032e, // U+032E Combining Breve Below + U_Combining_Inverted_Breve_Below = 0x032f, // U+032F Combining Inverted Breve Below + U_Combining_Tilde_Below = 0x0330, // U+0330 Combining Tilde Below + U_Combining_Macron_Below = 0x0331, // U+0331 Combining Macron Below + U_Combining_Low_Line = 0x0332, // U+0332 Combining Low Line + U_Combining_Double_Low_Line = 0x0333, // U+0333 Combining Double Low Line + U_Combining_Tilde_Overlay = 0x0334, // U+0334 Combining Tilde Overlay + U_Combining_Short_Stroke_Overlay = 0x0335, // U+0335 Combining Short Stroke Overlay + U_Combining_Long_Stroke_Overlay = 0x0336, // U+0336 Combining Long Stroke Overlay + U_Combining_Short_Solidus_Overlay = 0x0337, // U+0337 Combining Short Solidus Overlay + U_Combining_Long_Solidus_Overlay = 0x0338, // U+0338 Combining Long Solidus Overlay + U_Combining_Right_Half_Ring_Below = 0x0339, // U+0339 Combining Right Half Ring Below + U_Combining_Inverted_Bridge_Below = 0x033a, // U+033A Combining Inverted Bridge Below + U_Combining_Square_Below = 0x033b, // U+033B Combining Square Below + U_Combining_Seagull_Below = 0x033c, // U+033C Combining Seagull Below + U_Combining_X_Above = 0x033d, // U+033D Combining X Above + U_Combining_Vertical_Tilde = 0x033e, // U+033E Combining Vertical Tilde + U_Combining_Double_Overline = 0x033f, // U+033F Combining Double Overline + U_Combining_Grave_Tone_Mark = 0x0340, // U+0340 Combining Grave Tone Mark + U_Combining_Acute_Tone_Mark = 0x0341, // U+0341 Combining Acute Tone Mark + U_Combining_Greek_Perispomeni = 0x0342, // U+0342 Combining Greek Perispomeni + U_Combining_Greek_Koronis = 0x0343, // U+0343 Combining Greek Koronis + U_Combining_Greek_Dialytika_Tonos = 0x0344, // U+0344 Combining Greek Dialytika Tonos + U_Combining_Greek_Ypogegrammeni = 0x0345, // U+0345 Combining Greek Ypogegrammeni + U_Combining_Bridge_Above = 0x0346, // U+0346 Combining Bridge Above + U_Combining_Equals_Sign_Below = 0x0347, // U+0347 Combining Equals Sign Below + U_Combining_Double_Vertical_Line_Below = 0x0348, // U+0348 Combining Double Vertical Line Below + U_Combining_Left_Angle_Below = 0x0349, // U+0349 Combining Left Angle Below + U_Combining_Not_Tilde_Above = 0x034a, // U+034A Combining Not Tilde Above + U_Combining_Homothetic_Above = 0x034b, // U+034B Combining Homothetic Above + U_Combining_Almost_Equal_To_Above = 0x034c, // U+034C Combining Almost Equal To Above + U_Combining_Left_Right_Arrow_Below = 0x034d, // U+034D Combining Left Right Arrow Below + U_Combining_Upwards_Arrow_Below = 0x034e, // U+034E Combining Upwards Arrow Below + U_Combining_Grapheme_Joiner = 0x034f, // U+034F Combining Grapheme Joiner + U_Combining_Right_Arrowhead_Above = 0x0350, // U+0350 Combining Right Arrowhead Above + U_Combining_Left_Half_Ring_Above = 0x0351, // U+0351 Combining Left Half Ring Above + U_Combining_Fermata = 0x0352, // U+0352 Combining Fermata + U_Combining_X_Below = 0x0353, // U+0353 Combining X Below + U_Combining_Left_Arrowhead_Below = 0x0354, // U+0354 Combining Left Arrowhead Below + U_Combining_Right_Arrowhead_Below = 0x0355, // U+0355 Combining Right Arrowhead Below + U_Combining_Right_Arrowhead_And_Up_Arrowhead_Below = 0x0356, // U+0356 Combining Right Arrowhead And Up Arrowhead Below + U_Combining_Right_Half_Ring_Above = 0x0357, // U+0357 Combining Right Half Ring Above + U_Combining_Dot_Above_Right = 0x0358, // U+0358 Combining Dot Above Right + U_Combining_Asterisk_Below = 0x0359, // U+0359 Combining Asterisk Below + U_Combining_Double_Ring_Below = 0x035a, // U+035A Combining Double Ring Below + U_Combining_Zigzag_Above = 0x035b, // U+035B Combining Zigzag Above + U_Combining_Double_Breve_Below = 0x035c, // U+035C Combining Double Breve Below + U_Combining_Double_Breve = 0x035d, // U+035D Combining Double Breve + U_Combining_Double_Macron = 0x035e, // U+035E Combining Double Macron + U_Combining_Double_Macron_Below = 0x035f, // U+035F Combining Double Macron Below + U_Combining_Double_Tilde = 0x0360, // U+0360 Combining Double Tilde + U_Combining_Double_Inverted_Breve = 0x0361, // U+0361 Combining Double Inverted Breve + U_Combining_Double_Rightwards_Arrow_Below = 0x0362, // U+0362 Combining Double Rightwards Arrow Below + U_Combining_Latin_Small_Letter_A = 0x0363, // U+0363 Combining Latin Small Letter A + U_Combining_Latin_Small_Letter_E = 0x0364, // U+0364 Combining Latin Small Letter E + U_Combining_Latin_Small_Letter_I = 0x0365, // U+0365 Combining Latin Small Letter I + U_Combining_Latin_Small_Letter_O = 0x0366, // U+0366 Combining Latin Small Letter O + U_Combining_Latin_Small_Letter_U = 0x0367, // U+0367 Combining Latin Small Letter U + U_Combining_Latin_Small_Letter_C = 0x0368, // U+0368 Combining Latin Small Letter C + U_Combining_Latin_Small_Letter_D = 0x0369, // U+0369 Combining Latin Small Letter D + U_Combining_Latin_Small_Letter_H = 0x036a, // U+036A Combining Latin Small Letter H + U_Combining_Latin_Small_Letter_M = 0x036b, // U+036B Combining Latin Small Letter M + U_Combining_Latin_Small_Letter_R = 0x036c, // U+036C Combining Latin Small Letter R + U_Combining_Latin_Small_Letter_T = 0x036d, // U+036D Combining Latin Small Letter T + U_Combining_Latin_Small_Letter_V = 0x036e, // U+036E Combining Latin Small Letter V + U_Combining_Latin_Small_Letter_X = 0x036f, // U+036F Combining Latin Small Letter X + + /** + * Unicode Character 'LINE SEPARATOR' (U+2028) + * http://www.fileformat.info/info/unicode/char/2028/index.htm + */ + LINE_SEPARATOR = 0x2028, + /** + * Unicode Character 'PARAGRAPH SEPARATOR' (U+2029) + * http://www.fileformat.info/info/unicode/char/2029/index.htm + */ + PARAGRAPH_SEPARATOR = 0x2029, + /** + * Unicode Character 'NEXT LINE' (U+0085) + * http://www.fileformat.info/info/unicode/char/0085/index.htm + */ + NEXT_LINE = 0x0085, + + // http://www.fileformat.info/info/unicode/category/Sk/list.htm + U_CIRCUMFLEX = 0x005e, // U+005E CIRCUMFLEX + U_GRAVE_ACCENT = 0x0060, // U+0060 GRAVE ACCENT + U_DIAERESIS = 0x00a8, // U+00A8 DIAERESIS + U_MACRON = 0x00af, // U+00AF MACRON + U_ACUTE_ACCENT = 0x00b4, // U+00B4 ACUTE ACCENT + U_CEDILLA = 0x00b8, // U+00B8 CEDILLA + U_MODIFIER_LETTER_LEFT_ARROWHEAD = 0x02c2, // U+02C2 MODIFIER LETTER LEFT ARROWHEAD + U_MODIFIER_LETTER_RIGHT_ARROWHEAD = 0x02c3, // U+02C3 MODIFIER LETTER RIGHT ARROWHEAD + U_MODIFIER_LETTER_UP_ARROWHEAD = 0x02c4, // U+02C4 MODIFIER LETTER UP ARROWHEAD + U_MODIFIER_LETTER_DOWN_ARROWHEAD = 0x02c5, // U+02C5 MODIFIER LETTER DOWN ARROWHEAD + U_MODIFIER_LETTER_CENTRED_RIGHT_HALF_RING = 0x02d2, // U+02D2 MODIFIER LETTER CENTRED RIGHT HALF RING + U_MODIFIER_LETTER_CENTRED_LEFT_HALF_RING = 0x02d3, // U+02D3 MODIFIER LETTER CENTRED LEFT HALF RING + U_MODIFIER_LETTER_UP_TACK = 0x02d4, // U+02D4 MODIFIER LETTER UP TACK + U_MODIFIER_LETTER_DOWN_TACK = 0x02d5, // U+02D5 MODIFIER LETTER DOWN TACK + U_MODIFIER_LETTER_PLUS_SIGN = 0x02d6, // U+02D6 MODIFIER LETTER PLUS SIGN + U_MODIFIER_LETTER_MINUS_SIGN = 0x02d7, // U+02D7 MODIFIER LETTER MINUS SIGN + U_BREVE = 0x02d8, // U+02D8 BREVE + U_DOT_ABOVE = 0x02d9, // U+02D9 DOT ABOVE + U_RING_ABOVE = 0x02da, // U+02DA RING ABOVE + U_OGONEK = 0x02db, // U+02DB OGONEK + U_SMALL_TILDE = 0x02dc, // U+02DC SMALL TILDE + U_DOUBLE_ACUTE_ACCENT = 0x02dd, // U+02DD DOUBLE ACUTE ACCENT + U_MODIFIER_LETTER_RHOTIC_HOOK = 0x02de, // U+02DE MODIFIER LETTER RHOTIC HOOK + U_MODIFIER_LETTER_CROSS_ACCENT = 0x02df, // U+02DF MODIFIER LETTER CROSS ACCENT + U_MODIFIER_LETTER_EXTRA_HIGH_TONE_BAR = 0x02e5, // U+02E5 MODIFIER LETTER EXTRA-HIGH TONE BAR + U_MODIFIER_LETTER_HIGH_TONE_BAR = 0x02e6, // U+02E6 MODIFIER LETTER HIGH TONE BAR + U_MODIFIER_LETTER_MID_TONE_BAR = 0x02e7, // U+02E7 MODIFIER LETTER MID TONE BAR + U_MODIFIER_LETTER_LOW_TONE_BAR = 0x02e8, // U+02E8 MODIFIER LETTER LOW TONE BAR + U_MODIFIER_LETTER_EXTRA_LOW_TONE_BAR = 0x02e9, // U+02E9 MODIFIER LETTER EXTRA-LOW TONE BAR + U_MODIFIER_LETTER_YIN_DEPARTING_TONE_MARK = 0x02ea, // U+02EA MODIFIER LETTER YIN DEPARTING TONE MARK + U_MODIFIER_LETTER_YANG_DEPARTING_TONE_MARK = 0x02eb, // U+02EB MODIFIER LETTER YANG DEPARTING TONE MARK + U_MODIFIER_LETTER_UNASPIRATED = 0x02ed, // U+02ED MODIFIER LETTER UNASPIRATED + U_MODIFIER_LETTER_LOW_DOWN_ARROWHEAD = 0x02ef, // U+02EF MODIFIER LETTER LOW DOWN ARROWHEAD + U_MODIFIER_LETTER_LOW_UP_ARROWHEAD = 0x02f0, // U+02F0 MODIFIER LETTER LOW UP ARROWHEAD + U_MODIFIER_LETTER_LOW_LEFT_ARROWHEAD = 0x02f1, // U+02F1 MODIFIER LETTER LOW LEFT ARROWHEAD + U_MODIFIER_LETTER_LOW_RIGHT_ARROWHEAD = 0x02f2, // U+02F2 MODIFIER LETTER LOW RIGHT ARROWHEAD + U_MODIFIER_LETTER_LOW_RING = 0x02f3, // U+02F3 MODIFIER LETTER LOW RING + U_MODIFIER_LETTER_MIDDLE_GRAVE_ACCENT = 0x02f4, // U+02F4 MODIFIER LETTER MIDDLE GRAVE ACCENT + U_MODIFIER_LETTER_MIDDLE_DOUBLE_GRAVE_ACCENT = 0x02f5, // U+02F5 MODIFIER LETTER MIDDLE DOUBLE GRAVE ACCENT + U_MODIFIER_LETTER_MIDDLE_DOUBLE_ACUTE_ACCENT = 0x02f6, // U+02F6 MODIFIER LETTER MIDDLE DOUBLE ACUTE ACCENT + U_MODIFIER_LETTER_LOW_TILDE = 0x02f7, // U+02F7 MODIFIER LETTER LOW TILDE + U_MODIFIER_LETTER_RAISED_COLON = 0x02f8, // U+02F8 MODIFIER LETTER RAISED COLON + U_MODIFIER_LETTER_BEGIN_HIGH_TONE = 0x02f9, // U+02F9 MODIFIER LETTER BEGIN HIGH TONE + U_MODIFIER_LETTER_END_HIGH_TONE = 0x02fa, // U+02FA MODIFIER LETTER END HIGH TONE + U_MODIFIER_LETTER_BEGIN_LOW_TONE = 0x02fb, // U+02FB MODIFIER LETTER BEGIN LOW TONE + U_MODIFIER_LETTER_END_LOW_TONE = 0x02fc, // U+02FC MODIFIER LETTER END LOW TONE + U_MODIFIER_LETTER_SHELF = 0x02fd, // U+02FD MODIFIER LETTER SHELF + U_MODIFIER_LETTER_OPEN_SHELF = 0x02fe, // U+02FE MODIFIER LETTER OPEN SHELF + U_MODIFIER_LETTER_LOW_LEFT_ARROW = 0x02ff, // U+02FF MODIFIER LETTER LOW LEFT ARROW + U_GREEK_LOWER_NUMERAL_SIGN = 0x0375, // U+0375 GREEK LOWER NUMERAL SIGN + U_GREEK_TONOS = 0x0384, // U+0384 GREEK TONOS + U_GREEK_DIALYTIKA_TONOS = 0x0385, // U+0385 GREEK DIALYTIKA TONOS + U_GREEK_KORONIS = 0x1fbd, // U+1FBD GREEK KORONIS + U_GREEK_PSILI = 0x1fbf, // U+1FBF GREEK PSILI + U_GREEK_PERISPOMENI = 0x1fc0, // U+1FC0 GREEK PERISPOMENI + U_GREEK_DIALYTIKA_AND_PERISPOMENI = 0x1fc1, // U+1FC1 GREEK DIALYTIKA AND PERISPOMENI + U_GREEK_PSILI_AND_VARIA = 0x1fcd, // U+1FCD GREEK PSILI AND VARIA + U_GREEK_PSILI_AND_OXIA = 0x1fce, // U+1FCE GREEK PSILI AND OXIA + U_GREEK_PSILI_AND_PERISPOMENI = 0x1fcf, // U+1FCF GREEK PSILI AND PERISPOMENI + U_GREEK_DASIA_AND_VARIA = 0x1fdd, // U+1FDD GREEK DASIA AND VARIA + U_GREEK_DASIA_AND_OXIA = 0x1fde, // U+1FDE GREEK DASIA AND OXIA + U_GREEK_DASIA_AND_PERISPOMENI = 0x1fdf, // U+1FDF GREEK DASIA AND PERISPOMENI + U_GREEK_DIALYTIKA_AND_VARIA = 0x1fed, // U+1FED GREEK DIALYTIKA AND VARIA + U_GREEK_DIALYTIKA_AND_OXIA = 0x1fee, // U+1FEE GREEK DIALYTIKA AND OXIA + U_GREEK_VARIA = 0x1fef, // U+1FEF GREEK VARIA + U_GREEK_OXIA = 0x1ffd, // U+1FFD GREEK OXIA + U_GREEK_DASIA = 0x1ffe, // U+1FFE GREEK DASIA + + U_IDEOGRAPHIC_FULL_STOP = 0x3002, // U+3002 IDEOGRAPHIC FULL STOP + U_LEFT_CORNER_BRACKET = 0x300c, // U+300C LEFT CORNER BRACKET + U_RIGHT_CORNER_BRACKET = 0x300d, // U+300D RIGHT CORNER BRACKET + U_LEFT_BLACK_LENTICULAR_BRACKET = 0x3010, // U+3010 LEFT BLACK LENTICULAR BRACKET + U_RIGHT_BLACK_LENTICULAR_BRACKET = 0x3011, // U+3011 RIGHT BLACK LENTICULAR BRACKET + + U_OVERLINE = 0x203e, // Unicode Character 'OVERLINE' + + /** + * UTF-8 BOM + * Unicode Character 'ZERO WIDTH NO-BREAK SPACE' (U+FEFF) + * http://www.fileformat.info/info/unicode/char/feff/index.htm + */ + UTF8_BOM = 65279, + + U_FULLWIDTH_SEMICOLON = 0xff1b, // U+FF1B FULLWIDTH SEMICOLON + U_FULLWIDTH_COMMA = 0xff0c, // U+FF0C FULLWIDTH COMMA +} diff --git a/packages/semi-json-viewer-core/src/common/characterClassifier.ts b/packages/semi-json-viewer-core/src/common/characterClassifier.ts new file mode 100644 index 0000000000..eb24be16e7 --- /dev/null +++ b/packages/semi-json-viewer-core/src/common/characterClassifier.ts @@ -0,0 +1,81 @@ +/** reference from https://github.com/microsoft/vscode */ +import { toUint8 } from './uint'; + +/** + * A fast character classifier that uses a compact array for ASCII values. + */ +export class CharacterClassifier { + /** + * Maintain a compact (fully initialized ASCII map for quickly classifying ASCII characters - used more often in code). + */ + protected readonly _asciiMap: Uint8Array; + + /** + * The entire map (sparse array). + */ + protected readonly _map: Map; + + protected readonly _defaultValue: number; + + constructor(_defaultValue: T) { + const defaultValue = toUint8(_defaultValue); + + this._defaultValue = defaultValue; + this._asciiMap = CharacterClassifier._createAsciiMap(defaultValue); + this._map = new Map(); + } + + private static _createAsciiMap(defaultValue: number): Uint8Array { + const asciiMap = new Uint8Array(256); + asciiMap.fill(defaultValue); + return asciiMap; + } + + public set(charCode: number, _value: T): void { + const value = toUint8(_value); + + if (charCode >= 0 && charCode < 256) { + this._asciiMap[charCode] = value; + } else { + this._map.set(charCode, value); + } + } + + public get(charCode: number): T { + if (charCode >= 0 && charCode < 256) { + return this._asciiMap[charCode]; + } else { + return (this._map.get(charCode) || this._defaultValue); + } + } + + public clear() { + this._asciiMap.fill(this._defaultValue); + this._map.clear(); + } +} + +const enum BooleanEnum { + False = 0, + True = 1, +} + +export class CharacterSet { + private readonly _actual: CharacterClassifier; + + constructor() { + this._actual = new CharacterClassifier(BooleanEnum.False); + } + + public add(charCode: number): void { + this._actual.set(charCode, BooleanEnum.True); + } + + public has(charCode: number): boolean { + return this._actual.get(charCode) === BooleanEnum.True; + } + + public clear(): void { + return this._actual.clear(); + } +} diff --git a/packages/semi-json-viewer-core/src/common/dom.ts b/packages/semi-json-viewer-core/src/common/dom.ts new file mode 100644 index 0000000000..dcafa0d758 --- /dev/null +++ b/packages/semi-json-viewer-core/src/common/dom.ts @@ -0,0 +1,34 @@ +/** + * create element + * @param tag tagName + * @param className className + * @returns element + */ +export function elt(tag: string, className: string, style?: { [key: string]: string }): HTMLElement { + const el = document.createElement(tag); + el.className = className; + if (style) { + setStyles(el, style); + } + return el; +} + +/** + * set styles + * @param element element + * @param styles styles + */ +export function setStyles(element: HTMLElement, styles: { [key: string]: string }) { + for (const [key, value] of Object.entries(styles)) { + element.style[key as any] = value; + } +} + +/** + * get line element by child node + * @param node node + * @returns line element + */ +export function getLineElement(node: Node): HTMLElement | null { + return node.parentElement?.closest('[data-line-element="true"]') || null; +} diff --git a/packages/semi-json-viewer-core/src/common/emitter.ts b/packages/semi-json-viewer-core/src/common/emitter.ts new file mode 100644 index 0000000000..0194c7fe32 --- /dev/null +++ b/packages/semi-json-viewer-core/src/common/emitter.ts @@ -0,0 +1,62 @@ +import { GlobalEvents } from './emitterEvents'; +import { getCurrentNameSpaceId } from './nameSpace'; + +type EventHandler = (event: T) => void; + +const emitterMap = new Map>(); + +export class Emitter> { + public listeners: { [K in keyof Events]?: EventHandler[] } = {}; + + constructor() {} + + public on(event: K, listener: EventHandler): void { + if (!this.listeners[event]) { + this.listeners[event] = []; + } + this.listeners[event]?.push(listener); + } + + public off(event: K, listener: EventHandler): void { + if (!this.listeners[event]) return; + + this.listeners[event] = this.listeners[event]?.filter(l => l !== listener); + } + + public dispose() { + this.listeners = {}; + } + + public removeAllListeners() { + this.listeners = {}; + } + + public emit(event: K, data: Events[K]): void { + if (!this.listeners[event]) return; + + for (const listener of this.listeners[event]!) { + listener(data); + } + } +} + +export const getEmitter = () => { + const currentNameSpaceId = getCurrentNameSpaceId(); + if (!currentNameSpaceId) { + throw new Error('currentNameSpaceId is not set'); + } + let emitter = emitterMap.get(currentNameSpaceId); + if (!emitter) { + emitter = new Emitter(); + emitterMap.set(currentNameSpaceId, emitter); + } + return emitter; +}; + +export const disposeEmitter = (id: string) => { + const emitter = emitterMap.get(id); + if (emitter) { + emitter.dispose(); + emitterMap.delete(id); + } +}; diff --git a/packages/semi-json-viewer-core/src/common/emitterEvents.ts b/packages/semi-json-viewer-core/src/common/emitterEvents.ts new file mode 100644 index 0000000000..87b197b0ca --- /dev/null +++ b/packages/semi-json-viewer-core/src/common/emitterEvents.ts @@ -0,0 +1,51 @@ +import { JsonDocument } from '../service/parse'; +import { Diagnostic } from '../service/jsonTypes'; + +export interface GlobalEvents { + tokensChanged: IModelTokensChangedEvent; + contentChanged: IModelContentChangeEvent | IModelContentChangeEvent[]; + problemsChanged: IProblemsChangedEvent; + hoverNode: IHoverNodeEvent; + renderHoverNode: IRenderHoverNodeEvent +} + +interface IRange { + startLineNumber: number; + + startColumn: number; + + endLineNumber: number; + + endColumn: number +} + +export interface IModelTokensChangedEvent { + range: { + from: number; + to: number + } +} + +export interface IModelContentChangeEvent { + type: 'insert' | 'delete' | 'replace'; + range: IRange; + rangeOffset: number; + rangeLength: number; + oldText: string; + newText: string; + keepPosition?: { lineNumber: number; column: number } +} + +export interface IProblemsChangedEvent { + root: JsonDocument; + problems: Diagnostic[] +} + +export interface IRenderHoverNodeEvent { + el: HTMLElement +} + +export interface IHoverNodeEvent { + value: string; + target: HTMLElement +} diff --git a/packages/semi-json-viewer-core/src/common/map.ts b/packages/semi-json-viewer-core/src/common/map.ts new file mode 100644 index 0000000000..30a7979c7c --- /dev/null +++ b/packages/semi-json-viewer-core/src/common/map.ts @@ -0,0 +1,480 @@ +/** reference from https://github.com/microsoft/vscode */ +interface Item { + previous: Item | undefined; + next: Item | undefined; + key: K; + value: V +} + +export const enum Touch { + None = 0, + AsOld = 1, + AsNew = 2 +} + +export class LinkedMap implements Map { + readonly [Symbol.toStringTag] = 'LinkedMap'; + + private _map: Map>; + private _head: Item | undefined; + private _tail: Item | undefined; + private _size: number; + + private _state: number; + + constructor() { + this._map = new Map>(); + this._head = undefined; + this._tail = undefined; + this._size = 0; + this._state = 0; + } + + clear(): void { + this._map.clear(); + this._head = undefined; + this._tail = undefined; + this._size = 0; + this._state++; + } + + isEmpty(): boolean { + return !this._head && !this._tail; + } + + get size(): number { + return this._size; + } + + get first(): V | undefined { + return this._head?.value; + } + + get last(): V | undefined { + return this._tail?.value; + } + + has(key: K): boolean { + return this._map.has(key); + } + + get(key: K, touch: Touch = Touch.None): V | undefined { + const item = this._map.get(key); + if (!item) { + return undefined; + } + if (touch !== Touch.None) { + this.touch(item, touch); + } + return item.value; + } + + set(key: K, value: V, touch: Touch = Touch.None): this { + let item = this._map.get(key); + if (item) { + item.value = value; + if (touch !== Touch.None) { + this.touch(item, touch); + } + } else { + item = { key, value, next: undefined, previous: undefined }; + switch (touch) { + case Touch.None: + this.addItemLast(item); + break; + case Touch.AsOld: + this.addItemFirst(item); + break; + case Touch.AsNew: + this.addItemLast(item); + break; + default: + this.addItemLast(item); + break; + } + this._map.set(key, item); + this._size++; + } + return this; + } + + delete(key: K): boolean { + return !!this.remove(key); + } + + remove(key: K): V | undefined { + const item = this._map.get(key); + if (!item) { + return undefined; + } + this._map.delete(key); + this.removeItem(item); + this._size--; + return item.value; + } + + shift(): V | undefined { + if (!this._head && !this._tail) { + return undefined; + } + if (!this._head || !this._tail) { + throw new Error('Invalid list'); + } + const item = this._head; + this._map.delete(item.key); + this.removeItem(item); + this._size--; + return item.value; + } + + forEach( + callbackfn: (value: V, key: K, map: LinkedMap) => void, + thisArg?: any + ): void { + const state = this._state; + let current = this._head; + while (current) { + if (thisArg) { + callbackfn.bind(thisArg)(current.value, current.key, this); + } else { + callbackfn(current.value, current.key, this); + } + if (this._state !== state) { + throw new Error(`LinkedMap got modified during iteration.`); + } + current = current.next; + } + } + + keys(): IterableIterator { + const map = this; + const state = this._state; + let current = this._head; + const iterator: IterableIterator = { + [Symbol.iterator]() { + return iterator; + }, + next(): IteratorResult { + if (map._state !== state) { + throw new Error(`LinkedMap got modified during iteration.`); + } + if (current) { + const result = { value: current.key, done: false }; + current = current.next; + return result; + } else { + return { value: undefined, done: true }; + } + } + }; + return iterator; + } + + values(): IterableIterator { + const map = this; + const state = this._state; + let current = this._head; + const iterator: IterableIterator = { + [Symbol.iterator]() { + return iterator; + }, + next(): IteratorResult { + if (map._state !== state) { + throw new Error(`LinkedMap got modified during iteration.`); + } + if (current) { + const result = { value: current.value, done: false }; + current = current.next; + return result; + } else { + return { value: undefined, done: true }; + } + } + }; + return iterator; + } + + entries(): IterableIterator<[K, V]> { + const map = this; + const state = this._state; + let current = this._head; + const iterator: IterableIterator<[K, V]> = { + [Symbol.iterator]() { + return iterator; + }, + next(): IteratorResult<[K, V]> { + if (map._state !== state) { + throw new Error(`LinkedMap got modified during iteration.`); + } + if (current) { + const result: IteratorResult<[K, V]> = { + value: [current.key, current.value], + done: false + }; + current = current.next; + return result; + } else { + return { value: undefined, done: true }; + } + } + }; + return iterator; + } + + [Symbol.iterator](): IterableIterator<[K, V]> { + return this.entries(); + } + + protected trimOld(newSize: number) { + if (newSize >= this.size) { + return; + } + if (newSize === 0) { + this.clear(); + return; + } + let current = this._head; + let currentSize = this.size; + while (current && currentSize > newSize) { + this._map.delete(current.key); + current = current.next; + currentSize--; + } + this._head = current; + this._size = currentSize; + if (current) { + current.previous = undefined; + } + this._state++; + } + + protected trimNew(newSize: number) { + if (newSize >= this.size) { + return; + } + if (newSize === 0) { + this.clear(); + return; + } + let current = this._tail; + let currentSize = this.size; + while (current && currentSize > newSize) { + this._map.delete(current.key); + current = current.previous; + currentSize--; + } + this._tail = current; + this._size = currentSize; + if (current) { + current.next = undefined; + } + this._state++; + } + + private addItemFirst(item: Item): void { + // First time Insert + if (!this._head && !this._tail) { + this._tail = item; + } else if (!this._head) { + throw new Error('Invalid list'); + } else { + item.next = this._head; + this._head.previous = item; + } + this._head = item; + this._state++; + } + + private addItemLast(item: Item): void { + // First time Insert + if (!this._head && !this._tail) { + this._head = item; + } else if (!this._tail) { + throw new Error('Invalid list'); + } else { + item.previous = this._tail; + this._tail.next = item; + } + this._tail = item; + this._state++; + } + + private removeItem(item: Item): void { + if (item === this._head && item === this._tail) { + this._head = undefined; + this._tail = undefined; + } else if (item === this._head) { + // This can only happen if size === 1 which is handled + // by the case above. + if (!item.next) { + throw new Error('Invalid list'); + } + item.next.previous = undefined; + this._head = item.next; + } else if (item === this._tail) { + // This can only happen if size === 1 which is handled + // by the case above. + if (!item.previous) { + throw new Error('Invalid list'); + } + item.previous.next = undefined; + this._tail = item.previous; + } else { + const next = item.next; + const previous = item.previous; + if (!next || !previous) { + throw new Error('Invalid list'); + } + next.previous = previous; + previous.next = next; + } + item.next = undefined; + item.previous = undefined; + this._state++; + } + + private touch(item: Item, touch: Touch): void { + if (!this._head || !this._tail) { + throw new Error('Invalid list'); + } + if (touch !== Touch.AsOld && touch !== Touch.AsNew) { + return; + } + + if (touch === Touch.AsOld) { + if (item === this._head) { + return; + } + + const next = item.next; + const previous = item.previous; + + // Unlink the item + if (item === this._tail) { + // previous must be defined since item was not head but is tail + // So there are more than on item in the map + previous!.next = undefined; + this._tail = previous; + } else { + // Both next and previous are not undefined since item was neither head nor tail. + next!.previous = previous; + previous!.next = next; + } + + // Insert the node at head + item.previous = undefined; + item.next = this._head; + this._head.previous = item; + this._head = item; + this._state++; + } else if (touch === Touch.AsNew) { + if (item === this._tail) { + return; + } + + const next = item.next; + const previous = item.previous; + + // Unlink the item. + if (item === this._head) { + // next must be defined since item was not tail but is head + // So there are more than on item in the map + next!.previous = undefined; + this._head = next; + } else { + // Both next and previous are not undefined since item was neither head nor tail. + next!.previous = previous; + previous!.next = next; + } + item.next = undefined; + item.previous = this._tail; + this._tail.next = item; + this._tail = item; + this._state++; + } + } + + toJSON(): [K, V][] { + const data: [K, V][] = []; + + this.forEach((value, key) => { + data.push([key, value]); + }); + + return data; + } + + fromJSON(data: [K, V][]): void { + this.clear(); + + for (const [key, value] of data) { + this.set(key, value); + } + } +} + +abstract class Cache extends LinkedMap { + protected _limit: number; + protected _ratio: number; + + constructor(limit: number, ratio: number = 1) { + super(); + this._limit = limit; + this._ratio = Math.min(Math.max(0, ratio), 1); + } + + get limit(): number { + return this._limit; + } + + set limit(limit: number) { + this._limit = limit; + this.checkTrim(); + } + + get ratio(): number { + return this._ratio; + } + + set ratio(ratio: number) { + this._ratio = Math.min(Math.max(0, ratio), 1); + this.checkTrim(); + } + + override get(key: K, touch: Touch = Touch.AsNew): V | undefined { + return super.get(key, touch); + } + + peek(key: K): V | undefined { + return super.get(key, Touch.None); + } + + override set(key: K, value: V): this { + super.set(key, value, Touch.AsNew); + return this; + } + + protected checkTrim() { + if (this.size > this._limit) { + this.trim(Math.round(this._limit * this._ratio)); + } + } + + protected abstract trim(newSize: number): void; +} + +export class LRUCache extends Cache { + constructor(limit: number, ratio: number = 1) { + super(limit, ratio); + } + + protected override trim(newSize: number) { + this.trimOld(newSize); + } + + override set(key: K, value: V): this { + super.set(key, value); + this.checkTrim(); + return this; + } +} diff --git a/packages/semi-json-viewer-core/src/common/model.ts b/packages/semi-json-viewer-core/src/common/model.ts new file mode 100644 index 0000000000..f20e375aff --- /dev/null +++ b/packages/semi-json-viewer-core/src/common/model.ts @@ -0,0 +1,64 @@ +/** reference from https://github.com/microsoft/vscode */ +import { WordCharacterClassifier } from './wordCharacterClassifier'; +import { Range } from './range'; +export const enum EndOfLinePreference { + /** + * Use the end of line character identified in the text buffer. + */ + TextDefined = 0, + /** + * Use line feed (\n) as the end of line character. + */ + LF = 1, + /** + * Use carriage return and line feed (\r\n) as the end of line character. + */ + CRLF = 2, +} + +export class FindMatch { + _findMatchBrand: void = undefined; + + public readonly range: Range; + public readonly matches: string[] | null; + + /** + * @internal + */ + constructor(range: Range, matches: string[] | null) { + this.range = range; + this.matches = matches; + } +} +/** + * Text snapshot that works like an iterator. + * Will try to return chunks of roughly ~64KB size. + * Will return null when finished. + */ +export interface ITextSnapshot { + read(): string | null +} + +/** + * @internal + */ +export class SearchData { + /** + * The regex to search for. Always defined. + */ + public readonly regex: RegExp; + /** + * The word separator classifier. + */ + public readonly wordSeparators: WordCharacterClassifier | null; + /** + * The simple string to search for (if possible). + */ + public readonly simpleSearch: string | null; + + constructor(regex: RegExp, wordSeparators: WordCharacterClassifier | null, simpleSearch: string | null) { + this.regex = regex; + this.wordSeparators = wordSeparators; + this.simpleSearch = simpleSearch; + } +} diff --git a/packages/semi-json-viewer-core/src/common/nameSpace.ts b/packages/semi-json-viewer-core/src/common/nameSpace.ts new file mode 100644 index 0000000000..785643ef19 --- /dev/null +++ b/packages/semi-json-viewer-core/src/common/nameSpace.ts @@ -0,0 +1,9 @@ +let currentNameSpaceId: string = 'default'; + +export function setCurrentNameSpaceId(id: string) { + currentNameSpaceId = id; +} + +export function getCurrentNameSpaceId() { + return currentNameSpaceId; +} diff --git a/packages/semi-json-viewer-core/src/common/position.ts b/packages/semi-json-viewer-core/src/common/position.ts new file mode 100644 index 0000000000..3cb85a5006 --- /dev/null +++ b/packages/semi-json-viewer-core/src/common/position.ts @@ -0,0 +1,35 @@ +/** based on https://github.com/microsoft/vscode with modifications for custom requirements */ + +/** + * A position in the editor. This interface is suitable for serialization. + */ +export interface IPosition { + /** + * line number (starts at 1) + */ + readonly lineNumber: number; + /** + * column (the first character in a line is between column 1 and column 2) + */ + readonly column: number +} + +/** + * A position in the editor. + */ +export class Position { + /** + * line number (starts at 1) + */ + public readonly lineNumber: number; + /** + * column (the first character in a line is between column 1 and column 2) + */ + public readonly column: number; + + constructor(lineNumber: number, column: number) { + this.lineNumber = lineNumber; + this.column = column; + } + +} diff --git a/packages/semi-json-viewer-core/src/common/range.ts b/packages/semi-json-viewer-core/src/common/range.ts new file mode 100644 index 0000000000..0318f29524 --- /dev/null +++ b/packages/semi-json-viewer-core/src/common/range.ts @@ -0,0 +1,146 @@ +/** based on https://github.com/microsoft/vscode with modifications for custom requirements */ + +import { IPosition, Position } from './position'; + +/** + * A range in the editor. This interface is suitable for serialization. + */ +export interface IRange { + /** + * Line number on which the range starts (starts at 1). + */ + readonly startLineNumber: number; + /** + * Column on which the range starts in line `startLineNumber` (starts at 1). + */ + readonly startColumn: number; + /** + * Line number on which the range ends. + */ + readonly endLineNumber: number; + /** + * Column on which the range ends in line `endLineNumber`. + */ + readonly endColumn: number +} + +/** + * A range in the editor. (startLineNumber,startColumn) is <= (endLineNumber,endColumn) + */ +export class Range { + /** + * Line number on which the range starts (starts at 1). + */ + public readonly startLineNumber: number; + /** + * Column on which the range starts in line `startLineNumber` (starts at 1). + */ + public readonly startColumn: number; + /** + * Line number on which the range ends. + */ + public readonly endLineNumber: number; + /** + * Column on which the range ends in line `endLineNumber`. + */ + public readonly endColumn: number; + + constructor(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number) { + if (startLineNumber > endLineNumber || (startLineNumber === endLineNumber && startColumn > endColumn)) { + this.startLineNumber = endLineNumber; + this.startColumn = endColumn; + this.endLineNumber = startLineNumber; + this.endColumn = startColumn; + } else { + this.startLineNumber = startLineNumber; + this.startColumn = startColumn; + this.endLineNumber = endLineNumber; + this.endColumn = endColumn; + } + } + + static create(start: IPosition, end: IPosition): Range { + return new Range(start.lineNumber, start.column, end.lineNumber, end.column); + } + + /** + * Test if the two ranges are intersecting. If the ranges are touching it returns true. + */ + public static areIntersecting(a: IRange, b: IRange): boolean { + // Check if `a` is before `b` + if ( + a.endLineNumber < b.startLineNumber || + (a.endLineNumber === b.startLineNumber && a.endColumn <= b.startColumn) + ) { + return false; + } + + // Check if `b` is before `a` + if ( + b.endLineNumber < a.startLineNumber || + (b.endLineNumber === a.startLineNumber && b.endColumn <= a.startColumn) + ) { + return false; + } + + // These ranges must intersect + return true; + } + + /** + * A reunion of the two ranges. + * The smallest position will be used as the start point, and the largest one as the end point. + */ + public plusRange(range: IRange): Range { + return Range.plusRange(this, range); + } + + /** + * A reunion of the two ranges. + * The smallest position will be used as the start point, and the largest one as the end point. + */ + public static plusRange(a: IRange, b: IRange): Range { + let startLineNumber: number; + let startColumn: number; + let endLineNumber: number; + let endColumn: number; + + if (b.startLineNumber < a.startLineNumber) { + startLineNumber = b.startLineNumber; + startColumn = b.startColumn; + } else if (b.startLineNumber === a.startLineNumber) { + startLineNumber = b.startLineNumber; + startColumn = Math.min(b.startColumn, a.startColumn); + } else { + startLineNumber = a.startLineNumber; + startColumn = a.startColumn; + } + + if (b.endLineNumber > a.endLineNumber) { + endLineNumber = b.endLineNumber; + endColumn = b.endColumn; + } else if (b.endLineNumber === a.endLineNumber) { + endLineNumber = b.endLineNumber; + endColumn = Math.max(b.endColumn, a.endColumn); + } else { + endLineNumber = a.endLineNumber; + endColumn = a.endColumn; + } + + return new Range(startLineNumber, startColumn, endLineNumber, endColumn); + } + + /** + * Return the start position (which will be before or equal to the end position) + */ + public getStartPosition(): Position { + return Range.getStartPosition(this); + } + + /** + * Return the start position (which will be before or equal to the end position) + */ + public static getStartPosition(range: IRange): Position { + return new Position(range.startLineNumber, range.startColumn); + } +} diff --git a/packages/semi-json-viewer-core/src/common/stopWatch.ts b/packages/semi-json-viewer-core/src/common/stopWatch.ts new file mode 100644 index 0000000000..60a3ed2860 --- /dev/null +++ b/packages/semi-json-viewer-core/src/common/stopWatch.ts @@ -0,0 +1,41 @@ +/** reference from https://github.com/microsoft/vscode */ +// fake definition so that the valid layers check won't trip on this +declare const globalThis: { performance?: { now(): number } }; + +const hasPerformanceNow = globalThis.performance && typeof globalThis.performance.now === 'function'; + +export class StopWatch { + private _startTime: number; + private _stopTime: number; + + private readonly _now: () => number; + + public static create(highResolution?: boolean): StopWatch { + return new StopWatch(highResolution); + } + + constructor(highResolution?: boolean) { + this._now = + hasPerformanceNow && highResolution === false + ? Date.now + : globalThis.performance?.now.bind(globalThis.performance); + this._startTime = this._now(); + this._stopTime = -1; + } + + public stop(): void { + this._stopTime = this._now(); + } + + public reset(): void { + this._startTime = this._now(); + this._stopTime = -1; + } + + public elapsed(): number { + if (this._stopTime !== -1) { + return this._stopTime - this._startTime; + } + return this._now() - this._startTime; + } +} diff --git a/packages/semi-json-viewer-core/src/common/strings.ts b/packages/semi-json-viewer-core/src/common/strings.ts new file mode 100644 index 0000000000..77c375a8be --- /dev/null +++ b/packages/semi-json-viewer-core/src/common/strings.ts @@ -0,0 +1,140 @@ +/** reference from https://github.com/microsoft/vscode */ + +import { CharCode } from './charCode'; + +/** + * Escapes regular expression characters in a given string + * 转义正则表达式中的特殊字符。它将输入字符串中的正则表达式特殊字符(如 \ { } * + ? | ^ $ . [ ] ( ))前面加上反斜杠, + * 以确保这些字符被视为普通字符而不是正则表达式的元字符 + */ +export function escapeRegExpCharacters(value: string): string { + // eslint-disable-next-line no-useless-escape + return value.replace(/[\\\{\}\*\+\?\|\^\$\.\[\]\(\)]/g, '\\$&'); +} + +/** + * 检查给定的字符码是否表示一个高代理项(high surrogate)。 + * 高代理项是用于表示Unicode扩展字符的一部分,通常与低代理项一起使用。 + * 在JavaScript中,字符码可以通过 `str.charCodeAt(index)` 方法获取。 + * + * @param {number} charCode - 要检查的字符码。 + * @returns {boolean} - 如果字符码表示一个高代理项,则返回 true,否则返回 false。 + */ +export function isHighSurrogate(charCode: number): boolean { + return 0xd800 <= charCode && charCode <= 0xdbff; +} + +/** + * 检查给定的字符码是否表示一个低代理项(low surrogate)。 + * 低代理项是用于表示Unicode扩展字符的一部分,通常与高代理项一起使用。 + * 在JavaScript中,字符码可以通过 `str.charCodeAt(index)` 方法获取。 + * + * @param {number} charCode - 要检查的字符码。 + * @returns {boolean} - 如果字符码表示一个低代理项,则返回 true,否则返回 false。 + */ +export function isLowSurrogate(charCode: number): boolean { + return 0xdc00 <= charCode && charCode <= 0xdfff; +} + +/** + * 计算一个Unicode代码点(code point),它由一个高代理项(high surrogate)和一个低代理项(low surrogate)组成。 + * 在JavaScript中,Unicode代码点可以通过 `str.codePointAt(index)` 方法获取。 + * + * @param {number} highSurrogate - 高代理项的字符码。 + * @param {number} lowSurrogate - 低代理项的字符码。 + * @returns {number} - 计算得到的Unicode代码点。 + */ +export function computeCodePoint(highSurrogate: number, lowSurrogate: number): number { + return ((highSurrogate - 0xd800) << 10) + (lowSurrogate - 0xdc00) + 0x10000; +} + +/** + * 获取一个字符串中下一个Unicode代码点(code point)。 + * 在JavaScript中,Unicode代码点可以通过 `str.codePointAt(index)` 方法获取。 + * + * @param {string} str - 要检查的字符串。 + * @param {number} len - 字符串的长度。 + * @param {number} offset - 当前检查的索引位置。 + * @returns {number} - 下一个Unicode代码点。 + */ +export function getNextCodePoint(str: string, len: number, offset: number): number { + const charCode = str.charCodeAt(offset); + if (isHighSurrogate(charCode) && offset + 1 < len) { + const nextCharCode = str.charCodeAt(offset + 1); + if (isLowSurrogate(nextCharCode)) { + return computeCodePoint(charCode, nextCharCode); + } + } + return charCode; +} + +/** + * 表示正则表达式选项的接口。 + */ +export interface RegExpOptions { + matchCase?: boolean; + wholeWord?: boolean; + multiline?: boolean; + global?: boolean; + unicode?: boolean +} + +/** + * 创建一个正则表达式对象,根据给定的搜索字符串和选项进行配置。 + * + * @param {string} searchString - 要搜索的字符串。 + * @param {boolean} isRegex - 是否使用正则表达式。 + * @param {RegExpOptions} options - 正则表达式选项。 + * @returns {RegExp} - 创建的正则表达式对象。 + */ +export function createRegExp(searchString: string, isRegex: boolean, options: RegExpOptions = {}): RegExp { + if (!searchString) { + throw new Error('Cannot create regex from empty string'); + } + if (!isRegex) { + searchString = escapeRegExpCharacters(searchString); + } + if (options.wholeWord) { + if (!/\B/.test(searchString.charAt(0))) { + searchString = '\\b' + searchString; + } + if (!/\B/.test(searchString.charAt(searchString.length - 1))) { + searchString = searchString + '\\b'; + } + } + let modifiers = ''; + if (options.global) { + modifiers += 'g'; + } + if (!options.matchCase) { + modifiers += 'i'; + } + if (options.multiline) { + modifiers += 'm'; + } + if (options.unicode) { + modifiers += 'u'; + } + + return new RegExp(searchString, modifiers); +} + +export function getLeadingWhitespace(str: string, start: number = 0, end: number = str.length): string { + for (let i = start; i < end; i++) { + const chCode = str.charCodeAt(i); + if (chCode !== CharCode.Space && chCode !== CharCode.Tab) { + return str.substring(start, i); + } + } + return str.substring(start, end); +} + +export function firstNonWhitespaceIndex(str: string): number { + for (let i = 0, len = str.length; i < len; i++) { + const chCode = str.charCodeAt(i); + if (chCode !== CharCode.Space && chCode !== CharCode.Tab) { + return i; + } + } + return -1; +} diff --git a/packages/semi-json-viewer-core/src/common/uint.ts b/packages/semi-json-viewer-core/src/common/uint.ts new file mode 100644 index 0000000000..15dc4bc01d --- /dev/null +++ b/packages/semi-json-viewer-core/src/common/uint.ts @@ -0,0 +1,56 @@ +/** reference from https://github.com/microsoft/vscode */ + +export const enum Constants { + /** + * MAX SMI (SMall Integer) as defined in v8. + * one bit is lost for boxing/unboxing flag. + * one bit is lost for sign flag. + * See https://thibaultlaurens.github.io/javascript/2013/04/29/how-the-v8-engine-works/#tagged-values + */ + MAX_SAFE_SMALL_INTEGER = 1 << 30, + + /** + * MIN SMI (SMall Integer) as defined in v8. + * one bit is lost for boxing/unboxing flag. + * one bit is lost for sign flag. + * See https://thibaultlaurens.github.io/javascript/2013/04/29/how-the-v8-engine-works/#tagged-values + */ + MIN_SAFE_SMALL_INTEGER = -(1 << 30), + + /** + * Max unsigned integer that fits on 8 bits. + */ + MAX_UINT_8 = 255, // 2^8 - 1 + + /** + * Max unsigned integer that fits on 16 bits. + */ + MAX_UINT_16 = 65535, // 2^16 - 1 + + /** + * Max unsigned integer that fits on 32 bits. + */ + MAX_UINT_32 = 4294967295, // 2^32 - 1 + + UNICODE_SUPPLEMENTARY_PLANE_BEGIN = 0x010000, +} + +export function toUint8(v: number): number { + if (v < 0) { + return 0; + } + if (v > Constants.MAX_UINT_8) { + return Constants.MAX_UINT_8; + } + return v | 0; +} + +export function toUint32(v: number): number { + if (v < 0) { + return 0; + } + if (v > Constants.MAX_UINT_32) { + return Constants.MAX_UINT_32; + } + return v | 0; +} diff --git a/packages/semi-json-viewer-core/src/common/utils.ts b/packages/semi-json-viewer-core/src/common/utils.ts new file mode 100644 index 0000000000..2792a21610 --- /dev/null +++ b/packages/semi-json-viewer-core/src/common/utils.ts @@ -0,0 +1,7 @@ +export function isObject(val: any): val is Record { + return typeof val === 'object' && val !== null && !Array.isArray(val); +} + +export function isNumber(val: any): val is number { + return typeof val === 'number'; +} \ No newline at end of file diff --git a/packages/semi-json-viewer-core/src/common/wordCharacterClassifier.ts b/packages/semi-json-viewer-core/src/common/wordCharacterClassifier.ts new file mode 100644 index 0000000000..57e8499a3f --- /dev/null +++ b/packages/semi-json-viewer-core/src/common/wordCharacterClassifier.ts @@ -0,0 +1,113 @@ +/** reference from https://github.com/microsoft/vscode */ + +import { CharCode } from './charCode'; +import { LRUCache } from './map'; +import { CharacterClassifier } from './characterClassifier'; + +export const enum WordCharacterClass { + Regular = 0, + Whitespace = 1, + WordSeparator = 2, +} + +export class WordCharacterClassifier extends CharacterClassifier { + public readonly intlSegmenterLocales: Intl.UnicodeBCP47LocaleIdentifier[]; + private readonly _segmenter: Intl.Segmenter | null = null; + private _cachedLine: string | null = null; + private _cachedSegments: IntlWordSegmentData[] = []; + + constructor(wordSeparators: string, intlSegmenterLocales: Intl.UnicodeBCP47LocaleIdentifier[]) { + super(WordCharacterClass.Regular); + this.intlSegmenterLocales = intlSegmenterLocales; + if (this.intlSegmenterLocales.length > 0) { + this._segmenter = new Intl.Segmenter(this.intlSegmenterLocales, { + granularity: 'word', + }); + } else { + this._segmenter = null; + } + + for (let i = 0, len = wordSeparators.length; i < len; i++) { + this.set(wordSeparators.charCodeAt(i), WordCharacterClass.WordSeparator); + } + + this.set(CharCode.Space, WordCharacterClass.Whitespace); + this.set(CharCode.Tab, WordCharacterClass.Whitespace); + } + + public findPrevIntlWordBeforeOrAtOffset(line: string, offset: number): IntlWordSegmentData | null { + let candidate: IntlWordSegmentData | null = null; + for (const segment of this._getIntlSegmenterWordsOnLine(line)) { + if (segment.index > offset) { + break; + } + candidate = segment; + } + return candidate; + } + + public findNextIntlWordAtOrAfterOffset(lineContent: string, offset: number): IntlWordSegmentData | null { + for (const segment of this._getIntlSegmenterWordsOnLine(lineContent)) { + if (segment.index < offset) { + continue; + } + return segment; + } + return null; + } + + private _getIntlSegmenterWordsOnLine(line: string): IntlWordSegmentData[] { + if (!this._segmenter) { + return []; + } + + // Check if the line has changed from the previous call + if (this._cachedLine === line) { + return this._cachedSegments; + } + + // Update the cache with the new line + this._cachedLine = line; + this._cachedSegments = this._filterWordSegments(this._segmenter.segment(line)); + + return this._cachedSegments; + } + + private _filterWordSegments(segments: Intl.Segments): IntlWordSegmentData[] { + const result: IntlWordSegmentData[] = []; + for (const segment of segments) { + if (this._isWordLike(segment)) { + result.push(segment); + } + } + return result; + } + + private _isWordLike(segment: Intl.SegmentData): segment is IntlWordSegmentData { + if (segment.isWordLike) { + return true; + } + return false; + } +} + +export interface IntlWordSegmentData extends Intl.SegmentData { + isWordLike: true +} + +const wordClassifierCache = new LRUCache(10); + +type UnicodeBCP47LocaleIdentifier = string; + +export function getMapForWordSeparators( + wordSeparators: string, + intlSegmenterLocales: UnicodeBCP47LocaleIdentifier[] +): WordCharacterClassifier { + const key = `${wordSeparators}/${intlSegmenterLocales.join(',')}`; + let result = wordClassifierCache.get(key)!; + if (!result) { + result = new WordCharacterClassifier(wordSeparators, intlSegmenterLocales); + wordClassifierCache.set(key, result); + } + return result; +} diff --git a/packages/semi-json-viewer-core/src/common/worker.ts b/packages/semi-json-viewer-core/src/common/worker.ts new file mode 100644 index 0000000000..bc27597aac --- /dev/null +++ b/packages/semi-json-viewer-core/src/common/worker.ts @@ -0,0 +1,6 @@ +const isWebWorker = + typeof self === 'object' && self.constructor && self.constructor.name === 'DedicatedWorkerGlobalScope'; + +export function isInWorkerThread(): boolean { + return isWebWorker; +} diff --git a/packages/semi-json-viewer-core/src/index.ts b/packages/semi-json-viewer-core/src/index.ts new file mode 100644 index 0000000000..e7ff7f2cb6 --- /dev/null +++ b/packages/semi-json-viewer-core/src/index.ts @@ -0,0 +1 @@ +export * from './json-viewer/jsonViewer'; diff --git a/packages/semi-json-viewer-core/src/json-viewer/jsonViewer.ts b/packages/semi-json-viewer-core/src/json-viewer/jsonViewer.ts new file mode 100644 index 0000000000..800bbfe2cb --- /dev/null +++ b/packages/semi-json-viewer-core/src/json-viewer/jsonViewer.ts @@ -0,0 +1,68 @@ +import { View } from '../view/view'; +import { JSONModel } from '../model/jsonModel'; +import { disposeEmitter, Emitter, getEmitter } from '../common/emitter'; +import { createModel } from '../model'; +import { disposeWorkerManager, getJsonWorkerManager, JsonWorkerManager } from '../worker/jsonWorkerManager'; +import { CompletionItem } from '../service/jsonTypes'; +import { GlobalEvents } from '../common/emitterEvents'; +import { setCurrentNameSpaceId } from '../common/nameSpace'; +/** + * JsonViewer 主类 + */ +export interface JsonViewerOptions { + lineHeight?: number; + autoWrap?: boolean; + formatOptions?: FormattingOptions; + completionOptions?: CompletionOptions +} + +export interface CompletionOptions { + staticCompletions?: CompletionItem[] +} + +export interface FormattingOptions { + tabSize?: number; + insertSpaces?: boolean; + eol?: string +} + +export class JsonViewer { + private _container: HTMLElement; + private _jsonModel: JSONModel; + private _view: View; + private _jsonWorkerManager: JsonWorkerManager | null = null; + public emitter: Emitter; + private _id: string = `jsonviewer-${Math.random().toString(36).substr(2, 9)}`; + + constructor(container: HTMLElement, value: string, options?: JsonViewerOptions) { + setCurrentNameSpaceId(this._id); + this.emitter = getEmitter(); + this._container = container; + this._jsonModel = createModel(value); + this._jsonWorkerManager = getJsonWorkerManager(); + this._jsonWorkerManager.init(value); + this._view = new View(container, this._jsonModel, options); + } + + layout() { + this._view.layout(); + } + + getModel() { + return this._jsonModel; + } + + getSearchWidget() { + return this._view.searchWidget; + } + + format() { + this._view.editWidget.format(); + } + + dispose() { + disposeEmitter(this._id); + disposeWorkerManager(this._id); + this._view.dispose(); + } +} diff --git a/packages/semi-json-viewer-core/src/model/command.ts b/packages/semi-json-viewer-core/src/model/command.ts new file mode 100644 index 0000000000..694c4dd166 --- /dev/null +++ b/packages/semi-json-viewer-core/src/model/command.ts @@ -0,0 +1,113 @@ +import { JSONModel } from './jsonModel'; +import { IModelContentChangeEvent } from '../common/emitterEvents'; + +export interface Command { + execute(): void; + undo(): void; + readonly operation: IModelContentChangeEvent | IModelContentChangeEvent[]; + readonly oldPos: { lineNumber: number; column: number }; + readonly newPos: { lineNumber: number; column: number } +} + +export abstract class BaseCommand implements Command { + public readonly oldPos: { lineNumber: number; column: number }; + public readonly newPos: { lineNumber: number; column: number }; + + constructor(protected model: JSONModel, public readonly operation: IModelContentChangeEvent) { + this.oldPos = { ...model.lastChangeBufferPos }; + this.model.updateLastChangeBufferPos(operation); + this.newPos = { ...model.lastChangeBufferPos }; + } + + abstract execute(): void; + abstract undo(): void; + + protected updateBufferPos(isUndo: boolean): void { + this.model.lastChangeBufferPos = { + ...(isUndo ? this.oldPos : this.newPos), + }; + } +} + +export class InsertCommand extends BaseCommand { + execute(): void { + this.model.pieceTree.insert(this.operation.rangeOffset, this.operation.newText); + this.updateBufferPos(false); + } + + undo(): void { + this.model.pieceTree.delete(this.operation.rangeOffset, this.operation.newText.length); + this.updateBufferPos(true); + } +} + +export class DeleteCommand extends BaseCommand { + execute(): void { + this.model.pieceTree.delete(this.operation.rangeOffset, this.operation.rangeLength); + this.updateBufferPos(false); + } + + undo(): void { + this.model.pieceTree.insert(this.operation.rangeOffset, this.operation.oldText); + this.updateBufferPos(true); + } +} + +export class ReplaceCommand extends BaseCommand { + execute(): void { + this.model.pieceTree.delete(this.operation.rangeOffset, this.operation.oldText.length); + this.model.pieceTree.insert(this.operation.rangeOffset, this.operation.newText); + this.updateBufferPos(false); + } + + undo(): void { + this.model.pieceTree.delete(this.operation.rangeOffset, this.operation.newText.length); + this.model.pieceTree.insert(this.operation.rangeOffset, this.operation.oldText); + this.updateBufferPos(true); + } +} + +export class MultiCommand implements Command { + public readonly oldPos: { lineNumber: number; column: number }; + public readonly newPos: { lineNumber: number; column: number }; + + constructor(private model: JSONModel, public readonly operation: IModelContentChangeEvent[]) { + this.oldPos = { ...model.lastChangeBufferPos }; + // operation.forEach(op => this.model.updateLastChangeBufferPos(op)); + this.newPos = { ...model.lastChangeBufferPos }; + } + + execute(): void { + for (let i = 0; i < this.operation.length; i++) { + const op = this.operation[i]; + switch (op.type) { + case 'insert': + this.model.pieceTree.insert(op.rangeOffset, op.newText); + break; + case 'delete': + this.model.pieceTree.delete(op.rangeOffset, op.rangeLength); + break; + case 'replace': + this.model.pieceTree.delete(op.rangeOffset, op.oldText.length); + this.model.pieceTree.insert(op.rangeOffset, op.newText); + break; + } + } + this.model.lastChangeBufferPos = { ...this.newPos }; + } + + undo(): void { + for (let i = this.operation.length - 1; i >= 0; i--) { + const op = this.operation[i]; + if (op.newText && op.oldText) { + this.model.pieceTree.delete(op.rangeOffset, op.newText.length); + this.model.pieceTree.insert(op.rangeOffset, op.oldText); + } else if (op.newText) { + this.model.pieceTree.delete(op.rangeOffset, op.newText.length); + } else { + this.model.pieceTree.insert(op.rangeOffset, op.oldText); + } + } + this.model.lastChangeBufferPos = { ...this.oldPos }; + } +} diff --git a/packages/semi-json-viewer-core/src/model/foldingModel.ts b/packages/semi-json-viewer-core/src/model/foldingModel.ts new file mode 100644 index 0000000000..031671f2b2 --- /dev/null +++ b/packages/semi-json-viewer-core/src/model/foldingModel.ts @@ -0,0 +1,138 @@ +import { JSONModel } from './jsonModel'; +import { getFoldingRanges, FoldingRange } from '../service/jsonService'; +import { Emitter, getEmitter } from '../common/emitter'; +import { getJsonWorkerManager, JsonWorkerManager } from '../worker/jsonWorkerManager'; +import { GlobalEvents } from '../common/emitterEvents'; + +/** + * 折叠模型,管理JSON的折叠范围 + */ +//TODO 修改range数据结构 +export class FoldingModel { + private _jsonModel: JSONModel; + private _foldingRanges: FoldingRange[] = []; + private _collapsedRanges: Map = new Map(); // startLine -> endLine + private _jsonWorkerManager: JsonWorkerManager = getJsonWorkerManager(); + private emitter: Emitter = getEmitter(); + constructor(jsonModel: JSONModel) { + this._jsonModel = jsonModel; + this.updateFoldingRanges(); + this.emitter.on('problemsChanged', e => { + this.updateFoldingRanges(); + }); + } + + public updateFoldingRanges(): void { + this._jsonWorkerManager.foldRange().then(ranges => { + this._foldingRanges = ranges; + this.updateCollapsedRanges(); + }); + } + + private updateCollapsedRanges(): void { + const newCollapsedRanges = new Map(); + + for (const [startLine, endLine] of this._collapsedRanges) { + const range = this._foldingRanges.find(r => r.startLine === startLine); + if (range) { + newCollapsedRanges.set(startLine, range.endLine); + } + } + + this._collapsedRanges = newCollapsedRanges; + } + + public getFoldingRanges(): FoldingRange[] { + return this._foldingRanges; + } + + public toggleFoldingRange(startLine: number): void { + if (this._collapsedRanges.has(startLine)) { + this._collapsedRanges.delete(startLine); + } else { + const range = this._foldingRanges.find(r => r.startLine === startLine); + if (range) { + this._collapsedRanges.set(startLine, range.endLine); + } + } + } + + public isCollapsed(lineNumber: number): boolean { + return this._collapsedRanges.has(lineNumber); + } + + public isLineCollapsed(lineNumber: number): boolean { + if (this._collapsedRanges.has(lineNumber)) { + return false; + } + for (const [startLine, endLine] of this._collapsedRanges) { + if (lineNumber > startLine && lineNumber <= endLine) { + return true; + } + } + return false; + } + + public getVisibleLineNumber(actualLineNumber: number): number { + let visibleLine = actualLineNumber; + for (const [startLine, endLine] of this._collapsedRanges) { + if (startLine < actualLineNumber) { + if (endLine < actualLineNumber) { + visibleLine -= endLine - startLine; + } else if (actualLineNumber > startLine) { + return -1; + } + } else { + break; + } + } + return visibleLine; + } + + public getNextVisibleLine(actualLineNumber: number): number { + for (const [startLine, endLine] of this._collapsedRanges) { + if (actualLineNumber >= startLine && actualLineNumber <= endLine) { + return actualLineNumber === startLine ? startLine + 1 : endLine + 1; + } + } + return actualLineNumber + 1; + } + + public getActualLineNumber(visibleLineNumber: number): number { + let actualLine = visibleLineNumber; + for (const [startLine, endLine] of this._collapsedRanges) { + if (startLine < actualLine) { + actualLine += endLine - startLine; + } else { + break; + } + } + return actualLine; + } + + public isFoldable(lineNumber: number): boolean { + return this._foldingRanges.some(range => range.startLine === lineNumber); + } + + public expandLine(lineNumber: number): void { + for (const [startLine, endLine] of this._collapsedRanges) { + if (lineNumber > startLine && lineNumber <= endLine) { + this._collapsedRanges.delete(startLine); + } + } + } + + public getVisibleLineCount(): number { + let visibleCount = 0; + let lineNumber = 1; + + while (lineNumber <= this._jsonModel.getLineCount()) { + if (!this.isLineCollapsed(lineNumber)) { + visibleCount++; + } + lineNumber = this.getNextVisibleLine(lineNumber); + } + + return visibleCount; + } +} diff --git a/packages/semi-json-viewer-core/src/model/index.ts b/packages/semi-json-viewer-core/src/model/index.ts new file mode 100644 index 0000000000..3189188012 --- /dev/null +++ b/packages/semi-json-viewer-core/src/model/index.ts @@ -0,0 +1,5 @@ +import { JSONModel } from './jsonModel'; + +export function createModel(text: string, normalizeEOL: boolean = true): JSONModel { + return new JSONModel(text, normalizeEOL); +} diff --git a/packages/semi-json-viewer-core/src/model/jsonModel.ts b/packages/semi-json-viewer-core/src/model/jsonModel.ts new file mode 100644 index 0000000000..8dd66cdaba --- /dev/null +++ b/packages/semi-json-viewer-core/src/model/jsonModel.ts @@ -0,0 +1,326 @@ +import { GlobalEvents, IModelContentChangeEvent } from '../common/emitterEvents'; +import { DefaultEndOfLine, PieceTreeBase, PieceTreeTextBufferBuilder } from '../pieceTreeTextBuffer'; +import { Range } from '../common/range'; +import { Emitter, getEmitter } from '../common/emitter'; +import { Position } from '../common/position'; +import { EndOfLinePreference, FindMatch, SearchData } from '../common/model'; +import { SearchParams, TextModelSearch } from './textModelSearch'; +import { getJsonWorkerManager, JsonWorkerManager } from '../worker/jsonWorkerManager'; +import { isInWorkerThread } from '../common/worker'; +import { Command, DeleteCommand, InsertCommand, MultiCommand, ReplaceCommand } from './command'; + +/** + * JSONModel 类用于管理 JSON 数据模型 + */ +export class JSONModel { + private _pieceTree: PieceTreeBase; + private _normalizeEOL: boolean; + private _undoStack: Command[] = []; + private _redoStack: Command[] = []; + private readonly MAX_STACK_SIZE = 20; + public lastChangeBufferPos = { + lineNumber: 1, + column: 1, + }; + + private _jsonWorkerManager: JsonWorkerManager | null = null; + private emitter: Emitter | null = null; + + constructor(value: string, normalizeEOL: boolean = true) { + this._normalizeEOL = normalizeEOL; + this._pieceTree = this.createTextBuffer(value); + if (!isInWorkerThread()) { + this._jsonWorkerManager = getJsonWorkerManager(); + this.emitter = getEmitter(); + } + } + + get pieceTree() { + return this._pieceTree; + } + + createTextBufferFactory(value: string) { + const builder = new PieceTreeTextBufferBuilder(); + builder.acceptChunk(value); + return builder.finish(this._normalizeEOL); + } + + createTextBuffer(value: string) { + return this.createTextBufferFactory(value).create(DefaultEndOfLine.LF); + } + + /** + * 获取行数 + * @returns 行数 + */ + getLineCount(): number { + return this._pieceTree.getLineCount(); + } + + /** + * 获取行内容 + * @param lineNumber 行号 + * @returns 行内容 + */ + getLineContent(lineNumber: number): string { + return this._pieceTree.getLineContent(lineNumber); + } + + /** + * 获取行长度 + * @param lineNumber 行号 + * @returns 行内容 + */ + getLineLength(lineNumber: number): number { + return this._pieceTree.getLineLength(lineNumber); + } + + /** + * 获取偏移 + * @param lineNumber 行号 + * @param column 列号 + * @returns 行偏移 + */ + getOffsetAt(lineNumber: number, column: number): number { + return this._pieceTree.getOffsetAt(lineNumber, column); + } + + positionAt(offset: number): Position { + offset = Math.min(this._pieceTree.getLength(), Math.max(0, offset)); + return this._pieceTree.getPositionAt(offset); + } + + private _createCommand(op: IModelContentChangeEvent | IModelContentChangeEvent[]): Command { + if (Array.isArray(op)) { + return new MultiCommand(this, op); + } + switch (op.type) { + case 'insert': + return new InsertCommand(this, op); + case 'delete': + return new DeleteCommand(this, op); + case 'replace': + return new ReplaceCommand(this, op); + default: + throw new Error('Unknown operation type'); + } + } + + applyOperation(op: IModelContentChangeEvent | IModelContentChangeEvent[]) { + this._redoStack = []; + const command = this._createCommand(op); + this.pushUndoStack(command); + command.execute(); + + if (!isInWorkerThread()) { + this.emitter?.emit('contentChanged', op); + } + if (this._jsonWorkerManager) { + this._jsonWorkerManager + .updateModel(op) + .then(res => { + return this._jsonWorkerManager?.validate(); + }) + .then(result => { + this.emitter?.emit('problemsChanged', { + problems: result.problems, + root: result.root, + }); + }); + } + } + + updateLastChangeBufferPos(op: IModelContentChangeEvent) { + if (op.keepPosition) { + this.lastChangeBufferPos = op.keepPosition; + return; + } + switch (op.type) { + case 'insert': + this.lastChangeBufferPos.column += op.newText.length; + break; + case 'delete': + if (this.lastChangeBufferPos.column === 1) { + this.lastChangeBufferPos.lineNumber -= 1; + this.lastChangeBufferPos.column = this.getLineLength(this.lastChangeBufferPos.lineNumber) + 1; + } else { + const startColumn = op.range.startColumn; + const newColumn = op.rangeLength === 1 ? startColumn - 1 : startColumn; + this.lastChangeBufferPos.column = newColumn; + } + break; + case 'replace': + const newLineNumber = op.range.startLineNumber; + const newColumn = op.range.startColumn + op.newText.length; + this.lastChangeBufferPos.lineNumber = newLineNumber; + this.lastChangeBufferPos.column = newColumn; + break; + } + } + + pushUndoStack(command: Command) { + this._undoStack.push(command); + if (this._undoStack.length > this.MAX_STACK_SIZE) { + this._undoStack.shift(); + } + } + + pushRedoStack(command: Command) { + this._redoStack.push(command); + if (this._redoStack.length > this.MAX_STACK_SIZE) { + this._redoStack.shift(); + } + } + + canUndo(): boolean { + return this._undoStack.length > 0; + } + + canRedo(): boolean { + return this._redoStack.length > 0; + } + + undo() { + if (!this.canUndo()) return; + + const command = this._undoStack.pop()!; + command.undo(); + this._redoStack.push(command); + this.emitter?.emit('contentChanged', command.operation); + } + + redo() { + if (!this.canRedo()) return; + + const command = this._redoStack.pop()!; + command.execute(); + this._undoStack.push(command); + this.emitter?.emit('contentChanged', command.operation); + } + + + /** + * 获取值 + * @returns 值 + */ + getValue(): string { + return this._pieceTree.getValueInRange({ + startLineNumber: 1, + startColumn: 1, + endLineNumber: this._pieceTree.getLineCount(), + endColumn: this._pieceTree.getLineContent(this._pieceTree.getLineCount()).length + 1, + } as Range); + } + + /** + * 设置值 + * @param value 值 + */ + setValue(value: string): void { + const builder = new PieceTreeTextBufferBuilder(); + builder.acceptChunk(value); + this._pieceTree = builder.finish(this._normalizeEOL).create(1); + } + + getEOL() { + return this._pieceTree.getEOL(); + } + + private _getEndOfLine(eol: EndOfLinePreference): string { + switch (eol) { + case EndOfLinePreference.LF: + return '\n'; + case EndOfLinePreference.CRLF: + return '\r\n'; + case EndOfLinePreference.TextDefined: + return this.getEOL(); + default: + throw new Error('Unknown EOL preference'); + } + } + + getValueInRange(range: Range, eol: EndOfLinePreference = EndOfLinePreference.TextDefined) { + return this._pieceTree.getValueInRange(range, this._getEndOfLine(eol)); + } + + getFullModelRange(): Range { + const lineCount = this.getLineCount(); + return new Range(1, 1, lineCount, this.getLineLength(lineCount) + 1); + } + + findMatchesLineByLine( + searchRange: Range, + searchData: SearchData, + captureMatches: boolean, + limitResultCount: number + ) { + return this._pieceTree.findMatchesLineByLine(searchRange, searchData, captureMatches, limitResultCount); + } + + /** + * 查找匹配 + * @param searchString 搜索字符串 + * @param rawSearchScope 搜索范围 + * @param isRegex 是否为正则表达式 + * @param matchCase 是否匹配大小写 + * @param wordSeparators 分隔符 + * @param captureMatches 是否捕获匹配 + * @param limitResultCount 限制结果数量 + * @returns 匹配结果 + * Based on https://github.com/microsoft/vscode with modifications for custom requirements + */ + findMatches( + searchString: string, + rawSearchScope: unknown, + isRegex: boolean, + matchCase: boolean, + wordSeparators: string | null, + captureMatches: boolean, + limitResultCount: number = Infinity + ) { + let searchRanges: Range[] | null = null; + + if (searchRanges === null) { + searchRanges = [this.getFullModelRange()]; + } + + searchRanges = searchRanges.sort( + (d1, d2) => d1.startLineNumber - d2.startLineNumber || d1.startColumn - d2.startColumn + ); + + const uniqueSearchRanges: Range[] = []; + uniqueSearchRanges.push( + searchRanges.reduce((prev, curr) => { + if (Range.areIntersecting(prev, curr)) { + return prev.plusRange(curr); + } + + uniqueSearchRanges.push(prev); + return curr; + }) + ); + + let matchMapper: (value: Range, index: number, array: Range[]) => FindMatch[]; + if (!isRegex && searchString.indexOf('\n') < 0) { + // not regex, not multi line + const searchParams = new SearchParams(searchString, isRegex, matchCase, wordSeparators); + const searchData = searchParams.parseSearchRequest(); + if (!searchData) { + return []; + } + + matchMapper = (searchRange: Range) => + this.findMatchesLineByLine(searchRange, searchData, captureMatches, limitResultCount); + } else { + matchMapper = (searchRange: Range) => + TextModelSearch.findMatches( + this, + new SearchParams(searchString, isRegex, matchCase, wordSeparators), + searchRange, + captureMatches, + limitResultCount + ); + } + return uniqueSearchRanges.map(matchMapper).reduce((arr, matches: FindMatch[]) => arr.concat(matches), []); + } +} diff --git a/packages/semi-json-viewer-core/src/model/selectionModel.ts b/packages/semi-json-viewer-core/src/model/selectionModel.ts new file mode 100644 index 0000000000..c659c3869f --- /dev/null +++ b/packages/semi-json-viewer-core/src/model/selectionModel.ts @@ -0,0 +1,151 @@ +import { JSONModel } from './jsonModel'; +import { View } from '../view/view'; +import { getLineElement } from '../common/dom'; +import { Position } from '../common/position'; + +/** + * 选择模型,管理JSON的选中范围和选中状态 + */ +export class SelectionModel { + private _row: number; + private _col: number; + public startRow: number; + public startCol: number; + public endRow: number; + public endCol: number; + public isCollapsed: boolean; + public isSelectedAll: boolean = false; + private _view: View; + private _jsonModel: JSONModel; + constructor(row: number, col: number, view: View, jsonModel: JSONModel) { + this._row = row; + this._col = col; + this._view = view; + this.startRow = row; + this.startCol = col; + this.endRow = row; + this.endCol = col; + this.isCollapsed = true; + this._jsonModel = jsonModel; + } + + + updateSelection(row: number, col: number) { + this._row = row; + this._col = col; + } + + getSelection() { + return { + row: this._row, + col: this._col, + }; + } + + getPosition(): Position { + return { + lineNumber: this._row, + column: this._col, + } as Position; + } + + public updateFromSelection() { + const selection = window.getSelection(); + if (!selection || selection.rangeCount === 0) return; + + const range = selection.getRangeAt(0); + this.isCollapsed = range.collapsed; + const startContainer = range.startContainer; + const endContainer = range.endContainer; + + let { row: row1, col: col1 } = this.convertRangeToModelPosition(startContainer, selection, true); + let { row: row2, col: col2 } = this.convertRangeToModelPosition(endContainer, selection, false); + if (row1 > row2) { + [row1, row2] = [row2, row1]; + [col1, col2] = [col2, col1]; + } else if (row1 === row2 && col1 > col2) { + [col1, col2] = [col2, col1]; + } + + this._row = row1; + this._col = col1; + this.startRow = row1; + this.startCol = col1; + this.endRow = row2; + this.endCol = col2; + this._jsonModel.lastChangeBufferPos = { + lineNumber: this._row, + column: this._col, + }; + } + + public toViewPosition() { + const selection = window.getSelection(); + + if (!selection) return; + const range = new Range(); + + if (this.isSelectedAll) { + range.setStartBefore(this._view.scrollDom.firstChild!); + range.setEndAfter(this._view.scrollDom.lastChild!); + selection.removeAllRanges(); + selection.addRange(range); + return; + } + const row = this._jsonModel.lastChangeBufferPos.lineNumber; + const col = this._jsonModel.lastChangeBufferPos.column - 1; + + if (row < this._view.startLineNumber || row > this._view.startLineNumber + this._view.visibleLineCount) { + selection.removeAllRanges(); + return; + } + const lineElement = this._view.getLineElement(row); + if (!lineElement) return; + if (col === 0) { + range.setStart(lineElement, 0); + range.setEnd(lineElement, 0); + } else { + let offset = col; + for (let i = 0; i < lineElement.childNodes.length; i++) { + const childNode = lineElement.childNodes[i]; + if (childNode.textContent && offset <= childNode.textContent.length) { + range.setStart(childNode.childNodes[0], offset); + range.setEnd(childNode.childNodes[0], offset); + break; + } + offset -= (childNode as Text).textContent?.length || 0; + } + } + + if (!selection) return; + selection.removeAllRanges(); + selection.addRange(range); + } + + convertRangeToModelPosition(node: Node, selection: Selection, isStart: boolean) { + let row = 1; + let col = 0; + if (!node) return { row, col }; + let lineElement: HTMLElement | null; + if (node instanceof HTMLElement) { + lineElement = node.closest('.semi-json-viewer-view-line'); + } else { + lineElement = getLineElement(node); + if (!lineElement) return { row, col }; + let totalOffset = 0; + for (let i = 0; i < lineElement.childNodes.length; i++) { + const childNode = lineElement.childNodes[i]; + + if (childNode === node.parentElement) { + totalOffset += isStart ? selection.anchorOffset : selection.focusOffset; + break; + } + totalOffset += childNode.textContent?.length || 0; + } + + col = totalOffset; + } + row = (lineElement as any).lineNumber || 1; + return { row, col: col + 1 }; + } +} diff --git a/packages/semi-json-viewer-core/src/model/textModelSearch.ts b/packages/semi-json-viewer-core/src/model/textModelSearch.ts new file mode 100644 index 0000000000..f907ce6691 --- /dev/null +++ b/packages/semi-json-viewer-core/src/model/textModelSearch.ts @@ -0,0 +1,529 @@ +/** Based on https://github.com/microsoft/vscode-json-languageservice with modifications for custom requirements */ +import { CharCode } from '../common/charCode'; +import { + getMapForWordSeparators, + WordCharacterClass, + WordCharacterClassifier, +} from '../common/wordCharacterClassifier'; +import { FindMatch } from '../common/model'; +import { Range } from '../common/range'; +import { EndOfLinePreference, SearchData } from '../common/model'; +import { createRegExp, getNextCodePoint } from '../common/strings'; +import { JSONModel } from './jsonModel'; +const LIMIT_FIND_COUNT = 999; + + +class LineFeedCounter { + private readonly _lineFeedsOffsets: number[]; + + constructor(text: string) { + const lineFeedsOffsets: number[] = []; + let lineFeedsOffsetsLen = 0; + for (let i = 0, textLen = text.length; i < textLen; i++) { + if (text.charCodeAt(i) === CharCode.LineFeed) { + lineFeedsOffsets[lineFeedsOffsetsLen++] = i; + } + } + this._lineFeedsOffsets = lineFeedsOffsets; + } + + public findLineFeedCountBeforeOffset(offset: number): number { + const lineFeedsOffsets = this._lineFeedsOffsets; + let min = 0; + let max = lineFeedsOffsets.length - 1; + + if (max === -1) { + // no line feeds + return 0; + } + + if (offset <= lineFeedsOffsets[0]) { + // before first line feed + return 0; + } + + while (min < max) { + const mid = min + (((max - min) / 2) >> 0); + + if (lineFeedsOffsets[mid] >= offset) { + max = mid - 1; + } else { + if (lineFeedsOffsets[mid + 1] >= offset) { + // bingo! + min = mid; + max = mid; + } else { + min = mid + 1; + } + } + } + return min + 1; + } +} + +export class TextModelSearch { + public static findMatches( + model: JSONModel, + searchParams: SearchParams, + searchRange: Range, + captureMatches: boolean, + limitResultCount: number + ) { + const searchData = searchParams.parseSearchRequest(); + if (!searchData) return []; + if (searchData.regex.multiline) { + return this._doFindMatchesMultiline( + model, + searchRange, + new Searcher(searchData.wordSeparators, searchData.regex), + captureMatches, + limitResultCount + ); + } + return this._doFindMatchesLineByLine(model, searchRange, searchData, captureMatches, limitResultCount); + } + + private static _doFindMatchesMultiline( + model: JSONModel, + searchRange: Range, + searcher: Searcher, + captureMatches: boolean, + limitResultCount: number + ): FindMatch[] { + const pos = searchRange.getStartPosition(); + const deltaOffset = model.getOffsetAt(pos.lineNumber, pos.column); + // We always execute multiline search over the lines joined with \n + // This makes it that \n will match the EOL for both CRLF and LF models + // We compensate for offset errors in `_getMultilineMatchRange` + const text = model.getValueInRange(searchRange, EndOfLinePreference.LF); + const lfCounter = model.getEOL() === '\r\n' ? new LineFeedCounter(text) : null; + + const result: FindMatch[] = []; + let counter = 0; + + let m: RegExpExecArray | null; + searcher.reset(0); + while ((m = searcher.next(text))) { + result[counter++] = createFindMatch( + this._getMultilineMatchRange(model, deltaOffset, text, lfCounter, m.index, m[0]), + m, + captureMatches + ); + if (counter >= limitResultCount) { + return result; + } + } + + return result; + } + + /** + * Multiline search always executes on the lines concatenated with \n. + * We must therefore compensate for the count of \n in case the model is CRLF + */ + private static _getMultilineMatchRange( + model: JSONModel, + deltaOffset: number, + text: string, + lfCounter: LineFeedCounter | null, + matchIndex: number, + match0: string + ): Range { + let startOffset: number; + let lineFeedCountBeforeMatch = 0; + if (lfCounter) { + lineFeedCountBeforeMatch = lfCounter.findLineFeedCountBeforeOffset(matchIndex); + startOffset = deltaOffset + matchIndex + lineFeedCountBeforeMatch /* add as many \r as there were \n */; + } else { + startOffset = deltaOffset + matchIndex; + } + + let endOffset: number; + if (lfCounter) { + const lineFeedCountBeforeEndOfMatch = lfCounter.findLineFeedCountBeforeOffset(matchIndex + match0.length); + const lineFeedCountInMatch = lineFeedCountBeforeEndOfMatch - lineFeedCountBeforeMatch; + endOffset = startOffset + match0.length + lineFeedCountInMatch /* add as many \r as there were \n */; + } else { + endOffset = startOffset + match0.length; + } + + const startPosition = model.positionAt(startOffset); + const endPosition = model.positionAt(endOffset); + return new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column); + } + + private static _doFindMatchesLineByLine( + model: JSONModel, + searchRange: Range, + searchData: SearchData, + captureMatches: boolean, + limitResultCount: number + ) { + const res: FindMatch[] = []; + let resLen = 0; + if (searchRange.startLineNumber === searchRange.endLineNumber) { + const text = model + .getLineContent(searchRange.startLineNumber) + .substring(searchRange.startColumn - 1, searchRange.endColumn - 1); + resLen = this._findMatchesInLine( + searchData, + text, + searchRange.startLineNumber, + searchRange.startColumn - 1, + resLen, + res, + captureMatches, + limitResultCount + ); + } + const text = model.getLineContent(searchRange.startLineNumber).substring(searchRange.startColumn - 1); + resLen = this._findMatchesInLine( + searchData, + text, + searchRange.startLineNumber, + searchRange.startColumn - 1, + resLen, + res, + captureMatches, + limitResultCount + ); + + // Collect results from middle lines + for ( + let lineNumber = searchRange.startLineNumber + 1; + lineNumber < searchRange.endLineNumber && resLen < limitResultCount; + lineNumber++ + ) { + resLen = this._findMatchesInLine( + searchData, + model.getLineContent(lineNumber), + lineNumber, + 0, + resLen, + res, + captureMatches, + limitResultCount + ); + } + + // Collect results from last line + if (resLen < limitResultCount) { + const text = model.getLineContent(searchRange.endLineNumber).substring(0, searchRange.endColumn - 1); + resLen = this._findMatchesInLine( + searchData, + text, + searchRange.endLineNumber, + 0, + resLen, + res, + captureMatches, + limitResultCount + ); + } + + return res; + } + + private static _findMatchesInLine( + searchData: SearchData, + text: string, + lineNumber: number, + deltaOffset: number, + resultLen: number, + result: FindMatch[], + captureMatches: boolean, + limitResultCount: number + ) { + const wordSeparators = searchData.wordSeparators; + if (!captureMatches && searchData.simpleSearch) { + const searchString = searchData.simpleSearch; + const searchStringLen = searchString.length; + const textLength = text.length; + let lastMatchIndex = -searchStringLen; + while ((lastMatchIndex = text.indexOf(searchString, lastMatchIndex + searchStringLen)) !== -1) { + if ( + !wordSeparators || + isValidMatch(wordSeparators, text, textLength, lastMatchIndex, searchStringLen) + ) { + result[resultLen++] = new FindMatch( + new Range( + lineNumber, + lastMatchIndex + 1 + deltaOffset, + lineNumber, + lastMatchIndex + 1 + searchStringLen + deltaOffset + ), + null + ); + if (resultLen >= limitResultCount) return resultLen; + } + } + } + const searcher = new Searcher(searchData.wordSeparators, searchData.regex); + let m: RegExpExecArray | null; + searcher.reset(0); + do { + m = searcher.next(text); + if (m) { + result[resultLen++] = createFindMatch( + new Range( + lineNumber, + m.index + 1 + deltaOffset, + lineNumber, + m.index + 1 + m[0].length + deltaOffset + ), + m, + captureMatches + ); + if (resultLen >= limitResultCount) { + return resultLen; + } + } + } while (m); + return resultLen; + } +} + +export class SearchParams { + public readonly searchString: string; + public readonly isRegex: boolean; + public readonly matchCase: boolean; + public readonly wordSeparators: string | null; + + constructor(searchString: string, isRegex: boolean, matchCase: boolean, wordSeparators: string | null) { + this.searchString = searchString; + this.isRegex = isRegex; + this.matchCase = matchCase; + this.wordSeparators = wordSeparators; + } + + public parseSearchRequest(): SearchData | null { + if (this.searchString === '') { + return null; + } + + // Try to create a RegExp out of the params + let multiline: boolean; + if (this.isRegex) { + multiline = isMultilineRegexSource(this.searchString); + } else { + multiline = this.searchString.indexOf('\n') >= 0; + } + + let regex: RegExp | null = null; + try { + regex = createRegExp(this.searchString, this.isRegex, { + matchCase: this.matchCase, + wholeWord: false, + multiline: multiline, + global: true, + unicode: true, + }); + } catch (err) { + return null; + } + + if (!regex) { + return null; + } + + let canUseSimpleSearch = !this.isRegex && !multiline; + if (canUseSimpleSearch && this.searchString.toLowerCase() !== this.searchString.toUpperCase()) { + // casing might make a difference + canUseSimpleSearch = this.matchCase; + } + + return new SearchData( + regex, + this.wordSeparators ? getMapForWordSeparators(this.wordSeparators, []) : null, + canUseSimpleSearch ? this.searchString : null + ); + } +} + +export function isMultilineRegexSource(searchString: string): boolean { + if (!searchString || searchString.length === 0) { + return false; + } + + for (let i = 0, len = searchString.length; i < len; i++) { + const chCode = searchString.charCodeAt(i); + + if (chCode === CharCode.LineFeed) { + return true; + } + + if (chCode === CharCode.Backslash) { + // move to next char + i++; + + if (i >= len) { + // string ends with a \ + break; + } + + const nextChCode = searchString.charCodeAt(i); + if (nextChCode === CharCode.n || nextChCode === CharCode.r || nextChCode === CharCode.W) { + return true; + } + } + } + + return false; +} + +function leftIsWordBounday( + wordSeparators: WordCharacterClassifier, + text: string, + textLength: number, + matchStartIndex: number, + matchLength: number +): boolean { + if (matchStartIndex === 0) { + // Match starts at start of string + return true; + } + + const charBefore = text.charCodeAt(matchStartIndex - 1); + if (wordSeparators.get(charBefore) !== WordCharacterClass.Regular) { + // The character before the match is a word separator + return true; + } + + if (charBefore === CharCode.CarriageReturn || charBefore === CharCode.LineFeed) { + // The character before the match is line break or carriage return. + return true; + } + + if (matchLength > 0) { + const firstCharInMatch = text.charCodeAt(matchStartIndex); + if (wordSeparators.get(firstCharInMatch) !== WordCharacterClass.Regular) { + // The first character inside the match is a word separator + return true; + } + } + + return false; +} + +function rightIsWordBounday( + wordSeparators: WordCharacterClassifier, + text: string, + textLength: number, + matchStartIndex: number, + matchLength: number +): boolean { + if (matchStartIndex + matchLength === textLength) { + // Match ends at end of string + return true; + } + + const charAfter = text.charCodeAt(matchStartIndex + matchLength); + if (wordSeparators.get(charAfter) !== WordCharacterClass.Regular) { + // The character after the match is a word separator + return true; + } + + if (charAfter === CharCode.CarriageReturn || charAfter === CharCode.LineFeed) { + // The character after the match is line break or carriage return. + return true; + } + + if (matchLength > 0) { + const lastCharInMatch = text.charCodeAt(matchStartIndex + matchLength - 1); + if (wordSeparators.get(lastCharInMatch) !== WordCharacterClass.Regular) { + // The last character in the match is a word separator + return true; + } + } + + return false; +} + +export function isValidMatch( + wordSeparators: WordCharacterClassifier, + text: string, + textLength: number, + matchStartIndex: number, + matchLength: number +): boolean { + return ( + leftIsWordBounday(wordSeparators, text, textLength, matchStartIndex, matchLength) && + rightIsWordBounday(wordSeparators, text, textLength, matchStartIndex, matchLength) + ); +} + +export class Searcher { + public readonly _wordSeparators: WordCharacterClassifier | null; + private readonly _searchRegex: RegExp; + private _prevMatchStartIndex: number; + private _prevMatchLength: number; + + constructor(wordSeparators: WordCharacterClassifier | null, searchRegex: RegExp) { + this._wordSeparators = wordSeparators; + this._searchRegex = searchRegex; + this._prevMatchStartIndex = -1; + this._prevMatchLength = 0; + } + + public reset(lastIndex: number): void { + this._searchRegex.lastIndex = lastIndex; + this._prevMatchStartIndex = -1; + this._prevMatchLength = 0; + } + + public next(text: string): RegExpExecArray | null { + const textLength = text.length; + + let m: RegExpExecArray | null; + do { + if (this._prevMatchStartIndex + this._prevMatchLength === textLength) { + // Reached the end of the line + return null; + } + + m = this._searchRegex.exec(text); + if (!m) { + return null; + } + + const matchStartIndex = m.index; + const matchLength = m[0].length; + if (matchStartIndex === this._prevMatchStartIndex && matchLength === this._prevMatchLength) { + if (matchLength === 0) { + // the search result is an empty string and won't advance `regex.lastIndex`, so `regex.exec` will stuck here + // we attempt to recover from that by advancing by two if surrogate pair found and by one otherwise + if (getNextCodePoint(text, textLength, this._searchRegex.lastIndex) > 0xffff) { + this._searchRegex.lastIndex += 2; + } else { + this._searchRegex.lastIndex += 1; + } + continue; + } + // Exit early if the regex matches the same range twice + return null; + } + this._prevMatchStartIndex = matchStartIndex; + this._prevMatchLength = matchLength; + + if ( + !this._wordSeparators || + isValidMatch(this._wordSeparators, text, textLength, matchStartIndex, matchLength) + ) { + return m; + } + } while (m); + + return null; + } +} + +export function createFindMatch(range: Range, rawMatches: RegExpExecArray, captureMatches: boolean): FindMatch { + if (!captureMatches) { + return new FindMatch(range, null); + } + const matches: string[] = []; + for (let i = 0, len = rawMatches.length; i < len; i++) { + matches[i] = rawMatches[i]; + } + return new FindMatch(range, matches); +} diff --git a/packages/semi-json-viewer-core/src/pieceTreeTextBuffer/index.ts b/packages/semi-json-viewer-core/src/pieceTreeTextBuffer/index.ts new file mode 100644 index 0000000000..2822c6f633 --- /dev/null +++ b/packages/semi-json-viewer-core/src/pieceTreeTextBuffer/index.ts @@ -0,0 +1,2 @@ +export * from './pieceTreeBase'; +export * from './pieceTreeTextBufferBuilder'; diff --git a/packages/semi-json-viewer-core/src/pieceTreeTextBuffer/pieceTreeBase.ts b/packages/semi-json-viewer-core/src/pieceTreeTextBuffer/pieceTreeBase.ts new file mode 100644 index 0000000000..858888cd1a --- /dev/null +++ b/packages/semi-json-viewer-core/src/pieceTreeTextBuffer/pieceTreeBase.ts @@ -0,0 +1,2028 @@ +/** reference from https://github.com/microsoft/vscode */ + +import { CharCode } from '../common/charCode'; +import { Position } from '../common/position'; +import { Range } from '../common/range'; +import { FindMatch, ITextSnapshot, SearchData } from '../common/model'; +import { + NodeColor, + SENTINEL, + TreeNode, + fixInsert, + leftest, + rbDelete, + righttest, + updateTreeMetadata, +} from './rbTreeBase'; +import { Searcher, createFindMatch, isValidMatch } from '../model/textModelSearch'; + +// const lfRegex = new RegExp(/\r\n|\r|\n/g); +const AverageBufferSize = 65535; + +function createUintArray(arr: number[]): Uint32Array | Uint16Array { + let r; + if (arr[arr.length - 1] < 65536) { + r = new Uint16Array(arr.length); + } else { + r = new Uint32Array(arr.length); + } + r.set(arr, 0); + return r; +} + +class LineStarts { + constructor( + public readonly lineStarts: Uint32Array | Uint16Array | number[], + public readonly cr: number, + public readonly lf: number, + public readonly crlf: number, + public readonly isBasicASCII: boolean + ) {} +} + +export function createLineStartsFast(str: string, readonly: boolean = true): Uint32Array | Uint16Array | number[] { + const r: number[] = [0]; + let rLength = 1; + + for (let i = 0, len = str.length; i < len; i++) { + const chr = str.charCodeAt(i); + + if (chr === CharCode.CarriageReturn) { + if (i + 1 < len && str.charCodeAt(i + 1) === CharCode.LineFeed) { + // \r\n... case + r[rLength++] = i + 2; + i++; // skip \n + } else { + // \r... case + r[rLength++] = i + 1; + } + } else if (chr === CharCode.LineFeed) { + r[rLength++] = i + 1; + } + } + if (readonly) { + return createUintArray(r); + } else { + return r; + } +} + +export function createLineStarts(r: number[], str: string): LineStarts { + r.length = 0; + r[0] = 0; + let rLength = 1; + let cr = 0, + lf = 0, + crlf = 0; + let isBasicASCII = true; + for (let i = 0, len = str.length; i < len; i++) { + const chr = str.charCodeAt(i); + + if (chr === CharCode.CarriageReturn) { + if (i + 1 < len && str.charCodeAt(i + 1) === CharCode.LineFeed) { + // \r\n... case + crlf++; + r[rLength++] = i + 2; + i++; // skip \n + } else { + cr++; + // \r... case + r[rLength++] = i + 1; + } + } else if (chr === CharCode.LineFeed) { + lf++; + r[rLength++] = i + 1; + } else { + if (isBasicASCII) { + if (chr !== CharCode.Tab && (chr < 32 || chr > 126)) { + isBasicASCII = false; + } + } + } + } + const result = new LineStarts(createUintArray(r), cr, lf, crlf, isBasicASCII); + r.length = 0; + + return result; +} + +interface NodePosition { + /** + * Piece Index + */ + node: TreeNode; + /** + * remainder in current piece. + */ + remainder: number; + /** + * node start offset in document. + */ + nodeStartOffset: number +} + +export interface BufferCursor { + /** + * Line number in current buffer + */ + line: number; + /** + * Column number in current buffer + */ + column: number +} + +export class Piece { + readonly bufferIndex: number; + readonly start: BufferCursor; + readonly end: BufferCursor; + readonly length: number; + readonly lineFeedCnt: number; + + constructor(bufferIndex: number, start: BufferCursor, end: BufferCursor, lineFeedCnt: number, length: number) { + this.bufferIndex = bufferIndex; + this.start = start; + this.end = end; + this.lineFeedCnt = lineFeedCnt; + this.length = length; + } +} + +export class StringBuffer { + buffer: string; + lineStarts: Uint32Array | Uint16Array | number[]; + + constructor(buffer: string, lineStarts: Uint32Array | Uint16Array | number[]) { + this.buffer = buffer; + this.lineStarts = lineStarts; + } +} + +/** + * Readonly snapshot for piece tree. + * In a real multiple thread environment, to make snapshot reading always work correctly, we need to + * 1. Make TreeNode.piece immutable, then reading and writing can run in parallel. + * 2. TreeNode/Buffers normalization should not happen during snapshot reading. + */ +class PieceTreeSnapshot implements ITextSnapshot { + private readonly _pieces: Piece[]; + private _index: number; + private readonly _tree: PieceTreeBase; + private readonly _BOM: string; + + constructor(tree: PieceTreeBase, BOM: string) { + this._pieces = []; + this._tree = tree; + this._BOM = BOM; + this._index = 0; + if (tree.root !== SENTINEL) { + tree.iterate(tree.root, node => { + if (node !== SENTINEL) { + this._pieces.push(node.piece); + } + return true; + }); + } + } + + read(): string | null { + if (this._pieces.length === 0) { + if (this._index === 0) { + this._index++; + return this._BOM; + } else { + return null; + } + } + + if (this._index > this._pieces.length - 1) { + return null; + } + + if (this._index === 0) { + return this._BOM + this._tree.getPieceContent(this._pieces[this._index++]); + } + return this._tree.getPieceContent(this._pieces[this._index++]); + } +} + +interface CacheEntry { + node: TreeNode; + nodeStartOffset: number; + nodeStartLineNumber?: number +} + +class PieceTreeSearchCache { + private readonly _limit: number; + private _cache: CacheEntry[]; + + constructor(limit: number) { + this._limit = limit; + this._cache = []; + } + + public get(offset: number): CacheEntry | null { + for (let i = this._cache.length - 1; i >= 0; i--) { + const nodePos = this._cache[i]; + if (nodePos.nodeStartOffset <= offset && nodePos.nodeStartOffset + nodePos.node.piece.length >= offset) { + return nodePos; + } + } + return null; + } + + public get2( + lineNumber: number + ): { + node: TreeNode; + nodeStartOffset: number; + nodeStartLineNumber: number + } | null { + for (let i = this._cache.length - 1; i >= 0; i--) { + const nodePos = this._cache[i]; + if ( + nodePos.nodeStartLineNumber && + nodePos.nodeStartLineNumber < lineNumber && + nodePos.nodeStartLineNumber + nodePos.node.piece.lineFeedCnt >= lineNumber + ) { + return < + { + node: TreeNode; + nodeStartOffset: number; + nodeStartLineNumber: number + } + >nodePos; + } + } + return null; + } + + public set(nodePosition: CacheEntry) { + if (this._cache.length >= this._limit) { + this._cache.shift(); + } + this._cache.push(nodePosition); + } + + public validate(offset: number) { + let hasInvalidVal = false; + const tmp: Array = this._cache; + for (let i = 0; i < tmp.length; i++) { + const nodePos = tmp[i]!; + if (nodePos.node.parent === null || nodePos.nodeStartOffset >= offset) { + tmp[i] = null; + hasInvalidVal = true; + continue; + } + } + + if (hasInvalidVal) { + const newArr: CacheEntry[] = []; + for (const entry of tmp) { + if (entry !== null) { + newArr.push(entry); + } + } + + this._cache = newArr; + } + } +} + +export class PieceTreeBase { + root!: TreeNode; + protected _buffers!: StringBuffer[]; // 0 is change buffer, others are readonly original buffer. + protected _lineCnt!: number; + protected _length!: number; + protected _EOL!: '\r\n' | '\n'; + protected _EOLLength!: number; + protected _EOLNormalized!: boolean; + private _lastChangeBufferPos!: BufferCursor; + private _searchCache!: PieceTreeSearchCache; + private _lastVisitedLine!: { lineNumber: number; value: string }; + + constructor(chunks: StringBuffer[], eol: '\r\n' | '\n', eolNormalized: boolean) { + this.create(chunks, eol, eolNormalized); + } + + create(chunks: StringBuffer[], eol: '\r\n' | '\n', eolNormalized: boolean) { + this._buffers = [new StringBuffer('', [0])]; + this._lastChangeBufferPos = { line: 0, column: 0 }; + this.root = SENTINEL; + this._lineCnt = 1; + this._length = 0; + this._EOL = eol; + this._EOLLength = eol.length; + this._EOLNormalized = eolNormalized; + + let lastNode: TreeNode | null = null; + for (let i = 0, len = chunks.length; i < len; i++) { + if (chunks[i].buffer.length > 0) { + if (!chunks[i].lineStarts) { + chunks[i].lineStarts = createLineStartsFast(chunks[i].buffer); + } + + const piece = new Piece( + i + 1, + { line: 0, column: 0 }, + { + line: chunks[i].lineStarts.length - 1, + column: chunks[i].buffer.length - chunks[i].lineStarts[chunks[i].lineStarts.length - 1], + }, + chunks[i].lineStarts.length - 1, + chunks[i].buffer.length + ); + this._buffers.push(chunks[i]); + lastNode = this.rbInsertRight(lastNode, piece); + } + } + + this._searchCache = new PieceTreeSearchCache(1); + this._lastVisitedLine = { lineNumber: 0, value: '' }; + this.computeBufferMetadata(); + } + + normalizeEOL(eol: '\r\n' | '\n') { + const averageBufferSize = AverageBufferSize; + const min = averageBufferSize - Math.floor(averageBufferSize / 3); + const max = min * 2; + + let tempChunk = ''; + let tempChunkLen = 0; + const chunks: StringBuffer[] = []; + + this.iterate(this.root, node => { + const str = this.getNodeContent(node); + const len = str.length; + if (tempChunkLen <= min || tempChunkLen + len < max) { + tempChunk += str; + tempChunkLen += len; + return true; + } + + // flush anyways + const text = tempChunk.replace(/\r\n|\r|\n/g, eol); + chunks.push(new StringBuffer(text, createLineStartsFast(text))); + tempChunk = str; + tempChunkLen = len; + return true; + }); + + if (tempChunkLen > 0) { + const text = tempChunk.replace(/\r\n|\r|\n/g, eol); + chunks.push(new StringBuffer(text, createLineStartsFast(text))); + } + + this.create(chunks, eol, true); + } + + // #region Buffer API + public getEOL(): '\r\n' | '\n' { + return this._EOL; + } + + public setEOL(newEOL: '\r\n' | '\n'): void { + this._EOL = newEOL; + this._EOLLength = this._EOL.length; + this.normalizeEOL(newEOL); + } + + public createSnapshot(BOM: string): ITextSnapshot { + return new PieceTreeSnapshot(this, BOM); + } + + public equal(other: PieceTreeBase): boolean { + if (this.getLength() !== other.getLength()) { + return false; + } + if (this.getLineCount() !== other.getLineCount()) { + return false; + } + + let offset = 0; + const ret = this.iterate(this.root, node => { + if (node === SENTINEL) { + return true; + } + const str = this.getNodeContent(node); + const len = str.length; + const startPosition = other.nodeAt(offset); + const endPosition = other.nodeAt(offset + len); + const val = other.getValueInRange2(startPosition, endPosition); + + offset += len; + return str === val; + }); + + return ret; + } + + public getOffsetAt(lineNumber: number, column: number): number { + let leftLen = 0; // inorder + + let x = this.root; + + while (x !== SENTINEL) { + if (x.left !== SENTINEL && x.lf_left + 1 >= lineNumber) { + x = x.left; + } else if (x.lf_left + x.piece.lineFeedCnt + 1 >= lineNumber) { + leftLen += x.size_left; + // lineNumber >= 2 + const accumualtedValInCurrentIndex = this.getAccumulatedValue(x, lineNumber - x.lf_left - 2); + return (leftLen += accumualtedValInCurrentIndex + column - 1); + } else { + lineNumber -= x.lf_left + x.piece.lineFeedCnt; + leftLen += x.size_left + x.piece.length; + x = x.right; + } + } + + return leftLen; + } + + public getPositionAt(offset: number): Position { + offset = Math.floor(offset); + offset = Math.max(0, offset); + + let x = this.root; + let lfCnt = 0; + const originalOffset = offset; + + while (x !== SENTINEL) { + if (x.size_left !== 0 && x.size_left >= offset) { + x = x.left; + } else if (x.size_left + x.piece.length >= offset) { + const out = this.getIndexOf(x, offset - x.size_left); + + lfCnt += x.lf_left + out.index; + + if (out.index === 0) { + const lineStartOffset = this.getOffsetAt(lfCnt + 1, 1); + const column = originalOffset - lineStartOffset; + return new Position(lfCnt + 1, column + 1); + } + + return new Position(lfCnt + 1, out.remainder + 1); + } else { + offset -= x.size_left + x.piece.length; + lfCnt += x.lf_left + x.piece.lineFeedCnt; + + if (x.right === SENTINEL) { + // last node + const lineStartOffset = this.getOffsetAt(lfCnt + 1, 1); + const column = originalOffset - offset - lineStartOffset; + return new Position(lfCnt + 1, column + 1); + } else { + x = x.right; + } + } + } + + return new Position(1, 1); + } + + public getValueInRange(range: Range, eol?: string): string { + if (range.startLineNumber === range.endLineNumber && range.startColumn === range.endColumn) { + return ''; + } + + const startPosition = this.nodeAt2(range.startLineNumber, range.startColumn); + const endPosition = this.nodeAt2(range.endLineNumber, range.endColumn); + + const value = this.getValueInRange2(startPosition, endPosition); + if (eol) { + if (eol !== this._EOL || !this._EOLNormalized) { + return value.replace(/\r\n|\r|\n/g, eol); + } + + if (eol === this.getEOL() && this._EOLNormalized) { + // if (eol === '\r\n') {} + return value; + } + return value.replace(/\r\n|\r|\n/g, eol); + } + return value; + } + + public getValueInRange2(startPosition: NodePosition, endPosition: NodePosition): string { + if (startPosition.node === endPosition.node) { + const node = startPosition.node; + const buffer = this._buffers[node.piece.bufferIndex].buffer; + const startOffset = this.offsetInBuffer(node.piece.bufferIndex, node.piece.start); + return buffer.substring(startOffset + startPosition.remainder, startOffset + endPosition.remainder); + } + + let x = startPosition.node; + const buffer = this._buffers[x.piece.bufferIndex].buffer; + const startOffset = this.offsetInBuffer(x.piece.bufferIndex, x.piece.start); + let ret = buffer.substring(startOffset + startPosition.remainder, startOffset + x.piece.length); + + x = x.next(); + while (x !== SENTINEL) { + const buffer = this._buffers[x.piece.bufferIndex].buffer; + const startOffset = this.offsetInBuffer(x.piece.bufferIndex, x.piece.start); + + if (x === endPosition.node) { + ret += buffer.substring(startOffset, startOffset + endPosition.remainder); + break; + } else { + ret += buffer.substr(startOffset, x.piece.length); + } + + x = x.next(); + } + + return ret; + } + + public getLinesContent(): string[] { + const lines: string[] = []; + let linesLength = 0; + let currentLine = ''; + let danglingCR = false; + + this.iterate(this.root, node => { + if (node === SENTINEL) { + return true; + } + + const piece = node.piece; + let pieceLength = piece.length; + if (pieceLength === 0) { + return true; + } + + const buffer = this._buffers[piece.bufferIndex].buffer; + const lineStarts = this._buffers[piece.bufferIndex].lineStarts; + + const pieceStartLine = piece.start.line; + const pieceEndLine = piece.end.line; + let pieceStartOffset = lineStarts[pieceStartLine] + piece.start.column; + + if (danglingCR) { + if (buffer.charCodeAt(pieceStartOffset) === CharCode.LineFeed) { + // pretend the \n was in the previous piece.. + pieceStartOffset++; + pieceLength--; + } + lines[linesLength++] = currentLine; + currentLine = ''; + danglingCR = false; + if (pieceLength === 0) { + return true; + } + } + + if (pieceStartLine === pieceEndLine) { + // this piece has no new lines + if ( + !this._EOLNormalized && + buffer.charCodeAt(pieceStartOffset + pieceLength - 1) === CharCode.CarriageReturn + ) { + danglingCR = true; + currentLine += buffer.substr(pieceStartOffset, pieceLength - 1); + } else { + currentLine += buffer.substr(pieceStartOffset, pieceLength); + } + return true; + } + + // add the text before the first line start in this piece + currentLine += this._EOLNormalized + ? buffer.substring( + pieceStartOffset, + Math.max(pieceStartOffset, lineStarts[pieceStartLine + 1] - this._EOLLength) + ) + : buffer.substring(pieceStartOffset, lineStarts[pieceStartLine + 1]).replace(/(\r\n|\r|\n)$/, ''); + lines[linesLength++] = currentLine; + + for (let line = pieceStartLine + 1; line < pieceEndLine; line++) { + currentLine = this._EOLNormalized + ? buffer.substring(lineStarts[line], lineStarts[line + 1] - this._EOLLength) + : buffer.substring(lineStarts[line], lineStarts[line + 1]).replace(/(\r\n|\r|\n)$/, ''); + lines[linesLength++] = currentLine; + } + + if ( + !this._EOLNormalized && + buffer.charCodeAt(lineStarts[pieceEndLine] + piece.end.column - 1) === CharCode.CarriageReturn + ) { + danglingCR = true; + if (piece.end.column === 0) { + // The last line ended with a \r, let's undo the push, it will be pushed by next iteration + linesLength--; + } else { + currentLine = buffer.substr(lineStarts[pieceEndLine], piece.end.column - 1); + } + } else { + currentLine = buffer.substr(lineStarts[pieceEndLine], piece.end.column); + } + + return true; + }); + + if (danglingCR) { + lines[linesLength++] = currentLine; + currentLine = ''; + } + + lines[linesLength++] = currentLine; + return lines; + } + + public getLength(): number { + return this._length; + } + + public getLineCount(): number { + return this._lineCnt; + } + + public getLineContent(lineNumber: number): string { + if (this._lastVisitedLine.lineNumber === lineNumber) { + return this._lastVisitedLine.value; + } + + this._lastVisitedLine.lineNumber = lineNumber; + + if (lineNumber === this._lineCnt) { + this._lastVisitedLine.value = this.getLineRawContent(lineNumber); + } else if (this._EOLNormalized) { + this._lastVisitedLine.value = this.getLineRawContent(lineNumber, this._EOLLength); + } else { + this._lastVisitedLine.value = this.getLineRawContent(lineNumber).replace(/(\r\n|\r|\n)$/, ''); + } + + return this._lastVisitedLine.value; + } + + private _getCharCode(nodePos: NodePosition): number { + if (nodePos.remainder === nodePos.node.piece.length) { + // the char we want to fetch is at the head of next node. + const matchingNode = nodePos.node.next(); + if (!matchingNode) { + return 0; + } + + const buffer = this._buffers[matchingNode.piece.bufferIndex]; + const startOffset = this.offsetInBuffer(matchingNode.piece.bufferIndex, matchingNode.piece.start); + return buffer.buffer.charCodeAt(startOffset); + } else { + const buffer = this._buffers[nodePos.node.piece.bufferIndex]; + const startOffset = this.offsetInBuffer(nodePos.node.piece.bufferIndex, nodePos.node.piece.start); + const targetOffset = startOffset + nodePos.remainder; + + return buffer.buffer.charCodeAt(targetOffset); + } + } + + public getLineCharCode(lineNumber: number, index: number): number { + const nodePos = this.nodeAt2(lineNumber, index + 1); + return this._getCharCode(nodePos); + } + + public getLineLength(lineNumber: number): number { + if (lineNumber === this.getLineCount()) { + const startOffset = this.getOffsetAt(lineNumber, 1); + return this.getLength() - startOffset; + } + return this.getOffsetAt(lineNumber + 1, 1) - this.getOffsetAt(lineNumber, 1) - this._EOLLength; + } + + public getCharCode(offset: number): number { + const nodePos = this.nodeAt(offset); + return this._getCharCode(nodePos); + } + + public getNearestChunk(offset: number): string { + const nodePos = this.nodeAt(offset); + if (nodePos.remainder === nodePos.node.piece.length) { + // the offset is at the head of next node. + const matchingNode = nodePos.node.next(); + if (!matchingNode || matchingNode === SENTINEL) { + return ''; + } + + const buffer = this._buffers[matchingNode.piece.bufferIndex]; + const startOffset = this.offsetInBuffer(matchingNode.piece.bufferIndex, matchingNode.piece.start); + return buffer.buffer.substring(startOffset, startOffset + matchingNode.piece.length); + } else { + const buffer = this._buffers[nodePos.node.piece.bufferIndex]; + const startOffset = this.offsetInBuffer(nodePos.node.piece.bufferIndex, nodePos.node.piece.start); + const targetOffset = startOffset + nodePos.remainder; + const targetEnd = startOffset + nodePos.node.piece.length; + return buffer.buffer.substring(targetOffset, targetEnd); + } + } + + public findMatchesInNode( + node: TreeNode, + searcher: Searcher, + startLineNumber: number, + startColumn: number, + startCursor: BufferCursor, + endCursor: BufferCursor, + searchData: SearchData, + captureMatches: boolean, + limitResultCount: number, + resultLen: number, + result: FindMatch[] + ) { + const buffer = this._buffers[node.piece.bufferIndex]; + const startOffsetInBuffer = this.offsetInBuffer(node.piece.bufferIndex, node.piece.start); + const start = this.offsetInBuffer(node.piece.bufferIndex, startCursor); + const end = this.offsetInBuffer(node.piece.bufferIndex, endCursor); + + let m: RegExpExecArray | null; + // Reset regex to search from the beginning + const ret: BufferCursor = { line: 0, column: 0 }; + let searchText: string; + let offsetInBuffer: (offset: number) => number; + + if (searcher._wordSeparators) { + searchText = buffer.buffer.substring(start, end); + offsetInBuffer = (offset: number) => offset + start; + searcher.reset(0); + } else { + searchText = buffer.buffer; + offsetInBuffer = (offset: number) => offset; + searcher.reset(start); + } + + do { + m = searcher.next(searchText); + + if (m) { + if (offsetInBuffer(m.index) >= end) { + return resultLen; + } + this.positionInBuffer(node, offsetInBuffer(m.index) - startOffsetInBuffer, ret); + const lineFeedCnt = this.getLineFeedCnt(node.piece.bufferIndex, startCursor, ret); + const retStartColumn = + ret.line === startCursor.line ? ret.column - startCursor.column + startColumn : ret.column + 1; + const retEndColumn = retStartColumn + m[0].length; + result[resultLen++] = createFindMatch( + new Range( + startLineNumber + lineFeedCnt, + retStartColumn, + startLineNumber + lineFeedCnt, + retEndColumn + ), + m, + captureMatches + ); + + if (offsetInBuffer(m.index) + m[0].length >= end) { + return resultLen; + } + if (resultLen >= limitResultCount) { + return resultLen; + } + } + } while (m); + + return resultLen; + } + + public findMatchesLineByLine( + searchRange: Range, + searchData: SearchData, + captureMatches: boolean, + limitResultCount: number + ): FindMatch[] { + const result: FindMatch[] = []; + let resultLen = 0; + const searcher = new Searcher(searchData.wordSeparators, searchData.regex); + + let startPosition = this.nodeAt2(searchRange.startLineNumber, searchRange.startColumn); + if (startPosition === null) { + return []; + } + const endPosition = this.nodeAt2(searchRange.endLineNumber, searchRange.endColumn); + if (endPosition === null) { + return []; + } + let start = this.positionInBuffer(startPosition.node, startPosition.remainder); + const end = this.positionInBuffer(endPosition.node, endPosition.remainder); + + if (startPosition.node === endPosition.node) { + this.findMatchesInNode( + startPosition.node, + searcher, + searchRange.startLineNumber, + searchRange.startColumn, + start, + end, + searchData, + captureMatches, + limitResultCount, + resultLen, + result + ); + return result; + } + + let startLineNumber = searchRange.startLineNumber; + + let currentNode = startPosition.node; + while (currentNode !== endPosition.node) { + const lineBreakCnt = this.getLineFeedCnt(currentNode.piece.bufferIndex, start, currentNode.piece.end); + + if (lineBreakCnt >= 1) { + // last line break position + const lineStarts = this._buffers[currentNode.piece.bufferIndex].lineStarts; + const startOffsetInBuffer = this.offsetInBuffer(currentNode.piece.bufferIndex, currentNode.piece.start); + const nextLineStartOffset = lineStarts[start.line + lineBreakCnt]; + const startColumn = startLineNumber === searchRange.startLineNumber ? searchRange.startColumn : 1; + resultLen = this.findMatchesInNode( + currentNode, + searcher, + startLineNumber, + startColumn, + start, + this.positionInBuffer(currentNode, nextLineStartOffset - startOffsetInBuffer), + searchData, + captureMatches, + limitResultCount, + resultLen, + result + ); + + if (resultLen >= limitResultCount) { + return result; + } + + startLineNumber += lineBreakCnt; + } + + const startColumn = startLineNumber === searchRange.startLineNumber ? searchRange.startColumn - 1 : 0; + // search for the remaining content + if (startLineNumber === searchRange.endLineNumber) { + const text = this.getLineContent(startLineNumber).substring(startColumn, searchRange.endColumn - 1); + resultLen = this._findMatchesInLine( + searchData, + searcher, + text, + searchRange.endLineNumber, + startColumn, + resultLen, + result, + captureMatches, + limitResultCount + ); + return result; + } + + resultLen = this._findMatchesInLine( + searchData, + searcher, + this.getLineContent(startLineNumber).substr(startColumn), + startLineNumber, + startColumn, + resultLen, + result, + captureMatches, + limitResultCount + ); + + if (resultLen >= limitResultCount) { + return result; + } + + startLineNumber++; + startPosition = this.nodeAt2(startLineNumber, 1); + currentNode = startPosition.node; + start = this.positionInBuffer(startPosition.node, startPosition.remainder); + } + + if (startLineNumber === searchRange.endLineNumber) { + const startColumn = startLineNumber === searchRange.startLineNumber ? searchRange.startColumn - 1 : 0; + const text = this.getLineContent(startLineNumber).substring(startColumn, searchRange.endColumn - 1); + resultLen = this._findMatchesInLine( + searchData, + searcher, + text, + searchRange.endLineNumber, + startColumn, + resultLen, + result, + captureMatches, + limitResultCount + ); + return result; + } + + const startColumn = startLineNumber === searchRange.startLineNumber ? searchRange.startColumn : 1; + resultLen = this.findMatchesInNode( + endPosition.node, + searcher, + startLineNumber, + startColumn, + start, + end, + searchData, + captureMatches, + limitResultCount, + resultLen, + result + ); + return result; + } + + private _findMatchesInLine( + searchData: SearchData, + searcher: Searcher, + text: string, + lineNumber: number, + deltaOffset: number, + resultLen: number, + result: FindMatch[], + captureMatches: boolean, + limitResultCount: number + ): number { + const wordSeparators = searchData.wordSeparators; + if (!captureMatches && searchData.simpleSearch) { + const searchString = searchData.simpleSearch; + const searchStringLen = searchString.length; + const textLength = text.length; + + let lastMatchIndex = -searchStringLen; + while ((lastMatchIndex = text.indexOf(searchString, lastMatchIndex + searchStringLen)) !== -1) { + if ( + !wordSeparators || + isValidMatch(wordSeparators, text, textLength, lastMatchIndex, searchStringLen) + ) { + result[resultLen++] = new FindMatch( + new Range( + lineNumber, + lastMatchIndex + 1 + deltaOffset, + lineNumber, + lastMatchIndex + 1 + searchStringLen + deltaOffset + ), + null + ); + if (resultLen >= limitResultCount) { + return resultLen; + } + } + } + return resultLen; + } + + let m: RegExpExecArray | null; + // Reset regex to search from the beginning + searcher.reset(0); + do { + m = searcher.next(text); + if (m) { + result[resultLen++] = createFindMatch( + new Range( + lineNumber, + m.index + 1 + deltaOffset, + lineNumber, + m.index + 1 + m[0].length + deltaOffset + ), + m, + captureMatches + ); + if (resultLen >= limitResultCount) { + return resultLen; + } + } + } while (m); + return resultLen; + } + + // #endregion + + // #region Piece Table + public insert(offset: number, value: string, eolNormalized: boolean = false): void { + this._EOLNormalized = this._EOLNormalized && eolNormalized; + this._lastVisitedLine.lineNumber = 0; + this._lastVisitedLine.value = ''; + + if (this.root !== SENTINEL) { + const { node, remainder, nodeStartOffset } = this.nodeAt(offset); + const piece = node.piece; + const bufferIndex = piece.bufferIndex; + const insertPosInBuffer = this.positionInBuffer(node, remainder); + if ( + node.piece.bufferIndex === 0 && + piece.end.line === this._lastChangeBufferPos.line && + piece.end.column === this._lastChangeBufferPos.column && + nodeStartOffset + piece.length === offset && + value.length < AverageBufferSize + ) { + // changed buffer + this.appendToNode(node, value); + this.computeBufferMetadata(); + return; + } + + if (nodeStartOffset === offset) { + this.insertContentToNodeLeft(value, node); + this._searchCache.validate(offset); + } else if (nodeStartOffset + node.piece.length > offset) { + // we are inserting into the middle of a node. + const nodesToDel: TreeNode[] = []; + let newRightPiece = new Piece( + piece.bufferIndex, + insertPosInBuffer, + piece.end, + this.getLineFeedCnt(piece.bufferIndex, insertPosInBuffer, piece.end), + this.offsetInBuffer(bufferIndex, piece.end) - this.offsetInBuffer(bufferIndex, insertPosInBuffer) + ); + + if (this.shouldCheckCRLF() && this.endWithCR(value)) { + const headOfRight = this.nodeCharCodeAt(node, remainder); + + if (headOfRight === 10 /** \n */) { + const newStart: BufferCursor = { + line: newRightPiece.start.line + 1, + column: 0, + }; + newRightPiece = new Piece( + newRightPiece.bufferIndex, + newStart, + newRightPiece.end, + this.getLineFeedCnt(newRightPiece.bufferIndex, newStart, newRightPiece.end), + newRightPiece.length - 1 + ); + + value += '\n'; + } + } + + // reuse node for content before insertion point. + if (this.shouldCheckCRLF() && this.startWithLF(value)) { + const tailOfLeft = this.nodeCharCodeAt(node, remainder - 1); + if (tailOfLeft === 13 /** \r */) { + const previousPos = this.positionInBuffer(node, remainder - 1); + this.deleteNodeTail(node, previousPos); + value = '\r' + value; + + if (node.piece.length === 0) { + nodesToDel.push(node); + } + } else { + this.deleteNodeTail(node, insertPosInBuffer); + } + } else { + this.deleteNodeTail(node, insertPosInBuffer); + } + + const newPieces = this.createNewPieces(value); + if (newRightPiece.length > 0) { + this.rbInsertRight(node, newRightPiece); + } + + let tmpNode = node; + for (let k = 0; k < newPieces.length; k++) { + tmpNode = this.rbInsertRight(tmpNode, newPieces[k]); + } + this.deleteNodes(nodesToDel); + } else { + this.insertContentToNodeRight(value, node); + } + } else { + // insert new node + const pieces = this.createNewPieces(value); + let node = this.rbInsertLeft(null, pieces[0]); + + for (let k = 1; k < pieces.length; k++) { + node = this.rbInsertRight(node, pieces[k]); + } + } + + // todo, this is too brutal. Total line feed count should be updated the same way as lf_left. + this.computeBufferMetadata(); + } + + public delete(offset: number, cnt: number): void { + this._lastVisitedLine.lineNumber = 0; + this._lastVisitedLine.value = ''; + + if (cnt <= 0 || this.root === SENTINEL) { + return; + } + + const startPosition = this.nodeAt(offset); + const endPosition = this.nodeAt(offset + cnt); + const startNode = startPosition.node; + const endNode = endPosition.node; + + if (startNode === endNode) { + const startSplitPosInBuffer = this.positionInBuffer(startNode, startPosition.remainder); + const endSplitPosInBuffer = this.positionInBuffer(startNode, endPosition.remainder); + + if (startPosition.nodeStartOffset === offset) { + if (cnt === startNode.piece.length) { + // delete node + const next = startNode.next(); + rbDelete(this, startNode); + this.validateCRLFWithPrevNode(next); + this.computeBufferMetadata(); + return; + } + this.deleteNodeHead(startNode, endSplitPosInBuffer); + this._searchCache.validate(offset); + this.validateCRLFWithPrevNode(startNode); + this.computeBufferMetadata(); + return; + } + + if (startPosition.nodeStartOffset + startNode.piece.length === offset + cnt) { + this.deleteNodeTail(startNode, startSplitPosInBuffer); + this.validateCRLFWithNextNode(startNode); + this.computeBufferMetadata(); + return; + } + + // delete content in the middle, this node will be splitted to nodes + this.shrinkNode(startNode, startSplitPosInBuffer, endSplitPosInBuffer); + this.computeBufferMetadata(); + return; + } + + const nodesToDel: TreeNode[] = []; + + const startSplitPosInBuffer = this.positionInBuffer(startNode, startPosition.remainder); + this.deleteNodeTail(startNode, startSplitPosInBuffer); + this._searchCache.validate(offset); + if (startNode.piece.length === 0) { + nodesToDel.push(startNode); + } + + // update last touched node + const endSplitPosInBuffer = this.positionInBuffer(endNode, endPosition.remainder); + this.deleteNodeHead(endNode, endSplitPosInBuffer); + if (endNode.piece.length === 0) { + nodesToDel.push(endNode); + } + + // delete nodes in between + const secondNode = startNode.next(); + for (let node = secondNode; node !== SENTINEL && node !== endNode; node = node.next()) { + nodesToDel.push(node); + } + + const prev = startNode.piece.length === 0 ? startNode.prev() : startNode; + this.deleteNodes(nodesToDel); + this.validateCRLFWithNextNode(prev); + this.computeBufferMetadata(); + } + + private insertContentToNodeLeft(value: string, node: TreeNode) { + // we are inserting content to the beginning of node + const nodesToDel: TreeNode[] = []; + if (this.shouldCheckCRLF() && this.endWithCR(value) && this.startWithLF(node)) { + // move `\n` to new node. + + const piece = node.piece; + const newStart: BufferCursor = { line: piece.start.line + 1, column: 0 }; + const nPiece = new Piece( + piece.bufferIndex, + newStart, + piece.end, + this.getLineFeedCnt(piece.bufferIndex, newStart, piece.end), + piece.length - 1 + ); + + node.piece = nPiece; + + value += '\n'; + updateTreeMetadata(this, node, -1, -1); + + if (node.piece.length === 0) { + nodesToDel.push(node); + } + } + + const newPieces = this.createNewPieces(value); + let newNode = this.rbInsertLeft(node, newPieces[newPieces.length - 1]); + for (let k = newPieces.length - 2; k >= 0; k--) { + newNode = this.rbInsertLeft(newNode, newPieces[k]); + } + this.validateCRLFWithPrevNode(newNode); + this.deleteNodes(nodesToDel); + } + + private insertContentToNodeRight(value: string, node: TreeNode) { + // we are inserting to the right of this node. + if (this.adjustCarriageReturnFromNext(value, node)) { + // move \n to the new node. + value += '\n'; + } + + const newPieces = this.createNewPieces(value); + const newNode = this.rbInsertRight(node, newPieces[0]); + let tmpNode = newNode; + + for (let k = 1; k < newPieces.length; k++) { + tmpNode = this.rbInsertRight(tmpNode, newPieces[k]); + } + + this.validateCRLFWithPrevNode(newNode); + } + + private positionInBuffer(node: TreeNode, remainder: number): BufferCursor; + private positionInBuffer(node: TreeNode, remainder: number, ret: BufferCursor): null; + private positionInBuffer(node: TreeNode, remainder: number, ret?: BufferCursor): BufferCursor | null { + const piece = node.piece; + const bufferIndex = node.piece.bufferIndex; + const lineStarts = this._buffers[bufferIndex].lineStarts; + + const startOffset = lineStarts[piece.start.line] + piece.start.column; + + const offset = startOffset + remainder; + + // binary search offset between startOffset and endOffset + let low = piece.start.line; + let high = piece.end.line; + + let mid: number = 0; + let midStop: number = 0; + let midStart: number = 0; + + while (low <= high) { + mid = (low + (high - low) / 2) | 0; + midStart = lineStarts[mid]; + + if (mid === high) { + break; + } + + midStop = lineStarts[mid + 1]; + + if (offset < midStart) { + high = mid - 1; + } else if (offset >= midStop) { + low = mid + 1; + } else { + break; + } + } + + if (ret) { + ret.line = mid; + ret.column = offset - midStart; + return null; + } + + return { + line: mid, + column: offset - midStart, + }; + } + + private getLineFeedCnt(bufferIndex: number, start: BufferCursor, end: BufferCursor): number { + // we don't need to worry about start: abc\r|\n, or abc|\r, or abc|\n, or abc|\r\n doesn't change the fact that, there is one line break after start. + // now let's take care of end: abc\r|\n, if end is in between \r and \n, we need to add line feed count by 1 + if (end.column === 0) { + return end.line - start.line; + } + + const lineStarts = this._buffers[bufferIndex].lineStarts; + if (end.line === lineStarts.length - 1) { + // it means, there is no \n after end, otherwise, there will be one more lineStart. + return end.line - start.line; + } + + const nextLineStartOffset = lineStarts[end.line + 1]; + const endOffset = lineStarts[end.line] + end.column; + if (nextLineStartOffset > endOffset + 1) { + // there are more than 1 character after end, which means it can't be \n + return end.line - start.line; + } + // endOffset + 1 === nextLineStartOffset + // character at endOffset is \n, so we check the character before first + // if character at endOffset is \r, end.column is 0 and we can't get here. + const previousCharOffset = endOffset - 1; // end.column > 0 so it's okay. + const buffer = this._buffers[bufferIndex].buffer; + + if (buffer.charCodeAt(previousCharOffset) === 13) { + return end.line - start.line + 1; + } else { + return end.line - start.line; + } + } + + private offsetInBuffer(bufferIndex: number, cursor: BufferCursor): number { + const lineStarts = this._buffers[bufferIndex].lineStarts; + return lineStarts[cursor.line] + cursor.column; + } + + private deleteNodes(nodes: TreeNode[]): void { + for (let i = 0; i < nodes.length; i++) { + rbDelete(this, nodes[i]); + } + } + + private createNewPieces(text: string): Piece[] { + if (text.length > AverageBufferSize) { + // the content is large, operations like substring, charCode becomes slow + // so here we split it into smaller chunks, just like what we did for CR/LF normalization + const newPieces: Piece[] = []; + while (text.length > AverageBufferSize) { + const lastChar = text.charCodeAt(AverageBufferSize - 1); + let splitText; + if (lastChar === CharCode.CarriageReturn || (lastChar >= 0xd800 && lastChar <= 0xdbff)) { + // last character is \r or a high surrogate => keep it back + splitText = text.substring(0, AverageBufferSize - 1); + text = text.substring(AverageBufferSize - 1); + } else { + splitText = text.substring(0, AverageBufferSize); + text = text.substring(AverageBufferSize); + } + + const lineStarts = createLineStartsFast(splitText); + newPieces.push( + new Piece( + this._buffers.length /* buffer index */, + { line: 0, column: 0 }, + { + line: lineStarts.length - 1, + column: splitText.length - lineStarts[lineStarts.length - 1], + }, + lineStarts.length - 1, + splitText.length + ) + ); + this._buffers.push(new StringBuffer(splitText, lineStarts)); + } + + const lineStarts = createLineStartsFast(text); + newPieces.push( + new Piece( + this._buffers.length /* buffer index */, + { line: 0, column: 0 }, + { + line: lineStarts.length - 1, + column: text.length - lineStarts[lineStarts.length - 1], + }, + lineStarts.length - 1, + text.length + ) + ); + this._buffers.push(new StringBuffer(text, lineStarts)); + + return newPieces; + } + + let startOffset = this._buffers[0].buffer.length; + const lineStarts = createLineStartsFast(text, false); + + let start = this._lastChangeBufferPos; + if ( + this._buffers[0].lineStarts[this._buffers[0].lineStarts.length - 1] === startOffset && + startOffset !== 0 && + this.startWithLF(text) && + this.endWithCR(this._buffers[0].buffer) // todo, we can check this._lastChangeBufferPos's column as it's the last one + ) { + this._lastChangeBufferPos = { + line: this._lastChangeBufferPos.line, + column: this._lastChangeBufferPos.column + 1, + }; + start = this._lastChangeBufferPos; + + for (let i = 0; i < lineStarts.length; i++) { + lineStarts[i] += startOffset + 1; + } + + this._buffers[0].lineStarts = ( this._buffers[0].lineStarts).concat(lineStarts.slice(1)); + this._buffers[0].buffer += '_' + text; + startOffset += 1; + } else { + if (startOffset !== 0) { + for (let i = 0; i < lineStarts.length; i++) { + lineStarts[i] += startOffset; + } + } + this._buffers[0].lineStarts = ( this._buffers[0].lineStarts).concat(lineStarts.slice(1)); + this._buffers[0].buffer += text; + } + + const endOffset = this._buffers[0].buffer.length; + const endIndex = this._buffers[0].lineStarts.length - 1; + const endColumn = endOffset - this._buffers[0].lineStarts[endIndex]; + const endPos = { line: endIndex, column: endColumn }; + const newPiece = new Piece( + 0 /** todo@peng */, + start, + endPos, + this.getLineFeedCnt(0, start, endPos), + endOffset - startOffset + ); + this._lastChangeBufferPos = endPos; + return [newPiece]; + } + + public getLinesRawContent(): string { + return this.getContentOfSubTree(this.root); + } + + public getLineRawContent(lineNumber: number, endOffset: number = 0): string { + let x = this.root; + + let ret = ''; + const cache = this._searchCache.get2(lineNumber); + if (cache) { + x = cache.node; + const prevAccumulatedValue = this.getAccumulatedValue(x, lineNumber - cache.nodeStartLineNumber - 1); + const buffer = this._buffers[x.piece.bufferIndex].buffer; + const startOffset = this.offsetInBuffer(x.piece.bufferIndex, x.piece.start); + if (cache.nodeStartLineNumber + x.piece.lineFeedCnt === lineNumber) { + ret = buffer.substring(startOffset + prevAccumulatedValue, startOffset + x.piece.length); + } else { + const accumulatedValue = this.getAccumulatedValue(x, lineNumber - cache.nodeStartLineNumber); + return buffer.substring(startOffset + prevAccumulatedValue, startOffset + accumulatedValue - endOffset); + } + } else { + let nodeStartOffset = 0; + const originalLineNumber = lineNumber; + while (x !== SENTINEL) { + if (x.left !== SENTINEL && x.lf_left >= lineNumber - 1) { + x = x.left; + } else if (x.lf_left + x.piece.lineFeedCnt > lineNumber - 1) { + const prevAccumulatedValue = this.getAccumulatedValue(x, lineNumber - x.lf_left - 2); + const accumulatedValue = this.getAccumulatedValue(x, lineNumber - x.lf_left - 1); + const buffer = this._buffers[x.piece.bufferIndex].buffer; + const startOffset = this.offsetInBuffer(x.piece.bufferIndex, x.piece.start); + nodeStartOffset += x.size_left; + this._searchCache.set({ + node: x, + nodeStartOffset, + nodeStartLineNumber: originalLineNumber - (lineNumber - 1 - x.lf_left), + }); + + return buffer.substring( + startOffset + prevAccumulatedValue, + startOffset + accumulatedValue - endOffset + ); + } else if (x.lf_left + x.piece.lineFeedCnt === lineNumber - 1) { + const prevAccumulatedValue = this.getAccumulatedValue(x, lineNumber - x.lf_left - 2); + const buffer = this._buffers[x.piece.bufferIndex].buffer; + const startOffset = this.offsetInBuffer(x.piece.bufferIndex, x.piece.start); + + ret = buffer.substring(startOffset + prevAccumulatedValue, startOffset + x.piece.length); + break; + } else { + lineNumber -= x.lf_left + x.piece.lineFeedCnt; + nodeStartOffset += x.size_left + x.piece.length; + x = x.right; + } + } + } + + // search in order, to find the node contains end column + x = x.next(); + while (x !== SENTINEL) { + const buffer = this._buffers[x.piece.bufferIndex].buffer; + + if (x.piece.lineFeedCnt > 0) { + const accumulatedValue = this.getAccumulatedValue(x, 0); + const startOffset = this.offsetInBuffer(x.piece.bufferIndex, x.piece.start); + + ret += buffer.substring(startOffset, startOffset + accumulatedValue - endOffset); + return ret; + } else { + const startOffset = this.offsetInBuffer(x.piece.bufferIndex, x.piece.start); + ret += buffer.substr(startOffset, x.piece.length); + } + + x = x.next(); + } + + return ret; + } + + private computeBufferMetadata() { + let x = this.root; + + let lfCnt = 1; + let len = 0; + + while (x !== SENTINEL) { + lfCnt += x.lf_left + x.piece.lineFeedCnt; + len += x.size_left + x.piece.length; + x = x.right; + } + + this._lineCnt = lfCnt; + this._length = len; + this._searchCache.validate(this._length); + } + + // #region node operations + private getIndexOf(node: TreeNode, accumulatedValue: number): { index: number; remainder: number } { + const piece = node.piece; + const pos = this.positionInBuffer(node, accumulatedValue); + const lineCnt = pos.line - piece.start.line; + + if ( + this.offsetInBuffer(piece.bufferIndex, piece.end) - this.offsetInBuffer(piece.bufferIndex, piece.start) === + accumulatedValue + ) { + // we are checking the end of this node, so a CRLF check is necessary. + const realLineCnt = this.getLineFeedCnt(node.piece.bufferIndex, piece.start, pos); + if (realLineCnt !== lineCnt) { + // aha yes, CRLF + return { index: realLineCnt, remainder: 0 }; + } + } + + return { index: lineCnt, remainder: pos.column }; + } + + private getAccumulatedValue(node: TreeNode, index: number) { + if (index < 0) { + return 0; + } + const piece = node.piece; + const lineStarts = this._buffers[piece.bufferIndex].lineStarts; + const expectedLineStartIndex = piece.start.line + index + 1; + if (expectedLineStartIndex > piece.end.line) { + return lineStarts[piece.end.line] + piece.end.column - lineStarts[piece.start.line] - piece.start.column; + } else { + return lineStarts[expectedLineStartIndex] - lineStarts[piece.start.line] - piece.start.column; + } + } + + private deleteNodeTail(node: TreeNode, pos: BufferCursor) { + const piece = node.piece; + const originalLFCnt = piece.lineFeedCnt; + const originalEndOffset = this.offsetInBuffer(piece.bufferIndex, piece.end); + + const newEnd = pos; + const newEndOffset = this.offsetInBuffer(piece.bufferIndex, newEnd); + const newLineFeedCnt = this.getLineFeedCnt(piece.bufferIndex, piece.start, newEnd); + + const lf_delta = newLineFeedCnt - originalLFCnt; + const size_delta = newEndOffset - originalEndOffset; + const newLength = piece.length + size_delta; + + node.piece = new Piece(piece.bufferIndex, piece.start, newEnd, newLineFeedCnt, newLength); + + updateTreeMetadata(this, node, size_delta, lf_delta); + } + + private deleteNodeHead(node: TreeNode, pos: BufferCursor) { + const piece = node.piece; + const originalLFCnt = piece.lineFeedCnt; + const originalStartOffset = this.offsetInBuffer(piece.bufferIndex, piece.start); + + const newStart = pos; + const newLineFeedCnt = this.getLineFeedCnt(piece.bufferIndex, newStart, piece.end); + const newStartOffset = this.offsetInBuffer(piece.bufferIndex, newStart); + const lf_delta = newLineFeedCnt - originalLFCnt; + const size_delta = originalStartOffset - newStartOffset; + const newLength = piece.length + size_delta; + node.piece = new Piece(piece.bufferIndex, newStart, piece.end, newLineFeedCnt, newLength); + + updateTreeMetadata(this, node, size_delta, lf_delta); + } + + private shrinkNode(node: TreeNode, start: BufferCursor, end: BufferCursor) { + const piece = node.piece; + const originalStartPos = piece.start; + const originalEndPos = piece.end; + + // old piece, originalStartPos, start + const oldLength = piece.length; + const oldLFCnt = piece.lineFeedCnt; + const newEnd = start; + const newLineFeedCnt = this.getLineFeedCnt(piece.bufferIndex, piece.start, newEnd); + const newLength = + this.offsetInBuffer(piece.bufferIndex, start) - this.offsetInBuffer(piece.bufferIndex, originalStartPos); + + node.piece = new Piece(piece.bufferIndex, piece.start, newEnd, newLineFeedCnt, newLength); + + updateTreeMetadata(this, node, newLength - oldLength, newLineFeedCnt - oldLFCnt); + + // new right piece, end, originalEndPos + const newPiece = new Piece( + piece.bufferIndex, + end, + originalEndPos, + this.getLineFeedCnt(piece.bufferIndex, end, originalEndPos), + this.offsetInBuffer(piece.bufferIndex, originalEndPos) - this.offsetInBuffer(piece.bufferIndex, end) + ); + + const newNode = this.rbInsertRight(node, newPiece); + this.validateCRLFWithPrevNode(newNode); + } + + private appendToNode(node: TreeNode, value: string): void { + if (this.adjustCarriageReturnFromNext(value, node)) { + value += '\n'; + } + + const hitCRLF = this.shouldCheckCRLF() && this.startWithLF(value) && this.endWithCR(node); + const startOffset = this._buffers[0].buffer.length; + this._buffers[0].buffer += value; + const lineStarts = createLineStartsFast(value, false); + for (let i = 0; i < lineStarts.length; i++) { + lineStarts[i] += startOffset; + } + if (hitCRLF) { + const prevStartOffset = this._buffers[0].lineStarts[this._buffers[0].lineStarts.length - 2]; + ( this._buffers[0].lineStarts).pop(); + // _lastChangeBufferPos is already wrong + this._lastChangeBufferPos = { + line: this._lastChangeBufferPos.line - 1, + column: startOffset - prevStartOffset, + }; + } + + this._buffers[0].lineStarts = ( this._buffers[0].lineStarts).concat(lineStarts.slice(1)); + const endIndex = this._buffers[0].lineStarts.length - 1; + const endColumn = this._buffers[0].buffer.length - this._buffers[0].lineStarts[endIndex]; + const newEnd = { line: endIndex, column: endColumn }; + const newLength = node.piece.length + value.length; + const oldLineFeedCnt = node.piece.lineFeedCnt; + const newLineFeedCnt = this.getLineFeedCnt(0, node.piece.start, newEnd); + const lf_delta = newLineFeedCnt - oldLineFeedCnt; + + node.piece = new Piece(node.piece.bufferIndex, node.piece.start, newEnd, newLineFeedCnt, newLength); + + this._lastChangeBufferPos = newEnd; + updateTreeMetadata(this, node, value.length, lf_delta); + } + + private nodeAt(offset: number): NodePosition { + let x = this.root; + const cache = this._searchCache.get(offset); + if (cache) { + return { + node: cache.node, + nodeStartOffset: cache.nodeStartOffset, + remainder: offset - cache.nodeStartOffset, + }; + } + + let nodeStartOffset = 0; + + while (x !== SENTINEL) { + if (x.size_left > offset) { + x = x.left; + } else if (x.size_left + x.piece.length >= offset) { + nodeStartOffset += x.size_left; + const ret = { + node: x, + remainder: offset - x.size_left, + nodeStartOffset, + }; + this._searchCache.set(ret); + return ret; + } else { + offset -= x.size_left + x.piece.length; + nodeStartOffset += x.size_left + x.piece.length; + x = x.right; + } + } + + return null!; + } + + private nodeAt2(lineNumber: number, column: number): NodePosition { + let x = this.root; + let nodeStartOffset = 0; + + while (x !== SENTINEL) { + if (x.left !== SENTINEL && x.lf_left >= lineNumber - 1) { + x = x.left; + } else if (x.lf_left + x.piece.lineFeedCnt > lineNumber - 1) { + const prevAccumualtedValue = this.getAccumulatedValue(x, lineNumber - x.lf_left - 2); + const accumulatedValue = this.getAccumulatedValue(x, lineNumber - x.lf_left - 1); + nodeStartOffset += x.size_left; + + return { + node: x, + remainder: Math.min(prevAccumualtedValue + column - 1, accumulatedValue), + nodeStartOffset, + }; + } else if (x.lf_left + x.piece.lineFeedCnt === lineNumber - 1) { + const prevAccumualtedValue = this.getAccumulatedValue(x, lineNumber - x.lf_left - 2); + if (prevAccumualtedValue + column - 1 <= x.piece.length) { + return { + node: x, + remainder: prevAccumualtedValue + column - 1, + nodeStartOffset, + }; + } else { + column -= x.piece.length - prevAccumualtedValue; + break; + } + } else { + lineNumber -= x.lf_left + x.piece.lineFeedCnt; + nodeStartOffset += x.size_left + x.piece.length; + x = x.right; + } + } + + // search in order, to find the node contains position.column + x = x.next(); + while (x !== SENTINEL) { + if (x.piece.lineFeedCnt > 0) { + const accumulatedValue = this.getAccumulatedValue(x, 0); + const nodeStartOffset = this.offsetOfNode(x); + return { + node: x, + remainder: Math.min(column - 1, accumulatedValue), + nodeStartOffset, + }; + } else { + if (x.piece.length >= column - 1) { + const nodeStartOffset = this.offsetOfNode(x); + return { + node: x, + remainder: column - 1, + nodeStartOffset, + }; + } else { + column -= x.piece.length; + } + } + + x = x.next(); + } + + return null!; + } + + private nodeCharCodeAt(node: TreeNode, offset: number): number { + if (node.piece.lineFeedCnt < 1) { + return -1; + } + const buffer = this._buffers[node.piece.bufferIndex]; + const newOffset = this.offsetInBuffer(node.piece.bufferIndex, node.piece.start) + offset; + return buffer.buffer.charCodeAt(newOffset); + } + + private offsetOfNode(node: TreeNode): number { + if (!node) { + return 0; + } + let pos = node.size_left; + while (node !== this.root) { + if (node.parent.right === node) { + pos += node.parent.size_left + node.parent.piece.length; + } + + node = node.parent; + } + + return pos; + } + + // #endregion + + // #region CRLF + private shouldCheckCRLF() { + return !(this._EOLNormalized && this._EOL === '\n'); + } + + private startWithLF(val: string | TreeNode): boolean { + if (typeof val === 'string') { + return val.charCodeAt(0) === 10; + } + + if (val === SENTINEL || val.piece.lineFeedCnt === 0) { + return false; + } + + const piece = val.piece; + const lineStarts = this._buffers[piece.bufferIndex].lineStarts; + const line = piece.start.line; + const startOffset = lineStarts[line] + piece.start.column; + if (line === lineStarts.length - 1) { + // last line, so there is no line feed at the end of this line + return false; + } + const nextLineOffset = lineStarts[line + 1]; + if (nextLineOffset > startOffset + 1) { + return false; + } + return this._buffers[piece.bufferIndex].buffer.charCodeAt(startOffset) === 10; + } + + private endWithCR(val: string | TreeNode): boolean { + if (typeof val === 'string') { + return val.charCodeAt(val.length - 1) === 13; + } + + if (val === SENTINEL || val.piece.lineFeedCnt === 0) { + return false; + } + + return this.nodeCharCodeAt(val, val.piece.length - 1) === 13; + } + + private validateCRLFWithPrevNode(nextNode: TreeNode) { + if (this.shouldCheckCRLF() && this.startWithLF(nextNode)) { + const node = nextNode.prev(); + if (this.endWithCR(node)) { + this.fixCRLF(node, nextNode); + } + } + } + + private validateCRLFWithNextNode(node: TreeNode) { + if (this.shouldCheckCRLF() && this.endWithCR(node)) { + const nextNode = node.next(); + if (this.startWithLF(nextNode)) { + this.fixCRLF(node, nextNode); + } + } + } + + private fixCRLF(prev: TreeNode, next: TreeNode) { + const nodesToDel: TreeNode[] = []; + // update node + const lineStarts = this._buffers[prev.piece.bufferIndex].lineStarts; + let newEnd: BufferCursor; + if (prev.piece.end.column === 0) { + // it means, last line ends with \r, not \r\n + newEnd = { + line: prev.piece.end.line - 1, + column: lineStarts[prev.piece.end.line] - lineStarts[prev.piece.end.line - 1] - 1, + }; + } else { + // \r\n + newEnd = { line: prev.piece.end.line, column: prev.piece.end.column - 1 }; + } + + const prevNewLength = prev.piece.length - 1; + const prevNewLFCnt = prev.piece.lineFeedCnt - 1; + prev.piece = new Piece(prev.piece.bufferIndex, prev.piece.start, newEnd, prevNewLFCnt, prevNewLength); + + updateTreeMetadata(this, prev, -1, -1); + if (prev.piece.length === 0) { + nodesToDel.push(prev); + } + + // update nextNode + const newStart: BufferCursor = { + line: next.piece.start.line + 1, + column: 0, + }; + const newLength = next.piece.length - 1; + const newLineFeedCnt = this.getLineFeedCnt(next.piece.bufferIndex, newStart, next.piece.end); + next.piece = new Piece(next.piece.bufferIndex, newStart, next.piece.end, newLineFeedCnt, newLength); + + updateTreeMetadata(this, next, -1, -1); + if (next.piece.length === 0) { + nodesToDel.push(next); + } + + // create new piece which contains \r\n + const pieces = this.createNewPieces('\r\n'); + this.rbInsertRight(prev, pieces[0]); + // delete empty nodes + + for (let i = 0; i < nodesToDel.length; i++) { + rbDelete(this, nodesToDel[i]); + } + } + + private adjustCarriageReturnFromNext(value: string, node: TreeNode): boolean { + if (this.shouldCheckCRLF() && this.endWithCR(value)) { + const nextNode = node.next(); + if (this.startWithLF(nextNode)) { + // move `\n` forward + value += '\n'; + + if (nextNode.piece.length === 1) { + rbDelete(this, nextNode); + } else { + const piece = nextNode.piece; + const newStart: BufferCursor = { + line: piece.start.line + 1, + column: 0, + }; + const newLength = piece.length - 1; + const newLineFeedCnt = this.getLineFeedCnt(piece.bufferIndex, newStart, piece.end); + nextNode.piece = new Piece(piece.bufferIndex, newStart, piece.end, newLineFeedCnt, newLength); + + updateTreeMetadata(this, nextNode, -1, -1); + } + return true; + } + } + + return false; + } + + // #endregion + + // #endregion + + // #region Tree operations + iterate(node: TreeNode, callback: (node: TreeNode) => boolean): boolean { + if (node === SENTINEL) { + return callback(SENTINEL); + } + + const leftRet = this.iterate(node.left, callback); + if (!leftRet) { + return leftRet; + } + + return callback(node) && this.iterate(node.right, callback); + } + + private getNodeContent(node: TreeNode) { + if (node === SENTINEL) { + return ''; + } + const buffer = this._buffers[node.piece.bufferIndex]; + const piece = node.piece; + const startOffset = this.offsetInBuffer(piece.bufferIndex, piece.start); + const endOffset = this.offsetInBuffer(piece.bufferIndex, piece.end); + const currentContent = buffer.buffer.substring(startOffset, endOffset); + return currentContent; + } + + getPieceContent(piece: Piece) { + const buffer = this._buffers[piece.bufferIndex]; + const startOffset = this.offsetInBuffer(piece.bufferIndex, piece.start); + const endOffset = this.offsetInBuffer(piece.bufferIndex, piece.end); + const currentContent = buffer.buffer.substring(startOffset, endOffset); + return currentContent; + } + + /** + * node node + * / \ / \ + * a b <---- a b + * / + * z + */ + private rbInsertRight(node: TreeNode | null, p: Piece): TreeNode { + const z = new TreeNode(p, NodeColor.Red); + z.left = SENTINEL; + z.right = SENTINEL; + z.parent = SENTINEL; + z.size_left = 0; + z.lf_left = 0; + + const x = this.root; + if (x === SENTINEL) { + this.root = z; + z.color = NodeColor.Black; + } else if (node!.right === SENTINEL) { + node!.right = z; + z.parent = node!; + } else { + const nextNode = leftest(node!.right); + nextNode.left = z; + z.parent = nextNode; + } + + fixInsert(this, z); + return z; + } + + /** + * node node + * / \ / \ + * a b ----> a b + * \ + * z + */ + private rbInsertLeft(node: TreeNode | null, p: Piece): TreeNode { + const z = new TreeNode(p, NodeColor.Red); + z.left = SENTINEL; + z.right = SENTINEL; + z.parent = SENTINEL; + z.size_left = 0; + z.lf_left = 0; + + if (this.root === SENTINEL) { + this.root = z; + z.color = NodeColor.Black; + } else if (node!.left === SENTINEL) { + node!.left = z; + z.parent = node!; + } else { + const prevNode = righttest(node!.left); // a + prevNode.right = z; + z.parent = prevNode; + } + + fixInsert(this, z); + return z; + } + + private getContentOfSubTree(node: TreeNode): string { + let str = ''; + + this.iterate(node, node => { + str += this.getNodeContent(node); + return true; + }); + + return str; + } + // #endregion +} diff --git a/packages/semi-json-viewer-core/src/pieceTreeTextBuffer/pieceTreeTextBufferBuilder.ts b/packages/semi-json-viewer-core/src/pieceTreeTextBuffer/pieceTreeTextBufferBuilder.ts new file mode 100644 index 0000000000..cb8a53c46f --- /dev/null +++ b/packages/semi-json-viewer-core/src/pieceTreeTextBuffer/pieceTreeTextBufferBuilder.ts @@ -0,0 +1,166 @@ +/** reference from https://github.com/microsoft/vscode */ + +import { CharCode } from '../common/charCode'; +import { StringBuffer, createLineStarts, createLineStartsFast, PieceTreeBase } from './pieceTreeBase'; + +export const UTF8_BOM_CHARACTER = String.fromCharCode(CharCode.UTF8_BOM); + +export function startsWithUTF8BOM(str: string): boolean { + return !!(str && str.length > 0 && str.charCodeAt(0) === CharCode.UTF8_BOM); +} + +export const enum DefaultEndOfLine { + /** + * Use line feed (\n) as the end of line character. + */ + LF = 1, + /** + * Use carriage return and line feed (\r\n) as the end of line character. + */ + CRLF = 2, +} + +export class PieceTreeTextBufferFactory { + constructor( + private readonly _chunks: StringBuffer[], + private readonly _bom: string, + private readonly _cr: number, + private readonly _lf: number, + private readonly _crlf: number, + private readonly _normalizeEOL: boolean + ) {} + + private _getEOL(defaultEOL: DefaultEndOfLine): '\r\n' | '\n' { + const totalEOLCount = this._cr + this._lf + this._crlf; + const totalCRCount = this._cr + this._crlf; + if (totalEOLCount === 0) { + // This is an empty file or a file with precisely one line + return defaultEOL === DefaultEndOfLine.LF ? '\n' : '\r\n'; + } + if (totalCRCount > totalEOLCount / 2) { + // More than half of the file contains \r\n ending lines + return '\r\n'; + } + // At least one line more ends in \n + return '\n'; + } + + public create(defaultEOL: DefaultEndOfLine): PieceTreeBase { + const eol = this._getEOL(defaultEOL); + const chunks = this._chunks; + + if ( + this._normalizeEOL && + ((eol === '\r\n' && (this._cr > 0 || this._lf > 0)) || (eol === '\n' && (this._cr > 0 || this._crlf > 0))) + ) { + // Normalize pieces + for (let i = 0, len = chunks.length; i < len; i++) { + const str = chunks[i].buffer.replace(/\r\n|\r|\n/g, eol); + const newLineStart = createLineStartsFast(str); + chunks[i] = new StringBuffer(str, newLineStart); + } + } + + return new PieceTreeBase(chunks, eol, this._normalizeEOL); + } + + public getFirstLineText(lengthLimit: number): string { + return this._chunks[0].buffer.substr(0, 100).split(/\r\n|\r|\n/)[0]; + } +} + +export class PieceTreeTextBufferBuilder { + private readonly chunks: StringBuffer[]; + private BOM: string; + + private _hasPreviousChar: boolean; + private _previousChar: number; + private readonly _tmpLineStarts: number[]; + + private cr: number; + private lf: number; + private crlf: number; + + constructor() { + this.chunks = []; + this.BOM = ''; + + this._hasPreviousChar = false; + this._previousChar = 0; + this._tmpLineStarts = []; + + this.cr = 0; + this.lf = 0; + this.crlf = 0; + } + + public acceptChunk(chunk: string): void { + if (chunk.length === 0) { + return; + } + + if (this.chunks.length === 0) { + if (startsWithUTF8BOM(chunk)) { + this.BOM = UTF8_BOM_CHARACTER; + chunk = chunk.substr(1); + } + } + + const lastChar = chunk.charCodeAt(chunk.length - 1); + if (lastChar === CharCode.CarriageReturn || (lastChar >= 0xd800 && lastChar <= 0xdbff)) { + // last character is \r or a high surrogate => keep it back + this._acceptChunk1(chunk.substr(0, chunk.length - 1), false); + this._hasPreviousChar = true; + this._previousChar = lastChar; + } else { + this._acceptChunk1(chunk, false); + this._hasPreviousChar = false; + this._previousChar = lastChar; + } + } + + private _acceptChunk1(chunk: string, allowEmptyStrings: boolean): void { + if (!allowEmptyStrings && chunk.length === 0) { + // Nothing to do + return; + } + + if (this._hasPreviousChar) { + this._acceptChunk2(String.fromCharCode(this._previousChar) + chunk); + } else { + this._acceptChunk2(chunk); + } + } + + private _acceptChunk2(chunk: string): void { + const lineStarts = createLineStarts(this._tmpLineStarts, chunk); + + this.chunks.push(new StringBuffer(chunk, lineStarts.lineStarts)); + this.cr += lineStarts.cr; + this.lf += lineStarts.lf; + this.crlf += lineStarts.crlf; + } + + public finish(normalizeEOL: boolean = true): PieceTreeTextBufferFactory { + this._finish(); + return new PieceTreeTextBufferFactory(this.chunks, this.BOM, this.cr, this.lf, this.crlf, normalizeEOL); + } + + private _finish(): void { + if (this.chunks.length === 0) { + this._acceptChunk1('', true); + } + + if (this._hasPreviousChar) { + this._hasPreviousChar = false; + // recreate last chunk + const lastChunk = this.chunks[this.chunks.length - 1]; + lastChunk.buffer += String.fromCharCode(this._previousChar); + const newLineStarts = createLineStartsFast(lastChunk.buffer); + lastChunk.lineStarts = newLineStarts; + if (this._previousChar === CharCode.CarriageReturn) { + this.cr++; + } + } + } +} diff --git a/packages/semi-json-viewer-core/src/pieceTreeTextBuffer/rbTreeBase.ts b/packages/semi-json-viewer-core/src/pieceTreeTextBuffer/rbTreeBase.ts new file mode 100644 index 0000000000..0203782ed0 --- /dev/null +++ b/packages/semi-json-viewer-core/src/pieceTreeTextBuffer/rbTreeBase.ts @@ -0,0 +1,421 @@ +/** reference from https://github.com/microsoft/vscode */ +/* eslint-disable @typescript-eslint/no-this-alias */ + +import { Piece, PieceTreeBase } from './pieceTreeBase'; + +export class TreeNode { + parent: TreeNode; + left: TreeNode; + right: TreeNode; + color: NodeColor; + + // Piece + piece: Piece; + size_left: number; // size of the left subtree (not inorder) + lf_left: number; // line feeds cnt in the left subtree (not in order) + + constructor(piece: Piece, color: NodeColor) { + this.piece = piece; + this.color = color; + this.size_left = 0; + this.lf_left = 0; + this.parent = this; + this.left = this; + this.right = this; + } + + public next(): TreeNode { + if (this.right !== SENTINEL) { + return leftest(this.right); + } + + let node: TreeNode = this; + + while (node.parent !== SENTINEL) { + if (node.parent.left === node) { + break; + } + + node = node.parent; + } + + if (node.parent === SENTINEL) { + return SENTINEL; + } else { + return node.parent; + } + } + + public prev(): TreeNode { + if (this.left !== SENTINEL) { + return righttest(this.left); + } + + let node: TreeNode = this; + + while (node.parent !== SENTINEL) { + if (node.parent.right === node) { + break; + } + + node = node.parent; + } + + if (node.parent === SENTINEL) { + return SENTINEL; + } else { + return node.parent; + } + } + + public detach(): void { + this.parent = null!; + this.left = null!; + this.right = null!; + } +} + +export const enum NodeColor { + Black = 0, + Red = 1, +} + +export const SENTINEL: TreeNode = new TreeNode(null!, NodeColor.Black); +SENTINEL.parent = SENTINEL; +SENTINEL.left = SENTINEL; +SENTINEL.right = SENTINEL; +SENTINEL.color = NodeColor.Black; + +export function leftest(node: TreeNode): TreeNode { + while (node.left !== SENTINEL) { + node = node.left; + } + return node; +} + +export function righttest(node: TreeNode): TreeNode { + while (node.right !== SENTINEL) { + node = node.right; + } + return node; +} + +function calculateSize(node: TreeNode): number { + if (node === SENTINEL) { + return 0; + } + + return node.size_left + node.piece.length + calculateSize(node.right); +} + +function calculateLF(node: TreeNode): number { + if (node === SENTINEL) { + return 0; + } + + return node.lf_left + node.piece.lineFeedCnt + calculateLF(node.right); +} + +function resetSentinel(): void { + SENTINEL.parent = SENTINEL; +} + +export function leftRotate(tree: PieceTreeBase, x: TreeNode) { + const y = x.right; + + // fix size_left + y.size_left += x.size_left + (x.piece ? x.piece.length : 0); + y.lf_left += x.lf_left + (x.piece ? x.piece.lineFeedCnt : 0); + x.right = y.left; + + if (y.left !== SENTINEL) { + y.left.parent = x; + } + y.parent = x.parent; + if (x.parent === SENTINEL) { + tree.root = y; + } else if (x.parent.left === x) { + x.parent.left = y; + } else { + x.parent.right = y; + } + y.left = x; + x.parent = y; +} + +export function rightRotate(tree: PieceTreeBase, y: TreeNode) { + const x = y.left; + y.left = x.right; + if (x.right !== SENTINEL) { + x.right.parent = y; + } + x.parent = y.parent; + + // fix size_left + y.size_left -= x.size_left + (x.piece ? x.piece.length : 0); + y.lf_left -= x.lf_left + (x.piece ? x.piece.lineFeedCnt : 0); + + if (y.parent === SENTINEL) { + tree.root = x; + } else if (y === y.parent.right) { + y.parent.right = x; + } else { + y.parent.left = x; + } + + x.right = y; + y.parent = x; +} + +export function rbDelete(tree: PieceTreeBase, z: TreeNode) { + let x: TreeNode; + let y: TreeNode; + + if (z.left === SENTINEL) { + y = z; + x = y.right; + } else if (z.right === SENTINEL) { + y = z; + x = y.left; + } else { + y = leftest(z.right); + x = y.right; + } + + if (y === tree.root) { + tree.root = x; + + // if x is null, we are removing the only node + x.color = NodeColor.Black; + z.detach(); + resetSentinel(); + tree.root.parent = SENTINEL; + + return; + } + + const yWasRed = y.color === NodeColor.Red; + + if (y === y.parent.left) { + y.parent.left = x; + } else { + y.parent.right = x; + } + + if (y === z) { + x.parent = y.parent; + recomputeTreeMetadata(tree, x); + } else { + if (y.parent === z) { + x.parent = y; + } else { + x.parent = y.parent; + } + + // as we make changes to x's hierarchy, update size_left of subtree first + recomputeTreeMetadata(tree, x); + + y.left = z.left; + y.right = z.right; + y.parent = z.parent; + y.color = z.color; + + if (z === tree.root) { + tree.root = y; + } else { + if (z === z.parent.left) { + z.parent.left = y; + } else { + z.parent.right = y; + } + } + + if (y.left !== SENTINEL) { + y.left.parent = y; + } + if (y.right !== SENTINEL) { + y.right.parent = y; + } + // update metadata + // we replace z with y, so in this sub tree, the length change is z.item.length + y.size_left = z.size_left; + y.lf_left = z.lf_left; + recomputeTreeMetadata(tree, y); + } + + z.detach(); + + if (x.parent.left === x) { + const newSizeLeft = calculateSize(x); + const newLFLeft = calculateLF(x); + if (newSizeLeft !== x.parent.size_left || newLFLeft !== x.parent.lf_left) { + const delta = newSizeLeft - x.parent.size_left; + const lf_delta = newLFLeft - x.parent.lf_left; + x.parent.size_left = newSizeLeft; + x.parent.lf_left = newLFLeft; + updateTreeMetadata(tree, x.parent, delta, lf_delta); + } + } + + recomputeTreeMetadata(tree, x.parent); + + if (yWasRed) { + resetSentinel(); + return; + } + + // RB-DELETE-FIXUP + let w: TreeNode; + while (x !== tree.root && x.color === NodeColor.Black) { + if (x === x.parent.left) { + w = x.parent.right; + + if (w.color === NodeColor.Red) { + w.color = NodeColor.Black; + x.parent.color = NodeColor.Red; + leftRotate(tree, x.parent); + w = x.parent.right; + } + + if (w.left.color === NodeColor.Black && w.right.color === NodeColor.Black) { + w.color = NodeColor.Red; + x = x.parent; + } else { + if (w.right.color === NodeColor.Black) { + w.left.color = NodeColor.Black; + w.color = NodeColor.Red; + rightRotate(tree, w); + w = x.parent.right; + } + + w.color = x.parent.color; + x.parent.color = NodeColor.Black; + w.right.color = NodeColor.Black; + leftRotate(tree, x.parent); + x = tree.root; + } + } else { + w = x.parent.left; + + if (w.color === NodeColor.Red) { + w.color = NodeColor.Black; + x.parent.color = NodeColor.Red; + rightRotate(tree, x.parent); + w = x.parent.left; + } + + if (w.left.color === NodeColor.Black && w.right.color === NodeColor.Black) { + w.color = NodeColor.Red; + x = x.parent; + } else { + if (w.left.color === NodeColor.Black) { + w.right.color = NodeColor.Black; + w.color = NodeColor.Red; + leftRotate(tree, w); + w = x.parent.left; + } + + w.color = x.parent.color; + x.parent.color = NodeColor.Black; + w.left.color = NodeColor.Black; + rightRotate(tree, x.parent); + x = tree.root; + } + } + } + x.color = NodeColor.Black; + resetSentinel(); +} + +export function fixInsert(tree: PieceTreeBase, x: TreeNode) { + recomputeTreeMetadata(tree, x); + + while (x !== tree.root && x.parent.color === NodeColor.Red) { + if (x.parent === x.parent.parent.left) { + const y = x.parent.parent.right; + + if (y.color === NodeColor.Red) { + x.parent.color = NodeColor.Black; + y.color = NodeColor.Black; + x.parent.parent.color = NodeColor.Red; + x = x.parent.parent; + } else { + if (x === x.parent.right) { + x = x.parent; + leftRotate(tree, x); + } + + x.parent.color = NodeColor.Black; + x.parent.parent.color = NodeColor.Red; + rightRotate(tree, x.parent.parent); + } + } else { + const y = x.parent.parent.left; + + if (y.color === NodeColor.Red) { + x.parent.color = NodeColor.Black; + y.color = NodeColor.Black; + x.parent.parent.color = NodeColor.Red; + x = x.parent.parent; + } else { + if (x === x.parent.left) { + x = x.parent; + rightRotate(tree, x); + } + x.parent.color = NodeColor.Black; + x.parent.parent.color = NodeColor.Red; + leftRotate(tree, x.parent.parent); + } + } + } + + tree.root.color = NodeColor.Black; +} + +export function updateTreeMetadata(tree: PieceTreeBase, x: TreeNode, delta: number, lineFeedCntDelta: number): void { + // node length change or line feed count change + while (x !== tree.root && x !== SENTINEL) { + if (x.parent.left === x) { + x.parent.size_left += delta; + x.parent.lf_left += lineFeedCntDelta; + } + + x = x.parent; + } +} + +export function recomputeTreeMetadata(tree: PieceTreeBase, x: TreeNode) { + let delta = 0; + let lf_delta = 0; + if (x === tree.root) { + return; + } + + // go upwards till the node whose left subtree is changed. + while (x !== tree.root && x === x.parent.right) { + x = x.parent; + } + + if (x === tree.root) { + // well, it means we add a node to the end (inorder) + return; + } + + // x is the node whose right subtree is changed. + x = x.parent; + + delta = calculateSize(x.left) - x.size_left; + lf_delta = calculateLF(x.left) - x.lf_left; + x.size_left += delta; + x.lf_left += lf_delta; + + // go upwards till root. O(logN) + while (x !== tree.root && (delta !== 0 || lf_delta !== 0)) { + if (x.parent.left === x) { + x.parent.size_left += delta; + x.parent.lf_left += lf_delta; + } + + x = x.parent; + } +} diff --git a/packages/semi-json-viewer-core/src/service/completion.ts b/packages/semi-json-viewer-core/src/service/completion.ts new file mode 100644 index 0000000000..fa3a0fbf24 --- /dev/null +++ b/packages/semi-json-viewer-core/src/service/completion.ts @@ -0,0 +1,526 @@ +/** Based on https://github.com/microsoft/vscode-json-languageservice with modifications for custom requirements */ +import { Position } from '../common/position'; +import { JSONModel } from '../model/jsonModel'; +import { JsonDocument } from './parse'; +import { Range } from '../common/range'; +import * as Json from 'jsonc-parser'; +import * as Parser from './parse'; +import { + ASTNode, + CompletionItem, + CompletionItemKind, + CompletionList, + InsertTextFormat, + ObjectASTNode, + PropertyASTNode, + TextEdit, +} from './jsonTypes'; +import { CompletionsCollector, JSONCompletionItem } from './contribution'; +import { CompletionOptions } from '../json-viewer/jsonViewer'; + +/** + * Json补全功能的核心实现 + */ +export class JSONCompletion { + private _options: CompletionOptions | null; + + constructor(options: CompletionOptions | null) { + this._options = options; + } + + public doCompletion(jsonModel: JSONModel, position: Position, doc: JsonDocument) { + const result: CompletionList = { + items: [], + isIncomplete: false, + }; + const text = jsonModel.getValue(); + + const offset = jsonModel.getOffsetAt(position.lineNumber, position.column); + + let node = doc.getNodeFromOffset(offset, true); + + if (node && offset === node.offset + node.length && offset > 0) { + const ch = text[offset - 1]; + if ((node.type === 'object' && ch === '}') || (node.type === 'array' && ch === ']')) { + node = node.parent; + } + } + + const currentWord = this.getCurrentWord(jsonModel, offset); + let overwriteRange: Range; + if ( + node && + (node.type === 'string' || node.type === 'number' || node.type === 'boolean' || node.type === 'null') + ) { + overwriteRange = Range.create( + jsonModel.positionAt(node.offset), + jsonModel.positionAt(node.offset + node.length) + ); + } else { + let overwriteStart = offset - currentWord.length; + if (overwriteStart > 0 && text[overwriteStart - 1] === '"') { + overwriteStart--; + } + overwriteRange = Range.create(jsonModel.positionAt(overwriteStart), position); + } + const proposed = new Map(); + + const collector: CompletionsCollector = { + add: (suggestion: JSONCompletionItem) => { + let label = suggestion.label; + const existing = proposed.get(label); + if (!existing) { + label = label.replace(/[\n]/g, '↵'); + if (label.length > 60) { + const shortenedLabel = label.substring(0, 57).trim() + '...'; + if (!proposed.has(shortenedLabel)) { + label = shortenedLabel; + } + } + suggestion.textEdit = TextEdit.replace(overwriteRange as Range, suggestion.insertText); + suggestion.label = label; + proposed.set(label, suggestion); + result.items.push(suggestion); + } else { + if (!existing.documentation) { + existing.documentation = suggestion.documentation; + } + if (!existing.detail) { + existing.detail = suggestion.detail; + } + if (!existing.labelDetails) { + existing.labelDetails = suggestion.labelDetails; + } + } + }, + setAsIncomplete: () => { + result.isIncomplete = true; + }, + error: (message: string) => { + console.error(message); + }, + getNumberOfProposals: () => { + return result.items.length; + }, + }; + + return Promise.resolve().then(() => { + const collectionPromises: Promise[] = []; + + let addValue = true; + let currentKey = ''; + + let currentProperty: PropertyASTNode | undefined = undefined; + if (node) { + if (node.type === 'string') { + const parent = node.parent; + if (parent && parent.type === 'property' && parent.keyNode === node) { + addValue = !parent.valueNode; + currentProperty = parent; + currentKey = text.substr(node.offset + 1, node.length - 2); + if (parent) { + node = parent.parent; + } + } + } + } + + if (node && node.type === 'object') { + if (node.offset === offset) { + return result; + } + const properties = node.properties; + properties.forEach(p => { + if (!currentProperty || currentProperty !== p) { + proposed.set(p.keyNode.value, CompletionItem.create('__')); + } + }); + let separatorAfter = ''; + if (addValue) { + separatorAfter = this.evaluateSeparatorAfter( + jsonModel, + jsonModel.getOffsetAt(overwriteRange.endLineNumber, overwriteRange.endColumn) + ); + } + + // property proposals without schema + this.getSchemaLessPropertyCompletions(doc, node, currentKey, collector, currentWord); + + // const location = Parser.getNodePath(node); + if (currentWord.length > 0 && text.charAt(offset - currentWord.length - 1) !== '"') { + collector.add({ + kind: CompletionItemKind.Property, + label: this.getLabelForValue(currentWord), + insertText: this.getInsertTextForProperty(currentWord, undefined, false, separatorAfter), + insertTextFormat: InsertTextFormat.Snippet, + documentation: '', + }); + collector.setAsIncomplete(); + } + } + const types: { [type: string]: boolean } = {}; + + // value proposals without schema + this.getSchemaLessValueCompletions(doc, node, offset, jsonModel, collector); + + return Promise.all(collectionPromises).then(() => { + if (collector.getNumberOfProposals() === 0) { + let offsetForSeparator = offset; + if ( + node && + (node.type === 'string' || + node.type === 'number' || + node.type === 'boolean' || + node.type === 'null') + ) { + offsetForSeparator = node.offset + node.length; + } + const separatorAfter = this.evaluateSeparatorAfter(jsonModel, offsetForSeparator); + this.addFillerValueCompletions(types, separatorAfter, collector); + } else if (this._options?.staticCompletions) { + this._options.staticCompletions.forEach(item => { + collector.add({ + label: item.label, + insertText: item.insertText || item.label, + documentation: item.documentation || '', + }); + }); + } + return result; + }); + }); + } + + /** + * 获取光标位置前的当前单词 + * @param jsonModel + * @param offset + * @returns + */ + private getCurrentWord(jsonModel: JSONModel, offset: number) { + let i = offset - 1; + const text = jsonModel.getValue(); + while (i >= 0 && ' \t\n\r\v":{[,]}'.indexOf(text.charAt(i)) === -1) { + i--; + } + return text.substring(i + 1, offset); + } + + private evaluateSeparatorAfter(jsonModel: JSONModel, offset: number) { + const scanner = Json.createScanner(jsonModel.getValue(), true); + scanner.setPosition(offset); + const token = scanner.scan(); + switch (token) { + case Json.SyntaxKind.CommaToken: + case Json.SyntaxKind.CloseBraceToken: + case Json.SyntaxKind.CloseBracketToken: + case Json.SyntaxKind.EOF: + return ''; + default: + return ','; + } + } + + private getLabelForValue(value: any): string { + return JSON.stringify(value); + } + + private getInsertTextForPlainText(text: string): string { + return text.replace(/[\\\$\}]/g, '\\$&'); // escape $, \ and } + } + + private getInsertTextForValue(value: any, separatorAfter: string): string { + const text = JSON.stringify(value, null, '\t'); + if (text === '{}') { + return '{$1}' + separatorAfter; + } else if (text === '[]') { + return '[$1]' + separatorAfter; + } + return this.getInsertTextForPlainText(text + separatorAfter); + } + + private getFilterTextForValue(value: any): string { + return JSON.stringify(value); + } + + private getInsertTextForProperty( + key: string, + propertySchema: undefined, + addValue: boolean, + separatorAfter: string + ): string { + const propertyText = this.getInsertTextForValue(key, ''); + if (!addValue) { + return propertyText; + } + const resultText = propertyText + ': '; + + let value; + const nValueProposals = 0; + if (propertySchema) { + //TODO + } + if (!value || nValueProposals > 1) { + value = '$1'; + } + return resultText + value + separatorAfter; + } + + private getSchemaLessPropertyCompletions( + doc: Parser.JsonDocument, + node: ASTNode, + currentKey: string, + collector: CompletionsCollector, + currentWord: string + ): void { + const collectCompletionsForSimilarObject = (obj: ObjectASTNode) => { + obj.properties.forEach(p => { + const key = p.keyNode.value; + if (key.toLowerCase().startsWith(currentWord.toLowerCase()) && currentWord !== '') { + collector.add({ + kind: CompletionItemKind.Property, + label: key, + insertText: this.getInsertTextForValue(key, ''), + insertTextFormat: InsertTextFormat.Snippet, + filterText: this.getFilterTextForValue(key), + documentation: '', + }); + } + }); + }; + if (node.parent) { + if (node.parent.type === 'property') { + // if the object is a property value, check the tree for other objects that hang under a property of the same name + const parentKey = node.parent.keyNode.value; + doc.visit(n => { + if ( + n.type === 'property' && + n !== node.parent && + n.keyNode.value === parentKey && + n.valueNode && + n.valueNode.type === 'object' + ) { + collectCompletionsForSimilarObject(n.valueNode); + } + return true; + }); + } else if (node.parent.type === 'array') { + // if the object is in an array, use all other array elements as similar objects + node.parent.items.forEach(n => { + if (n.type === 'object' && n !== node) { + collectCompletionsForSimilarObject(n); + } + }); + } + } + // else if (node.type === 'object') { + // collector.add({ + // kind: CompletionItemKind.Property, + // label: '$schema', + // insertText: this.getInsertTextForProperty( + // '$schema', + // undefined, + // true, + // '' + // ), + // insertTextFormat: InsertTextFormat.Snippet, + // documentation: '', + // filterText: this.getFilterTextForValue('$schema') + // }); + // } + } + + private addFillerValueCompletions( + types: { [type: string]: boolean }, + separatorAfter: string, + collector: CompletionsCollector + ): void { + if (types['object']) { + collector.add({ + kind: this.getSuggestionKind('object'), + label: '{}', + insertText: this.getInsertTextForGuessedValue({}, separatorAfter), + insertTextFormat: InsertTextFormat.Snippet, + detail: 'New object', + documentation: '', + }); + } + if (types['array']) { + collector.add({ + kind: this.getSuggestionKind('array'), + label: '[]', + insertText: this.getInsertTextForGuessedValue([], separatorAfter), + insertTextFormat: InsertTextFormat.Snippet, + detail: 'New array', + documentation: '', + }); + } + } + + private getInsertTextForGuessedValue(value: any, separatorAfter: string): string { + switch (typeof value) { + case 'object': + if (value === null) { + return '${1:null}' + separatorAfter; + } + return this.getInsertTextForValue(value, separatorAfter); + case 'string': + let snippetValue = JSON.stringify(value); + snippetValue = snippetValue.substr(1, snippetValue.length - 2); // remove quotes + snippetValue = this.getInsertTextForPlainText(snippetValue); // escape \ and } + return '"${1:' + snippetValue + '}"' + separatorAfter; + case 'number': + case 'boolean': + return '${1:' + JSON.stringify(value) + '}' + separatorAfter; + } + return this.getInsertTextForValue(value, separatorAfter); + } + + private getSuggestionKind(type: any): CompletionItemKind { + if (Array.isArray(type)) { + const array = type; + type = array.length > 0 ? array[0] : undefined; + } + if (!type) { + return CompletionItemKind.Value; + } + switch (type) { + case 'string': + return CompletionItemKind.Value; + case 'object': + return CompletionItemKind.Module; + case 'property': + return CompletionItemKind.Property; + default: + return CompletionItemKind.Value; + } + } + + private getSchemaLessValueCompletions( + doc: Parser.JsonDocument, + node: ASTNode | undefined, + offset: number, + jsonModel: JSONModel, + collector: CompletionsCollector + ): void { + let offsetForSeparator = offset; + if ( + node && + (node.type === 'string' || node.type === 'number' || node.type === 'boolean' || node.type === 'null') + ) { + offsetForSeparator = node.offset + node.length; + node = node.parent; + } + + if (!node) { + // collector.add({ + // kind: this.getSuggestionKind('object'), + // label: 'Empty object', + // insertText: this.getInsertTextForValue({}, ''), + // insertTextFormat: InsertTextFormat.Snippet, + // documentation: '' + // }); + // collector.add({ + // kind: this.getSuggestionKind('array'), + // label: 'Empty array', + // insertText: this.getInsertTextForValue([], ''), + // insertTextFormat: InsertTextFormat.Snippet, + // documentation: '' + // }); + return; + } + const separatorAfter = this.evaluateSeparatorAfter(jsonModel, offsetForSeparator); + const collectSuggestionsForValues = (value: ASTNode) => { + if (value.parent && !Parser.contains(value.parent, offset, true)) { + collector.add({ + kind: this.getSuggestionKind(value.type), + label: this.getLabelTextForMatchingNode(value, jsonModel), + insertText: this.getInsertTextForMatchingNode(value, jsonModel, separatorAfter), + insertTextFormat: InsertTextFormat.Snippet, + documentation: '', + }); + } + if (value.type === 'boolean') { + this.addBooleanValueCompletion(!value.value, separatorAfter, collector); + } + }; + + if (node.type === 'property') { + if (offset > (node.colonOffset || 0)) { + const valueNode = node.valueNode; + if ( + valueNode && + (offset > valueNode.offset + valueNode.length || + valueNode.type === 'object' || + valueNode.type === 'array') + ) { + return; + } + // suggest values at the same key + const parentKey = node.keyNode.value; + doc.visit(n => { + if (n.type === 'property' && n.keyNode.value === parentKey && n.valueNode) { + collectSuggestionsForValues(n.valueNode); + } + return true; + }); + // if (parentKey === '$schema' && node.parent && !node.parent.parent) { + // this.addDollarSchemaCompletions(separatorAfter, collector); + // } + } + } + if (node.type === 'array') { + if (node.parent && node.parent.type === 'property') { + // suggest items of an array at the same key + const parentKey = node.parent.keyNode.value; + doc.visit(n => { + if ( + n.type === 'property' && + n.keyNode.value === parentKey && + n.valueNode && + n.valueNode.type === 'array' + ) { + n.valueNode.items.forEach(collectSuggestionsForValues); + } + return true; + }); + } else { + // suggest items in the same array + node.items.forEach(collectSuggestionsForValues); + } + } + } + private getLabelTextForMatchingNode(node: ASTNode, jsonModel: JSONModel): string { + switch (node.type) { + case 'array': + return '[]'; + case 'object': + return '{}'; + default: + const content = jsonModel.getValue().substr(node.offset, node.length); + return content; + } + } + + private getInsertTextForMatchingNode(node: ASTNode, jsonModel: JSONModel, separatorAfter: string): string { + switch (node.type) { + case 'array': + return this.getInsertTextForValue([], separatorAfter); + case 'object': + return this.getInsertTextForValue({}, separatorAfter); + default: + const content = jsonModel.getValue().substr(node.offset, node.length) + separatorAfter; + return this.getInsertTextForPlainText(content); + } + } + + private addBooleanValueCompletion(value: boolean, separatorAfter: string, collector: CompletionsCollector): void { + collector.add({ + kind: this.getSuggestionKind('boolean'), + label: value ? 'true' : 'false', + insertText: this.getInsertTextForValue(value, separatorAfter), + insertTextFormat: InsertTextFormat.Snippet, + documentation: '', + }); + } +} diff --git a/packages/semi-json-viewer-core/src/service/contribution.ts b/packages/semi-json-viewer-core/src/service/contribution.ts new file mode 100644 index 0000000000..30282447a3 --- /dev/null +++ b/packages/semi-json-viewer-core/src/service/contribution.ts @@ -0,0 +1,11 @@ +/** reference from https://github.com/microsoft/vscode-json-languageservice */ +import { CompletionItem } from './jsonTypes'; + +export type JSONCompletionItem = CompletionItem & { insertText: string }; + +export interface CompletionsCollector { + add(suggestion: JSONCompletionItem & { insertText: string }): void; + error(message: string): void; + setAsIncomplete(): void; + getNumberOfProposals(): number +} diff --git a/packages/semi-json-viewer-core/src/service/getRange.ts b/packages/semi-json-viewer-core/src/service/getRange.ts new file mode 100644 index 0000000000..41844bbaa8 --- /dev/null +++ b/packages/semi-json-viewer-core/src/service/getRange.ts @@ -0,0 +1,55 @@ +/** Based on https://github.com/microsoft/vscode-json-languageservice with modifications for custom requirements */ + +import { JSONModel } from '../model/jsonModel'; +import { createScanner, SyntaxKind, ScanError } from 'jsonc-parser'; +import { FoldingRange } from './jsonService'; +/** + * 获取json括号折叠信息 + * @param jsonModel + * @returns + */ +export function getFoldingRanges(jsonModel: JSONModel) { + const ranges: FoldingRange[] = []; + const nestingLevels: number[] = []; + const stack: FoldingRange[] = []; + let prevStart = -1; + const scanner = createScanner(jsonModel.getValue(), false); + let token = scanner.scan(); + + function addRange(range: FoldingRange) { + ranges.push(range); + nestingLevels.push(stack.length); + } + + while (token !== SyntaxKind.EOF) { + switch (token) { + case SyntaxKind.OpenBraceToken: + case SyntaxKind.OpenBracketToken: { + const startLine = jsonModel.positionAt(scanner.getPosition()).lineNumber; + const range: FoldingRange = { + startLine, + endLine: startLine, + kind: token === SyntaxKind.OpenBraceToken ? 'object' : 'array', + }; + stack.push(range); + break; + } + case SyntaxKind.CloseBraceToken: + case SyntaxKind.CloseBracketToken: { + const kind = token === SyntaxKind.CloseBraceToken ? 'object' : 'array'; + if (stack.length > 0 && stack[stack.length - 1].kind === kind) { + const range = stack.pop(); + const line = jsonModel.positionAt(scanner.getTokenOffset()).lineNumber; + if (range && line > range.startLine + 1 && prevStart !== range.startLine) { + range.endLine = line - 1; + addRange(range); + prevStart = range.startLine; + } + } + break; + } + } + token = scanner.scan(); + } + return ranges; +} diff --git a/packages/semi-json-viewer-core/src/service/jsonService.ts b/packages/semi-json-viewer-core/src/service/jsonService.ts new file mode 100644 index 0000000000..c2ddb3a6fb --- /dev/null +++ b/packages/semi-json-viewer-core/src/service/jsonService.ts @@ -0,0 +1,32 @@ +import { JSONModel } from '../model/jsonModel'; +import { format, FormattingOptions } from 'jsonc-parser'; +import { JsonDocument, parseJson } from './parse'; +import { Diagnostic } from './jsonTypes'; +export { getFoldingRanges } from './getRange'; + +/** + * Json 服务,提供json格式化、补全、折叠等功能 + */ + +export interface FoldingRange { + startLine: number; + endLine: number; + kind: 'object' | 'array' +} + +export function formatJson(jsonModel: JSONModel, options: FormattingOptions) { + const edits = format(jsonModel.getValue(), undefined, options); + return edits; +} + +export function doValidate(jsonModel: JSONModel) { + const { root, problems } = parseJson(jsonModel); + return { + problems, + root, + }; +} + +export function parseJsonAst(jsonModel: JSONModel) { + return parseJson(jsonModel).root; +} diff --git a/packages/semi-json-viewer-core/src/service/jsonTypes.ts b/packages/semi-json-viewer-core/src/service/jsonTypes.ts new file mode 100644 index 0000000000..aa4811ddb1 --- /dev/null +++ b/packages/semi-json-viewer-core/src/service/jsonTypes.ts @@ -0,0 +1,236 @@ +/** Based on https://github.com/microsoft/vscode-json-languageservice with modifications for custom requirements */ +import { Position } from '../common/position'; +import { Range } from '../common/range'; +/** + * Error codes used by diagnostics + */ +export enum ErrorCode { + Undefined = 0, + EnumValueMismatch = 1, + Deprecated = 2, + UnexpectedEndOfComment = 0x101, + UnexpectedEndOfString = 0x102, + UnexpectedEndOfNumber = 0x103, + InvalidUnicode = 0x104, + InvalidEscapeCharacter = 0x105, + InvalidCharacter = 0x106, + PropertyExpected = 0x201, + CommaExpected = 0x202, + ColonExpected = 0x203, + ValueExpected = 0x204, + CommaOrCloseBacketExpected = 0x205, + CommaOrCloseBraceExpected = 0x206, + TrailingComma = 0x207, + DuplicateKey = 0x208, + CommentNotPermitted = 0x209, + PropertyKeysMustBeDoublequoted = 0x210, + SchemaResolveError = 0x300, + SchemaUnsupportedFeature = 0x301, +} + +export type ASTNode = + | ObjectASTNode + | PropertyASTNode + | ArrayASTNode + | StringASTNode + | NumberASTNode + | BooleanASTNode + | NullASTNode; + +export interface BaseASTNode { + readonly type: 'object' | 'array' | 'property' | 'string' | 'number' | 'boolean' | 'null'; + readonly parent?: ASTNode; + readonly offset: number; + readonly length: number; + readonly children?: ASTNode[]; + readonly value?: string | boolean | number | null +} +export interface ObjectASTNode extends BaseASTNode { + readonly type: 'object'; + readonly properties: PropertyASTNode[]; + readonly children: ASTNode[] +} +export interface PropertyASTNode extends BaseASTNode { + readonly type: 'property'; + readonly keyNode: StringASTNode; + readonly valueNode?: ASTNode; + readonly colonOffset?: number; + readonly children: ASTNode[] +} +export interface ArrayASTNode extends BaseASTNode { + readonly type: 'array'; + readonly items: ASTNode[]; + readonly children: ASTNode[] +} +export interface StringASTNode extends BaseASTNode { + readonly type: 'string'; + readonly value: string +} +export interface NumberASTNode extends BaseASTNode { + readonly type: 'number'; + readonly value: number; + readonly isInteger: boolean +} +export interface BooleanASTNode extends BaseASTNode { + readonly type: 'boolean'; + readonly value: boolean +} +export interface NullASTNode extends BaseASTNode { + readonly type: 'null'; + readonly value: null +} + +export class Diagnostic { + readonly message: string; + readonly code: ErrorCode; + readonly range: ErrRange; + + constructor(message: string, code: ErrorCode, range: ErrRange) { + this.message = message; + this.code = code; + this.range = range; + } + + static create(message: string, code: ErrorCode, range: ErrRange) { + return new Diagnostic(message, code, range); + } +} + +export class ErrRange { + readonly start: Position; + readonly end: Position; + + constructor(start: Position, end: Position) { + this.start = start; + this.end = end; + } + static create(start: Position, end: Position) { + return new ErrRange(start, end); + } +} + +export type MarkupKind = 'plaintext' | 'markdown'; + +export interface MarkupContent { + kind: MarkupKind; + + value: string +} + +export interface CompletionItemLabelDetails { + detail?: string; + description?: string +} + +export namespace CompletionItemKind { + export const Text: 1 = 1; + export const Method: 2 = 2; + export const Function: 3 = 3; + export const Constructor: 4 = 4; + export const Field: 5 = 5; + export const Variable: 6 = 6; + export const Class: 7 = 7; + export const Interface: 8 = 8; + export const Module: 9 = 9; + export const Property: 10 = 10; + export const Unit: 11 = 11; + export const Value: 12 = 12; + export const Enum: 13 = 13; + export const Keyword: 14 = 14; + export const Snippet: 15 = 15; + export const Color: 16 = 16; + export const File: 17 = 17; + export const Reference: 18 = 18; + export const Folder: 19 = 19; + export const EnumMember: 20 = 20; + export const Constant: 21 = 21; + export const Struct: 22 = 22; + export const Event: 23 = 23; + export const Operator: 24 = 24; + export const TypeParameter: 25 = 25; +} + +export type CompletionItemKind = + | 1 + | 2 + | 3 + | 4 + | 5 + | 6 + | 7 + | 8 + | 9 + | 10 + | 11 + | 12 + | 13 + | 14 + | 15 + | 16 + | 17 + | 18 + | 19 + | 20 + | 21 + | 22 + | 23 + | 24 + | 25; + +export namespace InsertTextFormat { + /** + * The primary text to be inserted is treated as a plain string. + */ + export const PlainText: 1 = 1; + + /** + * The primary text to be inserted is treated as a snippet. + * + * A snippet can define tab stops and placeholders with `$1`, `$2` + * and `${3:foo}`. `$0` defines the final tab stop, it defaults to + * the end of the snippet. Placeholders with equal identifiers are linked, + * that is typing in one will update others too. + * + * See also: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#snippet_syntax + */ + export const Snippet: 2 = 2; +} + +export type InsertTextFormat = 1 | 2; + +export interface TextEdit { + range: Range; + + newText: string +} + +export namespace TextEdit { + export function replace(range: Range, newText: string): TextEdit { + return { + range, + newText, + }; + } +} + +export interface CompletionItem { + label: string; + detail?: string; + labelDetails?: CompletionItemLabelDetails; + documentation?: string | MarkupContent; + kind?: CompletionItemKind; + insertText?: string; + insertTextFormat?: InsertTextFormat; + filterText?: string; + textEdit?: TextEdit +} +export namespace CompletionItem { + export function create(label: string): CompletionItem { + return { label }; + } +} + +export interface CompletionList { + items: CompletionItem[]; + isIncomplete: boolean +} diff --git a/packages/semi-json-viewer-core/src/service/parse.ts b/packages/semi-json-viewer-core/src/service/parse.ts new file mode 100644 index 0000000000..94b4218cf4 --- /dev/null +++ b/packages/semi-json-viewer-core/src/service/parse.ts @@ -0,0 +1,518 @@ +/** Based on https://github.com/microsoft/vscode-json-languageservice with modifications for custom requirements */ +import * as Json from 'jsonc-parser'; +import { JSONModel } from '../model/jsonModel'; +import { + ArrayASTNode, + ASTNode, + BooleanASTNode, + Diagnostic, + ErrorCode, + ErrRange, + NullASTNode, + NumberASTNode, + ObjectASTNode, + PropertyASTNode, + StringASTNode, +} from './jsonTypes'; +import { isObject, isNumber } from '../common/utils'; +/** + * Json 解析服务,提供json解析(AST)、获取节点值、获取节点路径等功能 + */ + +export function getNodeValue(node: ASTNode): any { + return Json.getNodeValue(node); +} + +export function getNodePath(node: ASTNode): Json.JSONPath { + return Json.getNodePath(node); +} + +export function contains(node: ASTNode, offset: number, includeRightBound = false): boolean { + return ( + (offset >= node.offset && offset < node.offset + node.length) || + (includeRightBound && offset === node.offset + node.length) + ); +} + +export abstract class ASTNodeImpl { + public abstract readonly type: 'object' | 'property' | 'array' | 'number' | 'boolean' | 'null' | 'string'; + + public offset: number; + public length: number; + public readonly parent: ASTNode | undefined; + + constructor(parent: ASTNode | undefined, offset: number, length: number = 0) { + this.offset = offset; + this.length = length; + this.parent = parent; + } + + public get children(): ASTNode[] { + return []; + } + + public toString(): string { + return ( + 'type: ' + + this.type + + ' (' + + this.offset + + '/' + + this.length + + ')' + + (this.parent ? ' parent: {' + this.parent.toString() + '}' : '') + ); + } +} + +export class NullASTNodeImpl extends ASTNodeImpl implements NullASTNode { + public type: 'null' = 'null'; + public value: null = null; + constructor(parent: ASTNode | undefined, offset: number) { + super(parent, offset); + } +} + +export class BooleanASTNodeImpl extends ASTNodeImpl implements BooleanASTNode { + public type: 'boolean' = 'boolean'; + public value: boolean; + + constructor(parent: ASTNode | undefined, boolValue: boolean, offset: number) { + super(parent, offset); + this.value = boolValue; + } +} + +export class ArrayASTNodeImpl extends ASTNodeImpl implements ArrayASTNode { + public type: 'array' = 'array'; + public items: ASTNode[]; + + constructor(parent: ASTNode | undefined, offset: number) { + super(parent, offset); + this.items = []; + } + + public get children(): ASTNode[] { + return this.items; + } +} +export class NumberASTNodeImpl extends ASTNodeImpl implements NumberASTNode { + public type: 'number' = 'number'; + public isInteger: boolean; + public value: number; + + constructor(parent: ASTNode | undefined, offset: number) { + super(parent, offset); + this.isInteger = true; + this.value = Number.NaN; + } +} + +export class ObjectASTNodeImpl extends ASTNodeImpl implements ObjectASTNode { + public type: 'object' = 'object'; + public properties: PropertyASTNode[]; + + constructor(parent: ASTNode | undefined, offset: number) { + super(parent, offset); + + this.properties = []; + } + + public get children(): ASTNode[] { + return this.properties; + } +} + +export class StringASTNodeImpl extends ASTNodeImpl implements StringASTNode { + public type: 'string' = 'string'; + public value: string; + + constructor(parent: ASTNode | undefined, offset: number, length?: number) { + super(parent, offset, length); + this.value = ''; + } +} + +export class PropertyASTNodeImpl extends ASTNodeImpl implements PropertyASTNode { + public type: 'property' = 'property'; + public keyNode: StringASTNode; + public valueNode?: ASTNode; + public colonOffset: number; + + constructor(parent: ObjectASTNode | undefined, offset: number, keyNode: StringASTNode) { + super(parent, offset); + this.colonOffset = -1; + this.keyNode = keyNode; + } + + public get children(): ASTNode[] { + return this.valueNode ? [this.keyNode, this.valueNode] : [this.keyNode]; + } +} + +export class JsonDocument { + public readonly root: ASTNode | undefined; + constructor(root: ASTNode | undefined) { + this.root = root; + } + + public getNodeFromOffset(offset: number, includeRightBound = false): ASTNode | undefined { + if (!this.root) { + return undefined; + } + return Json.findNodeAtOffset(this.root, offset, includeRightBound); + } + + public visit(visitor: (node: ASTNode) => boolean): void { + if (this.root) { + const doVisit = (node: ASTNode): boolean => { + let ctn = visitor(node); + const children = node.children; + if (Array.isArray(children)) { + for (let i = 0; i < children.length && ctn; i++) { + ctn = doVisit(children[i]); + } + } + return ctn; + }; + doVisit(this.root); + } + } +} + +export function parseJson(jsonModel: JSONModel) { + const problems: Diagnostic[] = []; + let lastProblemOffset = -1; + const text = jsonModel.getValue(); + const scanner = Json.createScanner(text, false); + function _scanNext(): Json.SyntaxKind { + while (true) { + const token = scanner.scan(); + _checkScanError(); + + switch (token) { + case Json.SyntaxKind.LineBreakTrivia: + case Json.SyntaxKind.Trivia: + break; + default: + return token; + } + } + } + + function _checkScanError(): boolean { + switch (scanner.getTokenError()) { + case Json.ScanError.InvalidUnicode: + _error('Invalid unicode sequence in string.', ErrorCode.InvalidUnicode); + return true; + case Json.ScanError.InvalidEscapeCharacter: + _error('Invalid escape character in string.', ErrorCode.InvalidEscapeCharacter); + return true; + case Json.ScanError.UnexpectedEndOfNumber: + _error('Unexpected end of number.', ErrorCode.UnexpectedEndOfNumber); + return true; + case Json.ScanError.UnexpectedEndOfComment: + _error('Unexpected end of comment.', ErrorCode.UnexpectedEndOfComment); + return true; + case Json.ScanError.UnexpectedEndOfString: + _error('Unexpected end of string.', ErrorCode.UnexpectedEndOfString); + return true; + case Json.ScanError.InvalidCharacter: + _error('Invalid characters in string. Control characters must be escaped.', ErrorCode.InvalidCharacter); + return true; + } + return false; + } + + function _errorAtRange(message: string, code: ErrorCode, startOffset: number, endOffset: number) { + if (problems.length === 0 || startOffset !== lastProblemOffset) { + const range = ErrRange.create(jsonModel.positionAt(startOffset), jsonModel.positionAt(endOffset)); + problems.push(Diagnostic.create(message, code, range)); + lastProblemOffset = startOffset; + } + return; + } + + function _finalize(node: T, scanNext: boolean): T { + node.length = scanner.getTokenOffset() + scanner.getTokenLength() - node.offset; + + if (scanNext) { + _scanNext(); + } + + return node; + } + + function _error( + message: string, + code: ErrorCode, + node: T | undefined = undefined, + skipUntilAfter: Json.SyntaxKind[] = [], + skipUntil: Json.SyntaxKind[] = [] + ): T | undefined { + let start = scanner.getTokenOffset(); + let end = scanner.getPosition() + scanner.getTokenLength(); + if (start === end && start > 0) { + start--; + while (start > 0 && /\s/.test(text.charAt(start))) { + start--; + } + end = start + 1; + } + _errorAtRange(message, code, start, end); + if (node) { + _finalize(node, false); + } + if (skipUntilAfter.length + skipUntil.length > 0) { + let token = scanner.getToken(); + while (token !== Json.SyntaxKind.EOF) { + if (skipUntilAfter.indexOf(token) !== -1) { + _scanNext(); + break; + } else if (skipUntil.indexOf(token) !== -1) { + break; + } + token = _scanNext(); + } + } + return node; + } + + function _parseArray(parent: ASTNode | undefined): ArrayASTNode | undefined { + if (scanner.getToken() !== Json.SyntaxKind.OpenBracketToken) { + return undefined; + } + const node = new ArrayASTNodeImpl(parent, scanner.getTokenOffset()); + _scanNext(); + + let needComma = false; + while (scanner.getToken() !== Json.SyntaxKind.CloseBracketToken && scanner.getToken() !== Json.SyntaxKind.EOF) { + if (scanner.getToken() === Json.SyntaxKind.CommaToken) { + if (!needComma) { + _error('Value expected.', ErrorCode.ValueExpected); + } + const commaOffset = scanner.getTokenOffset(); + _scanNext(); + if (scanner.getToken() === Json.SyntaxKind.CloseBracketToken) { + if (needComma) { + _errorAtRange('Trailing comma', ErrorCode.TrailingComma, commaOffset, commaOffset + 1); + } + continue; + } + } else if (needComma) { + _error('Comma expected.', ErrorCode.CommaExpected, undefined, [], [Json.SyntaxKind.CloseBracketToken]); + break; + } + const item = _parseValue(node); + if (!item) { + _error('Value expected.', ErrorCode.ValueExpected, undefined, [], [Json.SyntaxKind.CloseBracketToken]); + break; + } else { + node.items.push(item); + } + needComma = true; + } + + if (scanner.getToken() !== Json.SyntaxKind.CloseBracketToken) { + return _error('Expected comma or closing bracket', ErrorCode.CommaOrCloseBraceExpected, node); + } + return _finalize(node, true); + } + + const keyPlaceholder = new StringASTNodeImpl(undefined, 0, 0); + + function _parseProperty( + parent: ObjectASTNode | undefined, + keysSeen: { [key: string]: PropertyASTNode | boolean } + ): PropertyASTNode | undefined { + const node = new PropertyASTNodeImpl(parent, scanner.getTokenOffset(), keyPlaceholder); + let key = _parseString(node); + if (!key) { + if (scanner.getToken() === Json.SyntaxKind.Unknown) { + // give a more helpful error message + _error('Property keys must be doublequoted', ErrorCode.PropertyKeysMustBeDoublequoted); + const keyNode = new StringASTNodeImpl(node, scanner.getTokenOffset(), scanner.getTokenLength()); + keyNode.value = scanner.getTokenValue(); + key = keyNode; + _scanNext(); // consume Unknown + } else { + return undefined; + } + } + node.keyNode = key; + + // For JSON files that forbid code comments, there is a convention to use the key name "//" to add comments. + // Multiple instances of "//" are okay. + if (key.value !== '//') { + const seen = keysSeen[key.value]; + if (seen) { + _errorAtRange( + 'Duplicate object key', + ErrorCode.DuplicateKey, + node.keyNode.offset, + node.keyNode.offset + node.keyNode.length + ); + if (isObject(seen)) { + _errorAtRange( + 'Duplicate object key', + ErrorCode.DuplicateKey, + seen.keyNode.offset, + seen.keyNode.offset + seen.keyNode.length + ); + } + keysSeen[key.value] = true; // if the same key is duplicate again, avoid duplicate error reporting + } else { + keysSeen[key.value] = node; + } + } + + if (scanner.getToken() === Json.SyntaxKind.ColonToken) { + node.colonOffset = scanner.getTokenOffset(); + _scanNext(); // consume ColonToken + } else { + _error('Colon expected', ErrorCode.ColonExpected); + if ( + scanner.getToken() === Json.SyntaxKind.StringLiteral && + jsonModel.positionAt(key.offset + key.length).lineNumber < + jsonModel.positionAt(scanner.getTokenOffset()).lineNumber + ) { + node.length = key.length; + return node; + } + } + const value = _parseValue(node); + if (!value) { + return _error( + 'Value expected', + ErrorCode.ValueExpected, + node, + [], + [Json.SyntaxKind.CloseBraceToken, Json.SyntaxKind.CommaToken] + ); + } + node.valueNode = value; + node.length = value.offset + value.length - node.offset; + return node; + } + + function _parseObject(parent: ASTNode | undefined): ObjectASTNode | undefined { + if (scanner.getToken() !== Json.SyntaxKind.OpenBraceToken) { + return undefined; + } + const node = new ObjectASTNodeImpl(parent, scanner.getTokenOffset()); + const keysSeen: any = Object.create(null); + _scanNext(); // consume OpenBraceToken + let needsComma = false; + + while (scanner.getToken() !== Json.SyntaxKind.CloseBraceToken && scanner.getToken() !== Json.SyntaxKind.EOF) { + if (scanner.getToken() === Json.SyntaxKind.CommaToken) { + if (!needsComma) { + _error('Property expected', ErrorCode.PropertyExpected); + } + const commaOffset = scanner.getTokenOffset(); + _scanNext(); // consume comma + if (scanner.getToken() === Json.SyntaxKind.CloseBraceToken) { + if (needsComma) { + _errorAtRange('Trailing comma', ErrorCode.TrailingComma, commaOffset, commaOffset + 1); + } + continue; + } + } else if (needsComma) { + _error('Expected comma', ErrorCode.CommaExpected); + } + const property = _parseProperty(node, keysSeen); + if (!property) { + _error( + 'Property expected', + ErrorCode.PropertyExpected, + undefined, + [], + [Json.SyntaxKind.CloseBraceToken, Json.SyntaxKind.CommaToken] + ); + } else { + node.properties.push(property); + } + needsComma = true; + } + + if (scanner.getToken() !== Json.SyntaxKind.CloseBraceToken) { + return _error('Expected comma or closing brace', ErrorCode.CommaOrCloseBraceExpected, node); + } + return _finalize(node, true); + } + + function _parseString(parent: ASTNode | undefined): StringASTNode | undefined { + if (scanner.getToken() !== Json.SyntaxKind.StringLiteral) { + return undefined; + } + + const node = new StringASTNodeImpl(parent, scanner.getTokenOffset()); + node.value = scanner.getTokenValue(); + + return _finalize(node, true); + } + + function _parseNumber(parent: ASTNode | undefined): NumberASTNode | undefined { + if (scanner.getToken() !== Json.SyntaxKind.NumericLiteral) { + return undefined; + } + + const node = new NumberASTNodeImpl(parent, scanner.getTokenOffset()); + if (scanner.getTokenError() === Json.ScanError.None) { + const tokenValue = scanner.getTokenValue(); + try { + const numberValue = JSON.parse(tokenValue); + if (!isNumber(numberValue)) { + return _error('Invalid number format.', ErrorCode.Undefined, node); + } + node.value = numberValue; + } catch (e) { + return _error('Invalid number format.', ErrorCode.Undefined, node); + } + node.isInteger = tokenValue.indexOf('.') === -1; + } + return _finalize(node, true); + } + + function _parseLiteral(parent: ASTNode | undefined): ASTNode | undefined { + let node: ASTNodeImpl; + switch (scanner.getToken()) { + case Json.SyntaxKind.NullKeyword: + return _finalize(new NullASTNodeImpl(parent, scanner.getTokenOffset()), true); + case Json.SyntaxKind.TrueKeyword: + return _finalize(new BooleanASTNodeImpl(parent, true, scanner.getTokenOffset()), true); + case Json.SyntaxKind.FalseKeyword: + return _finalize(new BooleanASTNodeImpl(parent, false, scanner.getTokenOffset()), true); + default: + return undefined; + } + } + + function _parseValue(parent: ASTNode | undefined): ASTNode | undefined { + return ( + _parseArray(parent) || + _parseObject(parent) || + _parseString(parent) || + _parseNumber(parent) || + _parseLiteral(parent) + ); + } + + let _root: ASTNode | undefined = undefined; + + const token = _scanNext(); + + if (token !== Json.SyntaxKind.EOF) { + _root = _parseValue(_root); + if (!_root) { + _error('Expected a JSON object, array or literal', ErrorCode.Undefined); + } else if (scanner.getToken() !== Json.SyntaxKind.EOF) { + _error('End of file expected.', ErrorCode.Undefined); + } + } + + return { + problems, + root: new JsonDocument(_root), + }; +} diff --git a/packages/semi-json-viewer-core/src/tokens/index.md b/packages/semi-json-viewer-core/src/tokens/index.md new file mode 100644 index 0000000000..7bfe0c54bf --- /dev/null +++ b/packages/semi-json-viewer-core/src/tokens/index.md @@ -0,0 +1,43 @@ +# JSON Viewer Tokens 模块总结 + +## 核心功能 +这个模块主要负责 JSON 文本的词法分析(tokenization)和语法高亮,基于 VS Code 的实现进行了定制化改造。 + +## 主要模块 + +### 1. tokenize.ts +- 定义了基础的词法分析支持接口和状态管理 +- 实现了 JSON 的词法分析器,可以识别以下类型的 token: + - 分隔符 (括号、冒号、逗号等) + - 关键字 (null, true, false) + - 字符串 + - 数字 + - 注释 (行注释和块注释) +- 支持多层级的括号颜色区分 + +### 2. tokenizationJsonModelPart.ts +- 管理整个文档的词法分析状态 +- 提供了 token 存储和更新的接口 +- 处理文档内容变化时的 token 重新计算 + +### 3. jsonModelToken.ts +- 实现了带状态追踪的词法分析器 +- 提供了后台分析的功能,通过 `IdleDeadline` 来避免阻塞主线程 +- 包含了 token 状态的缓存管理 +- 实现了增量式的词法分析,只重新分析发生变化的部分 + +### 4. offsetRange.ts +- 提供了处理偏移量范围的工具类 +- 支持范围的各种运算操作: + - 合并 (join) + - 相交 (intersect) + - 偏移 (delta) + - 包含判断等 +- 用于精确控制需要重新分析的文本范围 + +## 工作流程 +1. 当 JSON 文本发生变化时,系统会标记受影响的行号范围 +2. 后台分析器会在空闲时间对这些行进行重新分析 +3. 分析结果会被缓存,并触发界面更新 +4. 整个过程是增量式的,只处理必要的部分,保证了性能 + diff --git a/packages/semi-json-viewer-core/src/tokens/jsonModelToken.ts b/packages/semi-json-viewer-core/src/tokens/jsonModelToken.ts new file mode 100644 index 0000000000..a141bc50cd --- /dev/null +++ b/packages/semi-json-viewer-core/src/tokens/jsonModelToken.ts @@ -0,0 +1,306 @@ +/** reference from https://github.com/microsoft/vscode */ +import { JSONModel } from '../model/jsonModel'; +import { JSONState, JsonTokenizationSupport } from './tokenize'; +import { OffsetRange } from './offsetRange'; +import { runWhenGlobalIdle } from '../common/async'; +import { IBackgroundTokenizationStore } from './tokenizationJsonModelPart'; +import { StopWatch } from '../common/stopWatch'; + +export class TokenizerWithStateStore { + private readonly initialState: JSONState; + public readonly store: TrackingTokenizationStateStore; + constructor(lineCount: number, public readonly tokenizationSupport: JsonTokenizationSupport) { + this.initialState = tokenizationSupport.getInitialState(); + this.store = new TrackingTokenizationStateStore(lineCount); + } + + public getStartState(lineNumber: number): JSONState { + return this.store.getStartState(lineNumber, this.initialState); + } + + public getFirstInvalidLine(): { + lineNumber: number; + startState: JSONState + } | null { + return this.store.getFirstInvalidLine(this.initialState); + } +} + +export class JsonTokenizerWithStateStoreAndModel extends TokenizerWithStateStore { + constructor( + lineCount: number, + tokenizationSupport: JsonTokenizationSupport, + public readonly _jsonModel: JSONModel + ) { + super(lineCount, tokenizationSupport); + } + + public updateTokensUntilLine(lineNumber: number, backgroundTokenizationStore: IBackgroundTokenizationStore): void { + while (true) { + const lineToTokenize = this.getFirstInvalidLine(); + + if (!lineToTokenize || lineToTokenize.lineNumber > lineNumber) { + break; + } + + const text = this._jsonModel.getLineContent(lineToTokenize.lineNumber); + const result = this.tokenizationSupport.tokenize(text, lineToTokenize.startState); + backgroundTokenizationStore.setTokens(lineToTokenize.lineNumber, result.tokens); + + this.store.setEndState(lineToTokenize.lineNumber, result.endState); + } + } +} + +export class TrackingTokenizationStateStore { + private readonly _tokenizationStateStore = new TokenizationStateStore(); + private readonly _invalidatedLines = new RangePriorityQueue(); + constructor(private lineCount: number) { + this._invalidatedLines.addRange(new OffsetRange(1, lineCount + 1)); + } + + public getEndState(lineNumber: number): JSONState { + return this._tokenizationStateStore.getEndState(lineNumber); + } + + public setEndState(lineNumber: number, state: JSONState): boolean { + this._invalidatedLines.delete(lineNumber); + const result = this._tokenizationStateStore.setEndState(lineNumber, state); + if (result && lineNumber < this.lineCount) { + this._invalidatedLines.addRange(new OffsetRange(lineNumber + 1, lineNumber + 2)); + } + return result; + } + + public getStartState(lineNumber: number, initialState: JSONState): JSONState { + if (lineNumber === 1) { + return initialState; + } + return this.getEndState(lineNumber - 1); + } + + public getFirstInvalidEndStateLineNumber(): number | null { + return this._invalidatedLines.min; + } + + public getFirstInvalidLine( + initialState: JSONState + ): { + lineNumber: number; + startState: JSONState + } | null { + const lineNumber = this.getFirstInvalidEndStateLineNumber(); + if (lineNumber === null) { + return null; + } + const startState = this.getStartState(lineNumber, initialState); + if (!startState) { + throw new Error('Start state must be defined'); + } + return { + lineNumber, + startState: this.getStartState(lineNumber, initialState), + }; + } + + public allStatesValid(): boolean { + return this._invalidatedLines.min === null; + } + + public invalidateRange({ from, to }: { from: number; to: number }): void { + this._invalidatedLines.addRange(new OffsetRange(from, to)); + } +} + +export class TokenizationStateStore { + private readonly _lineEndState = new Array(); + + public getEndState(lineNumber: number): JSONState { + return this._lineEndState[lineNumber]; + } + + public setEndState(lineNumber: number, state: JSONState): boolean { + const oldState = this._lineEndState[lineNumber]; + if (oldState && oldState.equals(state)) { + return false; + } + this._lineEndState[lineNumber] = state; + return true; + } +} + +export class RangePriorityQueue { + private readonly _ranges: OffsetRange[] = []; + + public getRange(): OffsetRange[] { + return this._ranges; + } + + public addRange(range: OffsetRange): void { + OffsetRange.addRange(range, this._ranges); + } + + public get min(): number | null { + return this._ranges[0]?.start ?? null; + } + /** + * 现有的范围集合中添加一个新的范围 + * @param range + * @param newLength + */ + public addRangeAndResize(range: OffsetRange, newLength: number): void { + // 找到第一个可能与新范围相交的范围 + let idxFirstMightBeIntersecting = 0; + while ( + !( + idxFirstMightBeIntersecting >= this._ranges.length || + range.start <= this._ranges[idxFirstMightBeIntersecting].endExclusive + ) + ) { + idxFirstMightBeIntersecting++; + } + // 找到第一个在新范围之后,且与新范围不相交的范围 + let idxFirstIsAfter = idxFirstMightBeIntersecting; + while (!(idxFirstIsAfter >= this._ranges.length || range.endExclusive < this._ranges[idxFirstIsAfter].start)) { + idxFirstIsAfter++; + } + // 计算新范围与旧范围的差值 + const delta = newLength - range.length; + // 将所有在新范围之后的范围进行调整 + + for (let i = idxFirstIsAfter; i < this._ranges.length; i++) { + this._ranges[i] = this._ranges[i].delta(delta); + } + + if (idxFirstMightBeIntersecting === idxFirstIsAfter) { + const newRange = new OffsetRange(range.start, range.start + newLength); + if (!newRange.isEmpty) { + this._ranges.splice(idxFirstMightBeIntersecting, 0, newRange); + } + } else { + const start = Math.min(range.start, this._ranges[idxFirstMightBeIntersecting].start); + const endEx = Math.max(range.endExclusive, this._ranges[idxFirstIsAfter - 1].endExclusive); + // 创建一个新的范围,并将其添加到范围集合中 + const newRange = new OffsetRange(start, endEx + delta); + if (!newRange.isEmpty) { + this._ranges.splice( + idxFirstMightBeIntersecting, + idxFirstIsAfter - idxFirstMightBeIntersecting, + newRange + ); + } else { + this._ranges.splice(idxFirstMightBeIntersecting, idxFirstIsAfter - idxFirstMightBeIntersecting); + } + } + } + /** + * 删除一个值 + * @param value + */ + public delete(value: number): void { + // 找到第一个包含该值的范围 + const idx = this._ranges.findIndex(r => r.contains(value)); + if (idx !== -1) { + const range = this._ranges[idx]; + // 如果该值正好是范围的开始 + if (range.start === value) { + //如果范围长度为1,直接删除整个范围。 + //否则,将范围的起始点向后移动一位。 + if (range.endExclusive === value + 1) { + this._ranges.splice(idx, 1); + } else { + this._ranges[idx] = new OffsetRange(value + 1, range.endExclusive); + } + } else { + // 如果该值在范围的中间 + // 如果该值正好是范围的结束 + if (range.endExclusive === value + 1) { + this._ranges[idx] = new OffsetRange(range.start, value); + } else { + // 否则,将范围分成两个范围 + this._ranges.splice( + idx, + 1, + new OffsetRange(range.start, value), + new OffsetRange(value + 1, range.endExclusive) + ); + } + } + } + } +} + +export class JsonBackgroundTokenizer { + constructor( + private readonly _jsonTokenizerWithStateStoreAndModel: JsonTokenizerWithStateStoreAndModel, + private readonly _backgroundTokenizationStore: IBackgroundTokenizationStore + ) {} + + public handleChanges(): void { + this._beginBackgroundTokenization(); + } + + private _beginBackgroundTokenization(): void { + runWhenGlobalIdle((deadline: IdleDeadline) => { + this._backgroundTokenizeWithDeadline(deadline); + }); + } + + private _backgroundTokenizeWithDeadline(deadline: IdleDeadline): void { + const endTime = Date.now() + deadline.timeRemaining(); + + const execute = () => { + if (!this._hasLinesToTokenize()) return; + this._backgroundTokenize(); + + if (Date.now() < endTime) { + setTimeout(execute); + } else { + this._beginBackgroundTokenization(); + } + }; + execute(); + } + + private _backgroundTokenize(): void { + const lineCount = this._jsonTokenizerWithStateStoreAndModel._jsonModel.getLineCount(); + const stopWatch = StopWatch.create(true); + do { + if (stopWatch.elapsed() > 1) { + break; + } + const tokenizedNumber = this._tokenizeOneInvalidLine(); + if (tokenizedNumber > lineCount) { + break; + } + } while (this._hasLinesToTokenize()); + } + + private _hasLinesToTokenize(): boolean { + if (!this._jsonTokenizerWithStateStoreAndModel) { + return false; + } + return !this._jsonTokenizerWithStateStoreAndModel.store.allStatesValid(); + } + + private _tokenizeOneInvalidLine(): number { + const firstInvalidLine = this._jsonTokenizerWithStateStoreAndModel.getFirstInvalidLine(); + + if (!firstInvalidLine) { + return this._jsonTokenizerWithStateStoreAndModel._jsonModel.getLineCount() + 1; + } + //TODO builder + this._jsonTokenizerWithStateStoreAndModel.updateTokensUntilLine( + firstInvalidLine.lineNumber, + this._backgroundTokenizationStore + ); + return firstInvalidLine.lineNumber; + } + + public requestTokens({ from, to }: { from: number; to: number }): void { + this._jsonTokenizerWithStateStoreAndModel.store.invalidateRange({ + from: from === 1 ? 1 : from - 1, + to: to + 1, + }); + } +} diff --git a/packages/semi-json-viewer-core/src/tokens/offsetRange.ts b/packages/semi-json-viewer-core/src/tokens/offsetRange.ts new file mode 100644 index 0000000000..25bbe69827 --- /dev/null +++ b/packages/semi-json-viewer-core/src/tokens/offsetRange.ts @@ -0,0 +1,238 @@ +/** reference from https://github.com/microsoft/vscode */ +export interface IOffsetRange { + readonly start: number; + readonly endExclusive: number +} + +/** + * A range of offsets (0-based). + */ +export class OffsetRange implements IOffsetRange { + public static addRange(range: OffsetRange, sortedRanges: OffsetRange[]): void { + let i = 0; + while (i < sortedRanges.length && sortedRanges[i].endExclusive < range.start) { + i++; + } + let j = i; + while (j < sortedRanges.length && sortedRanges[j].start <= range.endExclusive) { + j++; + } + if (i === j) { + sortedRanges.splice(i, 0, range); + } else { + const start = Math.min(range.start, sortedRanges[i].start); + const end = Math.max(range.endExclusive, sortedRanges[j - 1].endExclusive); + sortedRanges.splice(i, j - i, new OffsetRange(start, end)); + } + } + + public static tryCreate(start: number, endExclusive: number): OffsetRange | undefined { + if (start > endExclusive) { + return undefined; + } + return new OffsetRange(start, endExclusive); + } + + public static ofLength(length: number): OffsetRange { + return new OffsetRange(0, length); + } + + public static ofStartAndLength(start: number, length: number): OffsetRange { + return new OffsetRange(start, start + length); + } + + constructor(public readonly start: number, public readonly endExclusive: number) { + if (start > endExclusive) { + throw new Error(`Invalid range: ${this.toString()}`); + } + } + + get isEmpty(): boolean { + return this.start === this.endExclusive; + } + + public delta(offset: number): OffsetRange { + return new OffsetRange(this.start + offset, this.endExclusive + offset); + } + + public deltaStart(offset: number): OffsetRange { + return new OffsetRange(this.start + offset, this.endExclusive); + } + + public deltaEnd(offset: number): OffsetRange { + return new OffsetRange(this.start, this.endExclusive + offset); + } + + public get length(): number { + return this.endExclusive - this.start; + } + + public toString() { + return `[${this.start}, ${this.endExclusive})`; + } + + public equals(other: OffsetRange): boolean { + return this.start === other.start && this.endExclusive === other.endExclusive; + } + + public containsRange(other: OffsetRange): boolean { + return this.start <= other.start && other.endExclusive <= this.endExclusive; + } + + public contains(offset: number): boolean { + return this.start <= offset && offset < this.endExclusive; + } + + /** + * for all numbers n: range1.contains(n) or range2.contains(n) => range1.join(range2).contains(n) + * The joined range is the smallest range that contains both ranges. + */ + public join(other: OffsetRange): OffsetRange { + return new OffsetRange(Math.min(this.start, other.start), Math.max(this.endExclusive, other.endExclusive)); + } + + /** + * for all numbers n: range1.contains(n) and range2.contains(n) <=> range1.intersect(range2).contains(n) + * + * The resulting range is empty if the ranges do not intersect, but touch. + * If the ranges don't even touch, the result is undefined. + */ + public intersect(other: OffsetRange): OffsetRange | undefined { + const start = Math.max(this.start, other.start); + const end = Math.min(this.endExclusive, other.endExclusive); + if (start <= end) { + return new OffsetRange(start, end); + } + return undefined; + } + + public intersects(other: OffsetRange): boolean { + const start = Math.max(this.start, other.start); + const end = Math.min(this.endExclusive, other.endExclusive); + return start < end; + } + + public intersectsOrTouches(other: OffsetRange): boolean { + const start = Math.max(this.start, other.start); + const end = Math.min(this.endExclusive, other.endExclusive); + return start <= end; + } + + public isBefore(other: OffsetRange): boolean { + return this.endExclusive <= other.start; + } + + public isAfter(other: OffsetRange): boolean { + return this.start >= other.endExclusive; + } + + public slice(arr: T[]): T[] { + return arr.slice(this.start, this.endExclusive); + } + + public substring(str: string): string { + return str.substring(this.start, this.endExclusive); + } + + /** + * Returns the given value if it is contained in this instance, otherwise the closest value that is contained. + * The range must not be empty. + */ + public clip(value: number): number { + if (this.isEmpty) { + throw new Error(`Invalid clipping range: ${this.toString()}`); + } + return Math.max(this.start, Math.min(this.endExclusive - 1, value)); + } + + /** + * Returns `r := value + k * length` such that `r` is contained in this range. + * The range must not be empty. + * + * E.g. `[5, 10).clipCyclic(10) === 5`, `[5, 10).clipCyclic(11) === 6` and `[5, 10).clipCyclic(4) === 9`. + */ + public clipCyclic(value: number): number { + if (this.isEmpty) { + throw new Error(`Invalid clipping range: ${this.toString()}`); + } + if (value < this.start) { + return this.endExclusive - ((this.start - value) % this.length); + } + if (value >= this.endExclusive) { + return this.start + ((value - this.start) % this.length); + } + return value; + } + + public map(f: (offset: number) => T): T[] { + const result: T[] = []; + for (let i = this.start; i < this.endExclusive; i++) { + result.push(f(i)); + } + return result; + } + + public forEach(f: (offset: number) => void): void { + for (let i = this.start; i < this.endExclusive; i++) { + f(i); + } + } +} + +export class OffsetRangeSet { + private readonly _sortedRanges: OffsetRange[] = []; + + public addRange(range: OffsetRange): void { + let i = 0; + while (i < this._sortedRanges.length && this._sortedRanges[i].endExclusive < range.start) { + i++; + } + let j = i; + while (j < this._sortedRanges.length && this._sortedRanges[j].start <= range.endExclusive) { + j++; + } + if (i === j) { + this._sortedRanges.splice(i, 0, range); + } else { + const start = Math.min(range.start, this._sortedRanges[i].start); + const end = Math.max(range.endExclusive, this._sortedRanges[j - 1].endExclusive); + this._sortedRanges.splice(i, j - i, new OffsetRange(start, end)); + } + } + + public toString(): string { + return this._sortedRanges.map(r => r.toString()).join(', '); + } + + /** + * Returns of there is a value that is contained in this instance and the given range. + */ + public intersectsStrict(other: OffsetRange): boolean { + // TODO use binary search + let i = 0; + while (i < this._sortedRanges.length && this._sortedRanges[i].endExclusive <= other.start) { + i++; + } + return i < this._sortedRanges.length && this._sortedRanges[i].start < other.endExclusive; + } + + public intersectWithRange(other: OffsetRange): OffsetRangeSet { + // TODO use binary search + slice + const result = new OffsetRangeSet(); + for (const range of this._sortedRanges) { + const intersection = range.intersect(other); + if (intersection) { + result.addRange(intersection); + } + } + return result; + } + + public intersectWithRangeLength(other: OffsetRange): number { + return this.intersectWithRange(other).length; + } + + public get length(): number { + return this._sortedRanges.reduce((prev, cur) => prev + cur.length, 0); + } +} diff --git a/packages/semi-json-viewer-core/src/tokens/tokenizationJsonModelPart.ts b/packages/semi-json-viewer-core/src/tokens/tokenizationJsonModelPart.ts new file mode 100644 index 0000000000..889e9661d9 --- /dev/null +++ b/packages/semi-json-viewer-core/src/tokens/tokenizationJsonModelPart.ts @@ -0,0 +1,114 @@ +/** Based on https://github.com/microsoft/vscode with modifications for custom requirements */ +import { JSONModel } from '../model/jsonModel'; +import { JsonBackgroundTokenizer, JsonTokenizerWithStateStoreAndModel } from './jsonModelToken'; +import { Token } from './tokenize'; +import { createTokenizationSupport } from './tokenize'; +import { GlobalEvents, IModelContentChangeEvent } from '../common/emitterEvents'; +import { Emitter, getEmitter } from '../common/emitter'; + +export interface IBackgroundTokenizationStore { + setTokens(lineNumber: number, tokens: Token[]): void + + // setEndState(lineNumber: number, state: JSONState): void; + + /** + * Should be called to indicate that the background tokenization has finished for now. + * (This triggers bracket pair colorization to re-parse the bracket pairs with token information) + */ + // backgroundTokenizationFinished(): void; +} + +export class TokenizationJsonModelPart { + private readonly tokens: GrammarTokens; + private _jsonModel: JSONModel | null = null; + private emitter: Emitter = getEmitter(); + constructor(jsonModel: JSONModel) { + this._jsonModel = jsonModel; + this.tokens = new GrammarTokens(this._jsonModel); + } + + public getLineTokens(lineNumber: number): Token[] { + return this.tokens.getLineTokens(lineNumber); + } + + public handleDidChangeContent(e: IModelContentChangeEvent) { + this.tokens.handleDidChangeContent(e); + } + + public forceTokenize(lineNumber: number) { + this.tokens.forceTokenize(lineNumber); + } + + public requestTokens(range: { from: number; to: number }) { + this.tokens.backgroundTokenizer?.requestTokens(range); + } +} + +export class GrammarTokens { + private _tokens: Map = new Map(); + private _tokenizer: JsonTokenizerWithStateStoreAndModel | null = null; + private _backgroundTokenizer: JsonBackgroundTokenizer | null = null; + private _jsonModel: JSONModel; + private emitter: Emitter = getEmitter(); + constructor(jsonModel: JSONModel) { + this._jsonModel = jsonModel; + this.emitter.on('contentChanged', (e: IModelContentChangeEvent | IModelContentChangeEvent[]) => { + let from = 0; + let to = this._jsonModel.getLineCount(); + if (Array.isArray(e)) { + from = e[e.length - 1].range.startLineNumber; + } else { + from = e.range.startLineNumber; + } + this._backgroundTokenizer?.requestTokens({ + from, + to, + }); + this._backgroundTokenizer?.handleChanges(); + }); + this.resetTokenization(); + } + + public get backgroundTokenizer() { + return this._backgroundTokenizer; + } + + public resetTokenization() { + this._tokens.clear(); + const JsonTokenizationSupport = createTokenizationSupport(true); + const initialState = JsonTokenizationSupport.getInitialState(); + if (JsonTokenizationSupport && initialState) { + this._tokenizer = new JsonTokenizerWithStateStoreAndModel( + this._jsonModel.getLineCount(), + JsonTokenizationSupport, + this._jsonModel + ); + } + + const b: IBackgroundTokenizationStore = { + setTokens: (lineNumber: number, tokens: Token[]) => { + this._tokens.set(lineNumber, tokens); + }, + }; + if (this._tokenizer) { + this._backgroundTokenizer = new JsonBackgroundTokenizer(this._tokenizer, b); + this._backgroundTokenizer.handleChanges(); + } + } + public getLineTokens(lineNumber: number): Token[] { + return this._tokens.get(lineNumber) || []; + } + + public handleDidChangeContent(e: IModelContentChangeEvent) { + this._backgroundTokenizer?.handleChanges(); + } + + public forceTokenize(lineNumber: number) { + const b: IBackgroundTokenizationStore = { + setTokens: (lineNumber: number, tokens: Token[]) => { + this._tokens.set(lineNumber, tokens); + }, + }; + this._tokenizer?.updateTokensUntilLine(lineNumber, b); + } +} diff --git a/packages/semi-json-viewer-core/src/tokens/tokenize.ts b/packages/semi-json-viewer-core/src/tokens/tokenize.ts new file mode 100644 index 0000000000..cf79c66587 --- /dev/null +++ b/packages/semi-json-viewer-core/src/tokens/tokenize.ts @@ -0,0 +1,282 @@ +/** Based on https://github.com/microsoft/vscode with modifications for custom requirements */ +import * as json from 'jsonc-parser'; + +export interface Token { + scopes: string; + startIndex: number +} + +export interface JsonTokenizationSupport { + getInitialState(): JSONState; + tokenize(line: string, state: JSONState): TokenizationResult +} + +export class TokenizationResult { + constructor(public readonly tokens: Token[], public readonly endState: JSONState) {} +} + +export function createTokenizationSupport(supportComments: boolean): JsonTokenizationSupport { + return { + getInitialState: () => new JSONState(null, null, false, null), + tokenize: (line: string, state?: JSONState) => tokenize(supportComments, line, state), + }; +} +export interface IState { + clone(): IState; + equals(other: IState): boolean +} + +export const TOKEN_DELIM_OBJECT = 'semi-json-viewer-delimiter-bracket'; +export const TOKEN_DELIM_ARRAY = 'semi-json-viewer-delimiter-array'; +export const TOKEN_DELIM_COLON = 'semi-json-viewer-delimiter-colon'; +export const TOKEN_DELIM_COMMA = 'semi-json-viewer-delimiter-comma'; +export const TOKEN_VALUE_BOOLEAN = 'semi-json-viewer-keyword'; +export const TOKEN_VALUE_NULL = 'semi-json-viewer-keyword'; +export const TOKEN_VALUE_STRING = 'semi-json-viewer-string-value'; +export const TOKEN_VALUE_NUMBER = 'semi-json-viewer-number'; +export const TOKEN_PROPERTY_NAME = 'semi-json-viewer-string-key'; +export const TOKEN_COMMENT_BLOCK = 'semi-json-viewer-comment-block'; +export const TOKEN_COMMENT_LINE = 'semi-json-viewer-comment-line'; + +const enum JSONParent { + Object = 0, + Array = 1, +} + +class ParentsStack { + constructor( + public readonly parent: ParentsStack | null, + public readonly type: JSONParent, + public readonly depth: number + ) {} + + public static pop(parents: ParentsStack | null): ParentsStack | null { + if (parents) { + return parents.parent; + } + return null; + } + + public static push(parents: ParentsStack | null, type: JSONParent): ParentsStack { + return new ParentsStack(parents, type, parents ? parents.depth + 1 : 0); + } + + public static equals(a: ParentsStack | null, b: ParentsStack | null): boolean { + if (!a && !b) { + return true; + } + if (!a || !b) { + return false; + } + while (a && b) { + if (a.type !== b.type || a.depth !== b.depth) { + return false; + } + a = a.parent; + b = b.parent; + } + return a === null && b === null; + } +} + +export class JSONState { + private _state: IState | null; + + public scanError: ScanError | null; + public lastWasColon: boolean; + public parents: ParentsStack | null; + + constructor( + state: IState | null, + scanError: ScanError | null, + lastWasColon: boolean, + parents: ParentsStack | null + ) { + this._state = state; + this.scanError = scanError; + this.lastWasColon = lastWasColon; + this.parents = parents; + } + + public clone(): JSONState { + return new JSONState(this._state, this.scanError, this.lastWasColon, this.parents); + } + + public equals(other: IState): boolean { + if (other === this) { + return true; + } + if (!other || !(other instanceof JSONState)) { + return false; + } + return ( + this.scanError === other.scanError && + this.lastWasColon === other.lastWasColon && + ParentsStack.equals(this.parents, other.parents) + ); + } + + public getStateData(): IState | null { + return this._state; + } + + public setStateData(state: IState): void { + this._state = state; + } +} + +const enum ScanError { + None = 0, + UnexpectedEndOfComment = 1, + UnexpectedEndOfString = 2, + UnexpectedEndOfNumber = 3, + InvalidUnicode = 4, + InvalidEscapeCharacter = 5, + InvalidCharacter = 6, +} + +const enum SyntaxKind { + OpenBraceToken = 1, + CloseBraceToken = 2, + OpenBracketToken = 3, + CloseBracketToken = 4, + CommaToken = 5, + ColonToken = 6, + NullKeyword = 7, + TrueKeyword = 8, + FalseKeyword = 9, + StringLiteral = 10, + NumericLiteral = 11, + LineCommentTrivia = 12, + BlockCommentTrivia = 13, + LineBreakTrivia = 14, + Trivia = 15, + Unknown = 16, + EOF = 17, +} + +function tokenize(comments: boolean, line: string, state: JSONState, offsetDelta: number = 0) { + // handle multiline strings and block comments + let numberOfInsertedCharacters = 0; + let adjustOffset = false; + + switch (state.scanError) { + case ScanError.UnexpectedEndOfString: + line = '"' + line; + numberOfInsertedCharacters = 1; + break; + case ScanError.UnexpectedEndOfComment: + line = '/*' + line; + numberOfInsertedCharacters = 2; + break; + } + + const scanner = json.createScanner(line); + let lastWasColon = state.lastWasColon; + let parents = state.parents; + + const ret = { + tokens: [], + endState: state.clone(), + }; + + while (true) { + let offset = offsetDelta + scanner.getPosition(); + let type = ''; + + const kind = (scanner.scan()); + if (kind === SyntaxKind.EOF) { + break; + } + + // Check that the scanner has advanced + if (offset === offsetDelta + scanner.getPosition()) { + throw new Error('Scanner did not advance, next 3 characters are: ' + line.substr(scanner.getPosition(), 3)); + } + + // In case we inserted /* or " character, we need to + // adjust the offset of all tokens (except the first) + if (adjustOffset) { + offset -= numberOfInsertedCharacters; + } + adjustOffset = numberOfInsertedCharacters > 0; + + // brackets and type + switch (kind) { + case SyntaxKind.OpenBraceToken: + parents = ParentsStack.push(parents, JSONParent.Object); + //TODO: 颜色根据depth变化 目前写死层级最大为3 + type = `${TOKEN_DELIM_OBJECT}-${parents ? parents.depth % 3 : 0}`; + lastWasColon = false; + break; + case SyntaxKind.CloseBraceToken: + type = `${TOKEN_DELIM_OBJECT}-${parents ? parents.depth % 3 : 0}`; + parents = ParentsStack.pop(parents); + lastWasColon = false; + break; + case SyntaxKind.OpenBracketToken: + parents = ParentsStack.push(parents, JSONParent.Array); + type = `${TOKEN_DELIM_ARRAY}-${parents ? parents.depth % 3 : 0}`; + lastWasColon = false; + break; + case SyntaxKind.CloseBracketToken: + type = `${TOKEN_DELIM_ARRAY}-${parents ? parents.depth % 3 : 0}`; + parents = ParentsStack.pop(parents); + lastWasColon = false; + break; + case SyntaxKind.ColonToken: + type = TOKEN_DELIM_COLON; + lastWasColon = true; + break; + case SyntaxKind.CommaToken: + type = TOKEN_DELIM_COMMA; + lastWasColon = false; + break; + case SyntaxKind.TrueKeyword: + case SyntaxKind.FalseKeyword: + type = TOKEN_VALUE_BOOLEAN; + lastWasColon = false; + break; + case SyntaxKind.NullKeyword: + type = TOKEN_VALUE_NULL; + lastWasColon = false; + break; + case SyntaxKind.StringLiteral: + const currentParent = parents ? parents.type : JSONParent.Object; + const inArray = currentParent === JSONParent.Array; + type = lastWasColon || inArray ? TOKEN_VALUE_STRING : TOKEN_PROPERTY_NAME; + lastWasColon = false; + break; + case SyntaxKind.NumericLiteral: + type = TOKEN_VALUE_NUMBER; + lastWasColon = false; + break; + } + + // comments, iff enabled + if (comments) { + switch (kind) { + case SyntaxKind.LineCommentTrivia: + type = TOKEN_COMMENT_LINE; + break; + case SyntaxKind.BlockCommentTrivia: + type = TOKEN_COMMENT_BLOCK; + break; + } + } + + ret.endState = new JSONState( + state.getStateData(), + (scanner.getTokenError()), + lastWasColon, + parents + ); + // @ts-ignore + ret.tokens.push({ + startIndex: offset, + scopes: type, + }); + } + + return ret; +} diff --git a/packages/semi-json-viewer-core/src/view/complete/completeWidget.ts b/packages/semi-json-viewer-core/src/view/complete/completeWidget.ts new file mode 100644 index 0000000000..61f9aa94f6 --- /dev/null +++ b/packages/semi-json-viewer-core/src/view/complete/completeWidget.ts @@ -0,0 +1,206 @@ +import { GlobalEvents, IModelContentChangeEvent } from '../../common/emitterEvents'; +import { elt, setStyles } from '../../common/dom'; +import { JSONModel } from '../../model/jsonModel'; +import { CompletionItem, CompletionItemKind, TextEdit } from '../../service/jsonTypes'; +import { View } from '../view'; +import { Emitter, getEmitter } from '../../common/emitter'; +import { doValidate, parseJsonAst } from '../../service/jsonService'; +import { JSONCompletion } from '../../service/completion'; +import { SelectionModel } from '../../model/selectionModel'; +import { Position } from '../../common/position'; + +/** + * CompleteWidget 类用于管理 JSON Viewer 中的补全功能 + */ +export class CompleteWidget { + private _view: View; + private _jsonModel: JSONModel; + private _selectionModel: SelectionModel; + private _container: HTMLElement; + private _suggestionsContainer: HTMLElement; + private _selectedIndex: number = 0; + private _suggestions: CompletionItem[] = []; + public isVisible: boolean = false; + private emitter: Emitter = getEmitter(); + + constructor(view: View, jsonModel: JSONModel, selectionModel: SelectionModel) { + this._view = view; + this._jsonModel = jsonModel; + this._selectionModel = selectionModel; + + this._container = this.createCompleteContainer(); + this._suggestionsContainer = this.createSuggestionsContainer(); + this._container.appendChild(this._suggestionsContainer); + this._view.jsonViewerDom.appendChild(this._container); + + this._attachEventListeners(); + } + + private _attachEventListeners() { + const shouldTrigger = (e: IModelContentChangeEvent): boolean => { + // 不是插入操作,不触发 + if (e.type !== 'insert') { + return false; + } + // 不是单个字符,不触发 + if (e.newText.length !== 1) { + return false; + } + // 是空白字符(空格、制表符、换行等),不触发 + if (/\s/.test(e.newText)) { + return false; + } + return true; + }; + + this.emitter.on('contentChanged', e => { + // 如果是批量操作,直接返回 + if (Array.isArray(e)) { + return; + } + + if (!shouldTrigger(e)) { + // 不符合触发条件时,隐藏补全框 + this.hide(); + return; + } + + this._fetchCompletions(); + }); + } + + private _fetchCompletions() { + const root = parseJsonAst(this._jsonModel); + const position = { + lineNumber: this._jsonModel.lastChangeBufferPos.lineNumber, + column: this._jsonModel.lastChangeBufferPos.column, + } as Position; + new JSONCompletion(this._view.options?.completionOptions || null) + .doCompletion(this._jsonModel, position, root) + .then(completions => { + this._suggestions = completions.items || []; + this.show(); + }); + } + + private _calculatePosition(): { x: number; y: number } { + const selection = window.getSelection(); + if (!selection || !selection.rangeCount) return { x: 0, y: 0 }; + + const range = selection.getRangeAt(0); + const rect = range.getBoundingClientRect(); + + // 获取编辑器容器的位置 + const editorRect = this._view.contentDom.getBoundingClientRect(); + + // 计算补全框的位置(相对于编辑器容器) + const x = rect.left - editorRect.left + 50; + const y = rect.bottom - editorRect.top; + return { x, y }; + } + + private createCompleteContainer(): HTMLElement { + const className = 'semi-json-viewer-complete-container'; + const container = elt('div', className); + setStyles(container, { + display: 'none', + }); + return container; + } + + private createSuggestionsContainer(): HTMLElement { + const className = 'semi-json-viewer-complete-suggestions-container'; + const container = elt('div', className); + setStyles(container, { + maxHeight: '200px', + overflowY: 'auto', + }); + return container; + } + + public show() { + if (this._suggestions.length === 0) { + return; + } + const { x, y } = this._calculatePosition(); + if (x < 0 || y < 0) { + return; + } + this.isVisible = true; + // 更新位置和内容 + setStyles(this._container, { + left: `${x}px`, + top: `${y}px`, + display: 'block', + }); + + // 清空并添加新的建议 + this._suggestionsContainer.innerHTML = ''; + this._renderCompletions(); + } + + public _handleKeyDown = (e: KeyboardEvent) => { + switch (e.key) { + case 'ArrowDown': + e.preventDefault(); + this._selectedIndex = (this._selectedIndex + 1) % this._suggestions.length; + this._renderCompletions(); + break; + case 'ArrowUp': + e.preventDefault(); + this._selectedIndex = (this._selectedIndex - 1 + this._suggestions.length) % this._suggestions.length; + this._renderCompletions(); + break; + case 'Enter': + case 'Tab': + e.preventDefault(); + const selectedItem = this._suggestions[this._selectedIndex]; + const { textEdit } = selectedItem; + if (!textEdit) { + return; + } + const { range } = textEdit; + const startOffset = this._jsonModel.getOffsetAt(range.startLineNumber, range.startColumn); + + const endOffset = this._jsonModel.getOffsetAt(range.endLineNumber, range.endColumn); + + const op: IModelContentChangeEvent = { + type: 'replace', + range: { + startLineNumber: range.startLineNumber, + startColumn: range.startColumn, + endLineNumber: range.endLineNumber, + endColumn: range.endColumn, + }, + rangeLength: endOffset - startOffset, + rangeOffset: startOffset, + oldText: this._jsonModel.getValueInRange(range), + newText: textEdit?.newText || '', + }; + this._jsonModel.applyOperation(op); + this.hide(); + break; + } + }; + + private _renderCompletions() { + const className = 'semi-json-viewer-complete-suggestions-item'; + this._suggestionsContainer.innerHTML = this._suggestions + .map( + (item, index) => ` +

  • + ${item.label} +
  • + ` + ) + .join(''); + } + + public hide() { + this.isVisible = false; + this._container.style.display = 'none'; + this._suggestions = []; + } +} diff --git a/packages/semi-json-viewer-core/src/view/edit/editWidget.ts b/packages/semi-json-viewer-core/src/view/edit/editWidget.ts new file mode 100644 index 0000000000..ddbbddc4a4 --- /dev/null +++ b/packages/semi-json-viewer-core/src/view/edit/editWidget.ts @@ -0,0 +1,413 @@ +import { SelectionModel } from '../../model/selectionModel'; +import { View } from '../view'; +import { JSONModel } from '../../model/jsonModel'; +import { applyEdits, Edit } from 'jsonc-parser'; +import { getJsonWorkerManager, JsonWorkerManager } from '../../worker/jsonWorkerManager'; +import { FoldingModel } from '../../model/foldingModel'; +import { Emitter, getEmitter } from '../../common/emitter'; +import { GlobalEvents, IModelContentChangeEvent } from '../../common/emitterEvents'; +import { Range } from '../../common/range'; +import { IndentAction, processJsonEnterAction } from './getEnterAction'; +import { firstNonWhitespaceIndex, getLeadingWhitespace } from '../../common/strings'; +/** + * EditWidget 类用于管理 JSON Viewer 中的编辑功能 + */ +export class EditWidget { + private _view: View; + private _selectionModel: SelectionModel; + private _jsonModel: JSONModel; + private _foldingModel: FoldingModel; + private _autoClosingPairs: Record = { + '{': '}', + '[': ']', + '(': ')', + '"': '"', + }; + private emitter: Emitter = getEmitter(); + constructor( + view: View, + jsonModel: JSONModel, + selectionModel: SelectionModel, + foldingModel: FoldingModel, + private _jsonWorkerManager: JsonWorkerManager = getJsonWorkerManager() + ) { + this._view = view; + this._jsonModel = jsonModel; + this._selectionModel = selectionModel; + this._foldingModel = foldingModel; + + this.attachEventListeners(); + } + + private attachEventListeners() { + this._jsonWorkerManager.validate().then(result => { + this.emitter.emit('problemsChanged', { + problems: result.problems, + root: result.root, + }); + }); + + this._view.contentDom.addEventListener('beforeinput', (e: InputEvent) => { + this._handleBeforeInput(e); + }); + + this._view.contentDom.addEventListener('keydown', (e: KeyboardEvent) => { + this._handleKeyDown(e); + }); + } + + private _handleBeforeInput(e: InputEvent) { + e.preventDefault(); + this._selectionModel.updateFromSelection(); + const startRow = this._selectionModel.startRow; + const startCol = this._selectionModel.startCol; + const endRow = this._selectionModel.endRow; + const endCol = this._selectionModel.endCol; + const startOffset = this._jsonModel.getOffsetAt(startRow, startCol); + const endOffset = this._jsonModel.getOffsetAt(endRow, endCol); + const op: IModelContentChangeEvent = { + type: 'insert', + range: { + startLineNumber: startRow, + startColumn: startCol, + endLineNumber: endRow, + endColumn: endCol, + }, + rangeOffset: startOffset, + rangeLength: endOffset - startOffset, + oldText: '', + newText: '', + }; + + switch (e.inputType) { + case 'insertText': + if (this._selectionModel.isCollapsed) { + op.type = 'insert'; + } else { + op.type = 'replace'; + } + op.newText = e.data || ''; + op.oldText = this._jsonModel.getValueInRange({ + startLineNumber: startRow, + startColumn: startCol, + endLineNumber: endRow, + endColumn: endCol, + } as Range); + if (this._autoClosingPairs[op.newText]) { + op.newText += this._autoClosingPairs[op.newText]; + op.keepPosition = { + lineNumber: startRow, + column: endCol + 1, + }; + } + break; + case 'insertParagraph': + op.newText = '\n'; + op.keepPosition = { + lineNumber: startRow + 1, + column: 1, + }; + const enterAction = processJsonEnterAction(this._jsonModel, { + startLineNumber: startRow, + startColumn: startCol, + endLineNumber: endRow, + endColumn: endCol, + } as Range); + if (enterAction) { + if (enterAction.indentAction === IndentAction.Indent) { + op.newText = '\n' + this.normalizeIndentation(enterAction.appendText + enterAction.indentation) || ''; + op.keepPosition = { + lineNumber: startRow + 1, + column: enterAction.appendText.length + enterAction.indentation.length + 1, + }; + } else { + const normalIndent = this.normalizeIndentation(enterAction.indentation); + const increasedIndent = this.normalizeIndentation(enterAction.indentation + enterAction.appendText); + op.newText = '\n' + increasedIndent + '\n' + normalIndent; + op.keepPosition = { + lineNumber: startRow + 1, + column: increasedIndent.length + 1, + }; + } + } else { + const lineText = this._jsonModel.getLineContent(startRow); + const indentation = getLeadingWhitespace(lineText).substring(0, startCol - 1); + op.newText = '\n' + this.normalizeIndentation(indentation) || ''; + op.keepPosition = { + lineNumber: startRow + 1, + column: indentation.length + 1, + }; + } + break; + case 'deleteContentBackward': + let oldText = ''; + if (this._selectionModel.isCollapsed) { + op.rangeOffset = startOffset - 1; + oldText = this._jsonModel.getValueInRange({ + startLineNumber: startRow, + startColumn: startCol - 1, + endLineNumber: endRow, + endColumn: endCol, + } as Range); + } else { + oldText = this._jsonModel.getValueInRange({ + startLineNumber: startRow, + startColumn: startCol, + endLineNumber: endRow, + endColumn: endCol, + } as Range); + } + op.oldText = oldText; + op.type = 'delete'; + op.rangeLength = oldText.length; + break; + case 'insertFromPaste': + const pasteData = e.dataTransfer?.getData('text/plain'); + op.type = 'replace'; + op.newText = pasteData || ''; + op.oldText = this._jsonModel.getValueInRange({ + startLineNumber: startRow, + startColumn: startCol, + endLineNumber: endRow, + endColumn: endCol, + } as Range); + break; + } + if (this._selectionModel.isSelectedAll) { + op.range = { + startLineNumber: 1, + startColumn: 1, + endLineNumber: this._jsonModel.getLineCount(), + endColumn: this._jsonModel.getLineLength(this._jsonModel.getLineCount()), + }; + op.rangeOffset = 0; + op.rangeLength = this._jsonModel.getValue().length; + op.oldText = this._jsonModel.getValue(); + } + this._selectionModel.isSelectedAll = false; + + this._jsonModel.applyOperation(op); + } + + public format() { + this._jsonWorkerManager + .formatJson( + this._view.options?.formatOptions || { + tabSize: 2, + insertSpaces: true, + } + ) + .then((edits: Edit[]) => { + const newValue = applyEdits(this._jsonModel.getValue(), edits); + const op: IModelContentChangeEvent = { + type: 'replace', + range: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: this._jsonModel.getLineCount(), + endColumn: this._jsonModel.getLineLength(this._jsonModel.getLineCount()) + 1, + }, + rangeOffset: 0, + rangeLength: this._jsonModel.getValue().length, + oldText: this._jsonModel.getValue(), + newText: newValue, + }; + this._jsonModel.applyOperation(op); + }); + } + + private normalizeIndentation(str: string) { + const indentSize = this._view.options?.formatOptions?.tabSize || 4; + const insertSpaces = !!this._view.options?.formatOptions?.insertSpaces; + let firstIndex = firstNonWhitespaceIndex(str); + if (firstIndex === -1) { + firstIndex = str.length; + } + return ( + this._normalizeIndentationFromWhitespace(str.substring(0, firstIndex), indentSize, insertSpaces) + + str.substring(firstIndex) + ); + } + + private _normalizeIndentationFromWhitespace(str: string, indentSize: number, insertSpaces: boolean) { + let spacesCnt = 0; + for (let i = 0; i < str.length; i++) { + if (str.charAt(i) === '\t') { + spacesCnt = this.nextIndentTabStop(spacesCnt, indentSize); + } else { + spacesCnt++; + } + } + + let result = ''; + if (!insertSpaces) { + const tabsCnt = Math.floor(spacesCnt / indentSize); + spacesCnt = spacesCnt % indentSize; + for (let i = 0; i < tabsCnt; i++) { + result += '\t'; + } + } + + for (let i = 0; i < spacesCnt; i++) { + result += ' '; + } + + return result; + } + + private nextIndentTabStop(spacesCnt: number, indentSize: number) { + return spacesCnt + indentSize - (spacesCnt % indentSize); + } + + private _handleKeyDown(e: KeyboardEvent) { + this._selectionModel.updateFromSelection(); + const startRow = this._selectionModel.startRow; + const startCol = this._selectionModel.startCol; + const endRow = this._selectionModel.endRow; + const endCol = this._selectionModel.endCol; + const startOffset = this._jsonModel.getOffsetAt(startRow, startCol); + const endOffset = this._jsonModel.getOffsetAt(endRow, endCol); + switch (e.key) { + case 'Tab': + if (this._view.completeWidget.isVisible) { + e.preventDefault(); + this._view.completeWidget._handleKeyDown(e); + return; + } + e.preventDefault(); + let insertText = ''; + + if (this._view.options?.formatOptions?.insertSpaces) { + const tabSize = this._view.options?.formatOptions?.tabSize || 4; + for (let i = 0; i < tabSize; i++) { + insertText += ' '; + } + } else { + insertText = '\t'; + } + const op: IModelContentChangeEvent = { + type: 'insert', + range: { + startLineNumber: startRow, + startColumn: startCol, + endLineNumber: endRow, + endColumn: endCol, + }, + rangeOffset: startOffset, + rangeLength: endOffset - startOffset, + oldText: '', + newText: insertText, + }; + this._jsonModel.applyOperation(op); + break; + case 'f': + if (e.shiftKey && e.metaKey) { + e.preventDefault(); + this.format(); + } + break; + case 'ArrowRight': + case 'ArrowLeft': + if (this._view.completeWidget.isVisible) { + this._view.completeWidget.hide(); + } + break; + case 'ArrowDown': + case 'ArrowUp': + if (this._view.completeWidget.isVisible) { + e.preventDefault(); + this._view.completeWidget._handleKeyDown(e); + } + break; + case 'Enter': + if (this._view.completeWidget.isVisible) { + e.preventDefault(); + this._view.completeWidget._handleKeyDown(e); + } + break; + case 'a': + if (e.metaKey) { + this._selectionModel.isSelectedAll = true; + } + break; + case 'x': + if (e.metaKey) { + e.preventDefault(); + this._cutHandler(); + } + break; + case 'z': + if (e.metaKey && !e.shiftKey) { + e.preventDefault(); + this._jsonModel.undo(); + } else { + e.preventDefault(); + this._jsonModel.redo(); + } + break; + } + } + + private _cutHandler() { + const startRow = this._selectionModel.startRow; + const startCol = this._selectionModel.startCol; + const endRow = this._selectionModel.endRow; + const endCol = this._selectionModel.endCol; + let startOffset; + let oldText = ''; + const op: IModelContentChangeEvent = { + type: 'replace', + range: { + startLineNumber: startRow, + startColumn: startCol, + endLineNumber: endRow, + endColumn: endCol, + }, + rangeOffset: 0, + rangeLength: 0, + oldText: '', + newText: '', + }; + if (!this._selectionModel.isCollapsed) { + oldText = this._jsonModel.getValueInRange({ + startLineNumber: startRow, + startColumn: startCol, + endLineNumber: endRow, + endColumn: endCol, + } as Range); + startOffset = this._jsonModel.getOffsetAt(startRow, startCol); + } else { + oldText = this._jsonModel.getValueInRange({ + startLineNumber: startRow, + startColumn: 1, + endLineNumber: endRow, + endColumn: this._jsonModel.getLineLength(endRow) + 1, + } as Range); + op.range = { + startLineNumber: startRow, + startColumn: 1, + endLineNumber: endRow, + endColumn: this._jsonModel.getLineLength(endRow) + 1, + }; + startOffset = this._jsonModel.getOffsetAt(startRow, 1); + } + + op.oldText = oldText; + op.rangeOffset = startOffset; + op.rangeLength = oldText.length; + + if (this._selectionModel.isSelectedAll) { + op.range = { + startLineNumber: 1, + startColumn: 1, + endLineNumber: this._jsonModel.getLineCount(), + endColumn: this._jsonModel.getLineLength(this._jsonModel.getLineCount()) + 1, + }; + op.rangeOffset = 0; + op.rangeLength = this._jsonModel.getValue().length; + op.oldText = this._jsonModel.getValue(); + } + navigator.clipboard.writeText(op.oldText); + this._jsonModel.applyOperation(op); + } +} diff --git a/packages/semi-json-viewer-core/src/view/edit/getEnterAction.ts b/packages/semi-json-viewer-core/src/view/edit/getEnterAction.ts new file mode 100644 index 0000000000..82c27311d4 --- /dev/null +++ b/packages/semi-json-viewer-core/src/view/edit/getEnterAction.ts @@ -0,0 +1,89 @@ +/** based on https://github.com/microsoft/vscode with modifications for custom requirements */ +import { JSONModel } from '../../model/jsonModel'; +import { Range } from '../../common/range'; +import * as strings from '../../common/strings'; +export enum IndentAction { + None = 0, + Indent = 1, + IndentOutdent = 2, + Outdent = 3, +} + +interface JsonEnterResult { + indentAction: IndentAction; + appendText?: string; + removeText?: number +} + +export function getIndentationAtPosition(model: JSONModel, lineNumber: number, column: number) { + const lineText = model.getLineContent(lineNumber); + let indentation = strings.getLeadingWhitespace(lineText); + if (indentation.length > column - 1) { + indentation = indentation.substring(0, column - 1); + } + return indentation; +} + +export function processJsonEnterAction(model: JSONModel, range: Range) { + // 获取上下文信息 + const currentLineText = model.getLineContent(range.startLineNumber); + const beforeText = currentLineText.substring(0, range.startColumn - 1); + const afterText = currentLineText.substring(range.startColumn - 1); + const previousLineText = range.startLineNumber > 1 ? model.getLineContent(range.startLineNumber - 1) : ''; + + // 获取回车处理结果 + const enterResult: JsonEnterResult | null = onEnter(beforeText, afterText, previousLineText); + if (!enterResult) { + return null; + } + const indentAction = enterResult.indentAction; + let appendText = enterResult.appendText; + const removeText = enterResult.removeText || 0; + if (!appendText) { + if (indentAction === IndentAction.Indent || indentAction === IndentAction.IndentOutdent) { + appendText = '\t'; + } else { + appendText = ''; + } + } else if (indentAction === IndentAction.Indent) { + appendText = '\t' + appendText; + } + + let indentation = getIndentationAtPosition(model, range.startLineNumber, range.startColumn); + if (removeText) { + indentation = indentation.substring(0, indentation.length - removeText); + } + + return { + indentAction: indentAction, + appendText: appendText, + removeText: removeText, + indentation: indentation, + }; +} + +function onEnter(beforeText: string, afterText: string, previousLineText: string) { + const brackets = [ + { open: '{', openRegExp: /\{\s*$/, close: '}', closeRegExp: /^\s*\}/ }, + { open: '[', openRegExp: /\[\s*$/, close: ']', closeRegExp: /^\s*\]/ }, + ]; + if (beforeText.length > 0 && afterText.length > 0) { + for (let i = 0, len = brackets.length; i < len; i++) { + const bracket = brackets[i]; + if (bracket.openRegExp.test(beforeText) && bracket.closeRegExp.test(afterText)) { + return { indentAction: IndentAction.IndentOutdent }; + } + } + } + + if (beforeText.length > 0) { + for (let i = 0, len = brackets.length; i < len; i++) { + const bracket = brackets[i]; + if (bracket.openRegExp.test(beforeText)) { + return { indentAction: IndentAction.Indent }; + } + } + } + + return null; +} diff --git a/packages/semi-json-viewer-core/src/view/fold/foldWidget.ts b/packages/semi-json-viewer-core/src/view/fold/foldWidget.ts new file mode 100644 index 0000000000..faee0caca4 --- /dev/null +++ b/packages/semi-json-viewer-core/src/view/fold/foldWidget.ts @@ -0,0 +1,99 @@ +import { elt, setStyles } from '../../common/dom'; +import { View } from '../view'; +import { FoldingModel } from '../../model/foldingModel'; + +/** + * FoldWidget 类用于管理 JSON Viewer 中的折叠功能 + */ +export class FoldWidget { + private _view: View; + private _foldingModel: FoldingModel; + + constructor(view: View, foldingModel: FoldingModel) { + this._view = view; + this._foldingModel = foldingModel; + this._attachEventListeners(); + } + + private _attachEventListeners() { + this._view.lineScrollDom.addEventListener('mouseover', e => { + this._handleLineNumberHover(e); + }); + this._view.lineScrollDom.addEventListener('mouseleave', () => { + this._handleLineNumberContainerLeave(); + }); + } + + private _handleLineNumberHover(e: MouseEvent) { + this._showFoldingIcon(); + } + + private _handleLineNumberContainerLeave() { + this.removeAllFoldingIcons(); + } + + private _showFoldingIcon() { + const lineNumberElement = this._view.lineScrollDom.children; + for (let i = 0; i < lineNumberElement.length; i++) { + const element: HTMLElement = lineNumberElement[i] as HTMLElement; + if (this._foldingModel.isFoldable(Number(element.dataset.lineNumber))) { + element.appendChild(this._createFoldingIcon(Number(element.dataset.lineNumber))); + } + } + } + + private _createFoldSvg(isCollapsed: boolean): SVGElement { + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('viewBox', '0 0 24 24'); + svg.setAttribute('fill', 'none'); + svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); + svg.setAttribute('width', '1em'); + svg.setAttribute('height', '1em'); + if (isCollapsed) { + svg.setAttribute('transform', 'rotate(270)'); + } + const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + const d = 'M21.8329 6.59139L12.8063 18.9004C12.4068 19.4452 11.5931 19.4452 11.1935 18.9004L2.16693 6.59139C1.68255 5.93086 2.15424 5.00003 2.97334 5.00003L21.0265 5.00003C21.8456 5.00003 22.3173 5.93087 21.8329 6.59139Z'; + path.setAttribute('d', d); + path.setAttribute('fill', 'var(--semi-color-tertiary)'); + svg.appendChild(path); + return svg; + } + + private _createFoldingIcon(lineNumber: number): HTMLElement { + const foldingIconClass = 'semi-json-viewer-folding-icon'; + + const foldingIcon = elt('span', foldingIconClass); + const isCollapsed = this._foldingModel.isCollapsed(lineNumber); + foldingIcon.appendChild(this._createFoldSvg(isCollapsed)); + setStyles(foldingIcon, { + position: 'absolute', + right: '0', + top: '0', + width: '40%', + height: '100%', + cursor: 'pointer', + zIndex: '1', + userSelect: 'none', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }); + + foldingIcon.addEventListener('mousedown', e => { + e.preventDefault(); // 防止文本选择 + e.stopPropagation(); + this._foldingModel.toggleFoldingRange(lineNumber); + this._view.scalingCellSizeAndPositionManager.resetCell(0); + this._view.layout(); + }); + + return foldingIcon; + } + + removeAllFoldingIcons() { + const foldingIconClass = 'semi-json-viewer-folding-icon'; + const foldingIcons = this._view.lineScrollDom.querySelectorAll(`.${foldingIconClass}`); + foldingIcons.forEach(icon => icon.remove()); + } +} diff --git a/packages/semi-json-viewer-core/src/view/hover/hoverWidget.ts b/packages/semi-json-viewer-core/src/view/hover/hoverWidget.ts new file mode 100644 index 0000000000..e6dbf2fbff --- /dev/null +++ b/packages/semi-json-viewer-core/src/view/hover/hoverWidget.ts @@ -0,0 +1,121 @@ +import { View } from '../view'; +import { Emitter, getEmitter } from '../../common/emitter'; +import { elt, setStyles } from '../../common/dom'; +import { GlobalEvents } from '../../common/emitterEvents'; +/** + * HoverWidget 类用于管理 JSON Viewer 中的悬浮提示功能 + * 当鼠标悬停在字符串值上时,显示一个自定义的提示框 + */ +export class HoverWidget { + private _view: View; + private _hoverDom: HTMLElement | null = null; + private _tooltipDom: HTMLElement; + private _hoverTimer: number | null = null; + private emitter: Emitter = getEmitter(); + + constructor(view: View) { + this._view = view; + + this._tooltipDom = this._createTooltipDom(); + this._view.jsonViewerDom.appendChild(this._tooltipDom); + this._attachEventListeners(); + } + + private _attachEventListeners() { + this._view.contentDom.addEventListener('mousemove', e => { + if (e.target instanceof HTMLSpanElement && e.target.classList.contains('semi-json-viewer-string-value')) { + if (this._hoverDom === e.target) { + return; + } + this._clearHoverTimer(); + this._hideTooltip(); + + this._hoverDom = e.target; + this._hoverTimer = window.setTimeout(() => { + if (this._hoverDom) { + this.emitter.emit('hoverNode', { + value: this._hoverDom.textContent ?? '', + target: this._hoverDom, + }); + } + }, 700); + } + }); + + this._view.contentDom.addEventListener('mouseout', e => { + const relatedTarget = e.relatedTarget as Node; + if (!this._tooltipDom.contains(relatedTarget)) { + this._clearHoverTimer(); + this._hideTooltip(); + } + }); + + this._tooltipDom.addEventListener('mouseleave', e => { + const relatedTarget = e.relatedTarget as Node; + if (!this._hoverDom?.contains(relatedTarget)) { + this._hideTooltip(); + } + }); + + this.emitter.on('renderHoverNode', e => { + this.render(e.el); + }); + } + + private _clearHoverTimer() { + if (this._hoverTimer) { + window.clearTimeout(this._hoverTimer); + this._hoverTimer = null; + } + } + + private _hideTooltip() { + setStyles(this._tooltipDom, { + visibility: 'hidden', + }); + this._tooltipDom.innerHTML = ''; + this._hoverDom = null; + } + + private _createTooltipDom() { + const div = elt('div', 'hover-container'); + setStyles(div, { + visibility: 'hidden', + position: 'absolute', + zIndex: '1000', + }); + return div; + } + + render(el: HTMLElement) { + if (!this._hoverDom) return; + this._tooltipDom.innerHTML = ''; + this._tooltipDom.appendChild(el); + + // 获取必要的位置信息 + const hoverRect = this._hoverDom.getBoundingClientRect(); + const editorRect = this._view.contentDom.getBoundingClientRect(); + const tooltipRect = this._tooltipDom.getBoundingClientRect(); + + // 计算水平居中位置 + let left = hoverRect.left - editorRect.left + (hoverRect.width + tooltipRect.width) / 2; + // 确保不会超出左边界 + left = Math.max(5, left); + // 确保不会超出右边界 + left = Math.min(left, editorRect.width - tooltipRect.width - 5); + + // 默认显示在上方,距离元素5px + let top = hoverRect.top - editorRect.top - tooltipRect.height; + + // 如果超出顶部,则显示在下方 + if (hoverRect.top - tooltipRect.height - 5 < editorRect.top) { + top = hoverRect.top - editorRect.top + hoverRect.height; + } + + setStyles(this._tooltipDom, { + visibility: 'visible', + top: `${top}px`, + left: `${left}px`, + }); + } +} diff --git a/packages/semi-json-viewer-core/src/view/search/searchWidget.ts b/packages/semi-json-viewer-core/src/view/search/searchWidget.ts new file mode 100644 index 0000000000..b740617bca --- /dev/null +++ b/packages/semi-json-viewer-core/src/view/search/searchWidget.ts @@ -0,0 +1,153 @@ +import { FindMatch } from '../../common/model'; +import { View } from '../view'; +import { JSONModel } from '../../model/jsonModel'; +import { IModelContentChangeEvent } from '../../common/emitterEvents'; + +/** + * SearchWidget 类用于管理 JSON Viewer 中的查找和替换功能 + */ +export class SearchWidget { + private _view: View; + private _searchInput: HTMLInputElement; + private _replaceInput: HTMLInputElement; + private _container: HTMLElement; + private _jsonModel: JSONModel; + //TODO: 修改searchResults存储数据结构 + public searchResults: FindMatch[] | null = null; + public _currentResultIndex: number = -1; + public matchCase: boolean = false; + public wordSeparators: string | null = null; + public isRegex: boolean = false; + private _searchDiv: HTMLElement; + private _replaceDiv: HTMLElement; + + constructor(view: View, jsonModel: JSONModel) { + this._view = view; + this._jsonModel = jsonModel; + } + + public search(searchText: string, caseSensitive: boolean, wholeWord: boolean, regex: boolean): void { + this._currentResultIndex = -1; + const isRegex = regex; + const matchCase = caseSensitive; + const wordSeparators = !wholeWord ? this.wordSeparators : '`~!@#$%^&*()-=+[{]}\\|;:\'",.<>/?'; + const searchScope = null; // 搜索整个文档 + const captureMatches = false; + const limitResultCount = Infinity; + this.searchResults = this._jsonModel.findMatches( + searchText, + searchScope, + isRegex, + matchCase, + wordSeparators, + captureMatches, + limitResultCount + ); + + this._view.layout(); + } + + public replace(replaceText: string): void { + if (!replaceText || !this.searchResults) return; + if (this._currentResultIndex < 0) { + this._currentResultIndex = 0; + } + const currentMatch = this.searchResults[this._currentResultIndex]; + const startOffset = this._jsonModel.getOffsetAt( + currentMatch.range.startLineNumber, + currentMatch.range.startColumn + ); + const endOffset = this._jsonModel.getOffsetAt( + currentMatch.range.endLineNumber, + currentMatch.range.endColumn + ); + const op: IModelContentChangeEvent = { + range: currentMatch.range, + newText: replaceText, + oldText: this._jsonModel.getValueInRange(currentMatch.range), + type: 'replace', + rangeOffset: startOffset, + rangeLength: endOffset - startOffset, + }; + this.searchResults.splice(this._currentResultIndex, 1); + + this._jsonModel.applyOperation(op); + } + + public replaceAll(replaceText: string): void { + if (!replaceText || !this.searchResults) return; + const op: IModelContentChangeEvent[] = []; + for (let i = this.searchResults.length - 1; i >= 0; i--) { + const match = this.searchResults[i]; + const startOffset = this._jsonModel.getOffsetAt(match.range.startLineNumber, match.range.startColumn); + const endOffset = this._jsonModel.getOffsetAt(match.range.endLineNumber, match.range.endColumn); + op.push({ + range: match.range, + newText: replaceText, + oldText: this._jsonModel.getValueInRange(match.range), + type: 'replace', + rangeOffset: startOffset, + rangeLength: endOffset - startOffset, + }); + } + this.searchResults = null; + this._jsonModel.applyOperation(op); + } + + public navigateResults(direction: number): void { + if (!this.searchResults || this.searchResults.length === 0) return; + + this._currentResultIndex += direction; + if (this._currentResultIndex < 0) { + this._currentResultIndex = this.searchResults.length - 1; + } else if (this._currentResultIndex >= this.searchResults.length) { + this._currentResultIndex = 0; + } + const currentMatch = this.searchResults[this._currentResultIndex]; + if (!currentMatch) return; + if ( + currentMatch.range.startLineNumber > this._view.startLineNumber + this._view.visibleLineCount || + currentMatch.range.startLineNumber < this._view.startLineNumber + ) { + this._view.scrollToLine(currentMatch.range.startLineNumber); + } else { + this._view.layout(); + } + } + + public binarySearchByLine(targetLine: number): FindMatch[] | null { + const matches: FindMatch[] = []; + if (!this.searchResults) return null; + // 二分查找第一个匹配的位置 + let left = 0; + let right = this.searchResults.length - 1; + + while (left <= right) { + const mid = Math.floor((left + right) / 2); + const currentLine = this.searchResults[mid].range.startLineNumber; + + if (currentLine === targetLine) { + // 找到匹配项,收集所有相同行号的结果 + let i = mid; + while (i >= 0 && this.searchResults[i].range.startLineNumber === targetLine) { + matches.unshift(this.searchResults[i]); + i--; + } + i = mid + 1; + while (i < this.searchResults.length && this.searchResults[i].range.startLineNumber === targetLine) { + matches.push(this.searchResults[i]); + i++; + } + return matches; + } + + if (currentLine < targetLine) { + left = mid + 1; + } else { + right = mid - 1; + } + } + + return matches; + } +} diff --git a/packages/semi-json-viewer-core/src/view/view.ts b/packages/semi-json-viewer-core/src/view/view.ts new file mode 100644 index 0000000000..420bcd7def --- /dev/null +++ b/packages/semi-json-viewer-core/src/view/view.ts @@ -0,0 +1,493 @@ +import { JSONModel } from '../model/jsonModel'; +import { elt, setStyles } from '../common/dom'; +import { Token } from '../tokens/tokenize'; +import { Emitter, getEmitter } from '../common/emitter'; +import { SelectionModel } from '../model/selectionModel'; +import { JsonViewerOptions } from '../json-viewer/jsonViewer'; +import { getJsonWorkerManager, JsonWorkerManager } from '../worker/jsonWorkerManager'; +import { FoldingModel } from '../model/foldingModel'; +import { SearchWidget } from './search/searchWidget'; +import { EditWidget } from './edit/editWidget'; +import { FindMatch } from '../common/model'; +import { FoldWidget } from './fold/foldWidget'; +import { TokenizationJsonModelPart } from '../tokens/tokenizationJsonModelPart'; +import { ScalingCellSizeAndPositionManager } from './virtualized/ScalingCellSizeAndPositionManager'; +import { CompleteWidget } from './complete/completeWidget'; +import { HoverWidget } from './hover/hoverWidget'; +import { GlobalEvents } from '../common/emitterEvents'; +//TODO 实现ViewModel抽离代码 + +/** + * View 类用于管理 JSON Viewer 的视图 + */ +export class View { + private _jsonModel: JSONModel; + private _selectionModel: SelectionModel; + private _foldingModel: FoldingModel; + + private _options: JsonViewerOptions | undefined; + public _lineHeight: number; + + private _container: HTMLElement; + private _jsonViewerDom: HTMLElement; + private _lineNumberDom: HTMLElement; + private _lineScrollDom: HTMLElement; + private _contentDom: HTMLElement; + private _scrollDom: HTMLElement; + + public startLineNumber: number = 1; + public visibleLineCount: number = 0; + + private _verticalOffsetAdjustment: number = 0; + + private _searchWidget: SearchWidget; + private _editWidget: EditWidget; + private _foldWidget: FoldWidget; + private _completeWidget: CompleteWidget; + private _hoverWidget: HoverWidget; + private _jsonWorkerManager: JsonWorkerManager = getJsonWorkerManager(); + private _tokenizationJsonModelPart: TokenizationJsonModelPart; + private _scalingCellSizeAndPositionManager: ScalingCellSizeAndPositionManager; + + private _measuredHeights: { [index: number]: number } = {}; + + private emitter: Emitter = getEmitter(); + + constructor(container: HTMLElement, model: JSONModel, options?: JsonViewerOptions) { + this._container = container; + this._jsonModel = model; + this._selectionModel = new SelectionModel(1, 0, this, model); + this._foldingModel = new FoldingModel(model); + + this._lineHeight = options?.lineHeight || 20; + this._options = options; + + this._jsonViewerDom = this.createRenderContainer(); + this._lineNumberDom = this.createLineNumberContainer(); + this._contentDom = this.createContentContainer(); + this._scrollDom = this.createScrollElement(); + this._lineScrollDom = this.createLineScrollContainerElement(); + + this._contentDom.appendChild(this._scrollDom); + this._lineNumberDom.appendChild(this._lineScrollDom); + this._jsonViewerDom.appendChild(this._lineNumberDom); + this._jsonViewerDom.appendChild(this._contentDom); + this._container.appendChild(this._jsonViewerDom); + + this._searchWidget = new SearchWidget(this, this._jsonModel); + this._foldWidget = new FoldWidget(this, this._foldingModel); + this._editWidget = new EditWidget(this, this._jsonModel, this._selectionModel, this._foldingModel); + this._completeWidget = new CompleteWidget(this, this._jsonModel, this._selectionModel); + this._hoverWidget = new HoverWidget(this); + + this._tokenizationJsonModelPart = new TokenizationJsonModelPart(this._jsonModel); + + this._scalingCellSizeAndPositionManager = new ScalingCellSizeAndPositionManager({ + cellCount: this._jsonModel.getLineCount(), + cellSizeGetter: ({ index }) => this.getCellSize(index), + estimatedCellSize: this._lineHeight, + }); + + this._attachEventListeners(); + } + + get tokenizationJsonModelPart() { + return this._tokenizationJsonModelPart; + } + + get contentDom() { + return this._contentDom; + } + + get jsonViewerDom() { + return this._jsonViewerDom; + } + + get scrollDom() { + return this._scrollDom; + } + + get lineScrollDom() { + return this._lineScrollDom; + } + + get options() { + return this._options; + } + + get completeWidget() { + return this._completeWidget; + } + + get editWidget() { + return this._editWidget; + } + + get scalingCellSizeAndPositionManager() { + return this._scalingCellSizeAndPositionManager; + } + + get searchWidget() { + return this._searchWidget; + } + + public dispose() { + this._container.removeChild(this._jsonViewerDom); + } + + private _attachEventListeners() { + this._jsonViewerDom.addEventListener('scroll', e => { + this.onScroll(this._jsonViewerDom.scrollTop); + }); + + this._contentDom.addEventListener('click', e => { + e.preventDefault(); + this._selectionModel.isSelectedAll = false; + this._selectionModel.updateFromSelection(); + }); + + this.emitter.on('contentChanged', () => { + this.resetScalingManagerConfigAndCell(0); + this.layout(); + }); + } + + public getLineElement(lineNumber: number): HTMLElement | null { + const visibleLineNumber = this._foldingModel.getVisibleLineNumber(lineNumber); + if ( + visibleLineNumber > this.visibleLineCount + this.startLineNumber || + visibleLineNumber < this.startLineNumber + ) + return null; + return this._scrollDom.children[ + visibleLineNumber - this._foldingModel.getVisibleLineNumber(this.startLineNumber) + ] as HTMLElement; + } + + public updateVisibleRange(start: number, end: number) { + this.startLineNumber = start; + this.visibleLineCount = end - start + 1; + } + + public onScroll(scrollTop: number) { + this._jsonViewerDom.scrollTop = scrollTop; + this.layout(); + } + + public scrollToLine(lineNumber: number): void { + const visibleLineNumber = this._foldingModel.getVisibleLineNumber(lineNumber); + const scrollTop = (visibleLineNumber - 1) * this._lineHeight; + this._contentDom.scrollTop = scrollTop; + this.onScroll(scrollTop); + } + + private createRenderContainer(): HTMLElement { + const renderContainer = elt('div', 'json-viewer-container'); + setStyles(renderContainer, { + position: 'relative', + height: '100%', + width: '100%', + overflow: 'auto', + }); + return renderContainer; + } + + private createLineNumberContainer(): HTMLElement { + const lineNumberClass = 'semi-json-viewer-line-number-container'; + const lineNumberContainer = elt('div', lineNumberClass); + setStyles(lineNumberContainer, { + position: 'absolute', + left: '0', + top: '0', + width: '50px', + }); + return lineNumberContainer; + } + + private createLineScrollContainerElement(): HTMLElement { + const lineScrollContainer = elt('div', 'line-scroll-container'); + setStyles(lineScrollContainer, { + position: 'absolute', + top: '0', + left: '0', + height: `${this._lineHeight * this._jsonModel.getLineCount()}px`, + width: '100%', + overflow: 'hidden', + }); + return lineScrollContainer; + } + + private createContentContainer(): HTMLElement { + const contentClass = 'semi-json-viewer-content-container'; + const contentContainer = elt('div', contentClass); + setStyles(contentContainer, { + position: 'absolute', + left: '50px', + top: '0', + right: '0', + overflowX: 'auto', + overflowY: 'scroll', + outline: 'none', + }); + contentContainer.contentEditable = 'true'; + contentContainer.style.caretColor = 'black'; + contentContainer.spellcheck = false; + return contentContainer; + } + + private createLineNumberElement(actualLineNumber: number, visibleLineNumber: number): HTMLElement { + const lineNumberClass = 'semi-json-viewer-line-number'; + const lineNumberElement = elt('div', lineNumberClass); + const rowDatum = this._scalingCellSizeAndPositionManager.getSizeAndPositionOfCell(visibleLineNumber); + setStyles(lineNumberElement, { + position: 'absolute', + width: '50px', + height: `${this._lineHeight}px`, + lineHeight: `${this._lineHeight}px`, + top: `${rowDatum.offset + this._verticalOffsetAdjustment}px`, + }); + const lineNumber = elt('span', 'line-number-text', { + position: 'absolute', + left: '0', + top: '0', + textAlign: 'right', + width: '60%', + height: '100%', + }); + lineNumber.innerHTML = actualLineNumber.toString(); + lineNumberElement.appendChild(lineNumber); + lineNumberElement.dataset.lineNumber = actualLineNumber.toString(); + return lineNumberElement; + } + + private createScrollElement(): HTMLElement { + const scrollEl = elt('div', 'lines-content'); + + setStyles(scrollEl, { + position: 'relative', + overflow: 'scroll', + top: '0', + left: '0', + tabSize: (this._options?.formatOptions?.tabSize || 4).toString(), + height: `${this._lineHeight * this._jsonModel.getLineCount()}px`, + }); + if (this._options?.autoWrap) { + scrollEl.style.width = '100%'; + } + return scrollEl; + } + + private createLineContentElement( + lineContent: string, + actualLineNumber: number, + visibleLineNumber: number + ): HTMLElement { + const rowDatum = this._scalingCellSizeAndPositionManager.getSizeAndPositionOfCell(visibleLineNumber); + const lineElementClass = 'semi-json-viewer-view-line'; + const lineElement = elt('div', lineElementClass); + lineElement.setAttribute('data-line-element', 'true'); + setStyles(lineElement, { + lineHeight: `${this._lineHeight}px`, + width: '100%', + position: 'absolute', + top: `${rowDatum.offset + this._verticalOffsetAdjustment}px`, + }); + if (!this._options?.autoWrap) { + lineElement.style.height = `${this._lineHeight}px`; + } + lineElement.innerHTML = lineContent; + lineElement.dataset.lineNumber = actualLineNumber.toString(); + // @ts-ignore + lineElement.lineNumber = actualLineNumber; + return lineElement; + } + + private getCellSize(index: number): number { + if (this._options?.autoWrap) { + return this._measuredHeights[index] || this._lineHeight; + } + return this._lineHeight; + } + + private _measureAndUpdateItemHeight(item: HTMLElement, index: number) { + const height = item.offsetHeight; + const width = item.textContent?.length * 10; + if (!this._options?.autoWrap && width > this._scrollDom.offsetWidth) { + this._scrollDom.style.width = `${width}px`; + } + if (height === 0) { + item.style.height = `${this._lineHeight}px`; + return; + } + if (height !== this._measuredHeights[index]) { + this._measuredHeights[index] = height; + this._scalingCellSizeAndPositionManager.resetCell(index); + this._scrollDom.style.height = `${this._scalingCellSizeAndPositionManager.getTotalSize()}px`; + } + } + + private clearContainers() { + this._lineScrollDom.innerHTML = ''; + this._scrollDom.innerHTML = ''; + } + + public resetScalingManagerConfigAndCell(index: number) { + this._scalingCellSizeAndPositionManager.configure({ + cellCount: this._jsonModel.getLineCount(), + cellSizeGetter: ({ index }) => this.getCellSize(index), + estimatedCellSize: this._lineHeight, + }); + this._scalingCellSizeAndPositionManager.resetCell(index); + } + + public layout() { + this.clearContainers(); + + const visibleLineCount = this._foldingModel.getVisibleLineCount(); + this._scalingCellSizeAndPositionManager.configure({ + cellCount: visibleLineCount, + cellSizeGetter: ({ index }) => this.getCellSize(index), + estimatedCellSize: this._lineHeight, + }); + + const visibleRange = this._scalingCellSizeAndPositionManager.getVisibleCellRange({ + containerSize: this._container.clientHeight, + offset: this._jsonViewerDom.scrollTop, + }); + + const verticalOffsetAdjustment = this._scalingCellSizeAndPositionManager.getOffsetAdjustment({ + containerSize: this._container.clientHeight, + offset: this._jsonViewerDom.scrollTop, + }); + this._verticalOffsetAdjustment = verticalOffsetAdjustment; + this.renderVisibleLines(visibleRange.start!, visibleRange.stop!); + this.updateVisibleRange(visibleRange.start! + 1, visibleRange.stop! + 1); + + this._selectionModel.toViewPosition(); + this._completeWidget.show(); + const totalSize = this._scalingCellSizeAndPositionManager.getTotalSize(); + this._scrollDom.style.height = `${totalSize}px`; + this._lineScrollDom.style.height = `${totalSize}px`; + } + + private renderVisibleLines(startVisibleLine: number, endVisibleLine: number) { + this._tokenizationJsonModelPart.forceTokenize(endVisibleLine + 1); + let actualLineNumber = this._foldingModel.getActualLineNumber(startVisibleLine + 1); + let visibleLineNumber = startVisibleLine; + while (visibleLineNumber <= endVisibleLine && actualLineNumber <= this._jsonModel.getLineCount()) { + if (!this._foldingModel.isLineCollapsed(actualLineNumber)) { + this.renderLine(actualLineNumber, visibleLineNumber); + visibleLineNumber++; + } + actualLineNumber = this._foldingModel.getNextVisibleLine(actualLineNumber); + } + } + + private renderLine(actualLineNumber: number, visibleLineNumber: number) { + // const cache = this._domCache.get(actualLineNumber); + // if (cache) { + // return; + // } + const line = this._jsonModel.getLineContent(actualLineNumber); + + const tokens = this._tokenizationJsonModelPart.getLineTokens(actualLineNumber); + + const lineNumberElement = this.renderLineNumber(actualLineNumber, visibleLineNumber); + const lineElement = this.renderLineContent(actualLineNumber, visibleLineNumber, tokens, line); + // this._domCache.set(actualLineNumber, { + // lineElement, + // lineNumberElement + // }); + } + + + private renderLineNumber(actualLineNumber: number, visibleLineNumber: number) { + const lineNumberElement = this.createLineNumberElement(actualLineNumber, visibleLineNumber); + this._lineScrollDom.appendChild(lineNumberElement); + return lineNumberElement; + } + + private renderLineContent(actualLineNumber: number, visibleLineNumber: number, tokens: Token[], line: string) { + const lineContent = this.renderTokensWithHighlight(tokens, line, actualLineNumber); + const lineElement = this.createLineContentElement(lineContent, actualLineNumber, visibleLineNumber); + this._scrollDom.appendChild(lineElement); + + // this._options?.autoWrap && + this._measureAndUpdateItemHeight(lineElement, visibleLineNumber); + return lineElement; + } + + private renderTokensWithHighlight(tokens: Token[], text: string, lineNumber: number): string { + let html = ''; + let currentOffset = 0; + + const searchResults = this._searchWidget.binarySearchByLine(lineNumber); + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i]; + const start = token.startIndex; + const end = i + 1 < tokens.length ? tokens[i + 1].startIndex : text.length; + let content = text.substring(start, end); + + if (searchResults && searchResults.length > 0) { + html += this.highlightContent(content, currentOffset, searchResults, token.scopes); + } else { + content = this.escapeHtml(content); + html += `${content}`; + } + + currentOffset += content.length; + } + + return html; + } + + private highlightContent(content: string, offset: number, searchResults: FindMatch[], tokenClass: string): string { + let result = ''; + let lastIndex = 0; + + for (const match of searchResults) { + const startIndex = Math.max(0, match.range.startColumn - 1 - offset); + const endIndex = Math.min(content.length, match.range.endColumn - 1 - offset); + + if (startIndex >= content.length || endIndex <= 0) continue; + + if (startIndex > lastIndex) { + result += `${this.escapeHtml( + content.substring(lastIndex, startIndex) + )}`; + } + + const highlightedText = this.escapeHtml(content.substring(startIndex, endIndex)); + const currentMatch = this._searchWidget.searchResults?.[this._searchWidget._currentResultIndex]; + const searchResultClass = 'semi-json-viewer-search-result'; + const currentSearchResultClass = 'semi-json-viewer-current-search-result'; + if ( + match.range.startLineNumber === currentMatch?.range.startLineNumber && + match.range.endLineNumber === currentMatch?.range.endLineNumber && + match.range.startColumn === currentMatch?.range.startColumn && + match.range.endColumn === currentMatch?.range.endColumn + ) { + result += `${highlightedText}`; + } else { + result += `${highlightedText}`; + } + + lastIndex = endIndex; + } + + if (lastIndex < content.length) { + result += `${this.escapeHtml(content.substring(lastIndex))}`; + } + + return result; + } + + private escapeHtml(text: string): string { + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/ /g, ' ') + .replace(/\t/g, ' '); + } +} diff --git a/packages/semi-json-viewer-core/src/view/virtualized/CellSizeAndPositionManager.ts b/packages/semi-json-viewer-core/src/view/virtualized/CellSizeAndPositionManager.ts new file mode 100644 index 0000000000..3504130827 --- /dev/null +++ b/packages/semi-json-viewer-core/src/view/virtualized/CellSizeAndPositionManager.ts @@ -0,0 +1,213 @@ +//reference from https://github.com/bvaughn/react-virtualized +import { Alignment, CellSizeGetter, VisibleCellRange } from './types'; + +type CellSizeAndPositionManagerParams = { + cellCount: number; + cellSizeGetter: CellSizeGetter; + estimatedCellSize: number +}; + +type GetUpdatedOffsetForIndex = { + align: Alignment; + containerSize: number; + currentOffset: number; + targetIndex: number +}; + +type SizeAndPositionData = { + offset: number; + size: number +}; + +type GetVisibleCellRangeParams = { + containerSize: number; + offset: number +}; + +export class CellSizeAndPositionManager { + private _cellCount: number; + private _cellSizeGetter: CellSizeGetter; + private _estimatedCellSize: number; + + private _lastMeasuredIndex = -1; + private _cellSizeAndPositionData: Record = {}; + private _lastBatchedIndex = -1; + + constructor(params: CellSizeAndPositionManagerParams) { + this._cellCount = params.cellCount; + this._cellSizeGetter = params.cellSizeGetter; + this._estimatedCellSize = params.estimatedCellSize; + } + + areOffsetsAdjusted() { + return false; + } + + configure(params: CellSizeAndPositionManagerParams) { + this._cellCount = params.cellCount; + this._cellSizeGetter = params.cellSizeGetter; + this._estimatedCellSize = params.estimatedCellSize; + } + + getCellCount() { + return this._cellCount; + } + + getEstimatedCellSize() { + return this._estimatedCellSize; + } + + getLastMeasuredIndex() { + return this._lastMeasuredIndex; + } + + getOffsetAdjustment() { + return 0; + } + + getSizeAndPositionOfCell(index: number): SizeAndPositionData { + if (index < 0 || index >= this._cellCount) { + throw new Error('index out of bounds'); + } + + if (index > this._lastMeasuredIndex) { + const lastMeasuredCellSizeAndPosition = this.getSizeAndPositionOfLastMeasuredCell(); + let offset = lastMeasuredCellSizeAndPosition.offset + lastMeasuredCellSizeAndPosition.size; + for (let i = this._lastMeasuredIndex + 1; i <= index; i++) { + const size = this._cellSizeGetter({ index: i }); + if (size === undefined || isNaN(size)) { + throw new Error('invalid size'); + } else if (size === null) { + this._cellSizeAndPositionData[i] = { + offset, + size: 0, + }; + this._lastBatchedIndex = index; + } else { + this._cellSizeAndPositionData[i] = { + offset, + size, + }; + offset += size; + this._lastMeasuredIndex = index; + } + } + } + return this._cellSizeAndPositionData[index]; + } + + getSizeAndPositionOfLastMeasuredCell(): SizeAndPositionData { + return this._lastMeasuredIndex >= 0 + ? this._cellSizeAndPositionData[this._lastMeasuredIndex] + : { offset: 0, size: 0 }; + } + + getTotalSize(): number { + const lastMeasuredCellSizeAndPosition = this.getSizeAndPositionOfLastMeasuredCell(); + const totalSizeOfMeasuredCells = lastMeasuredCellSizeAndPosition.offset + lastMeasuredCellSizeAndPosition.size; + const numUnmeasuredCells = this._cellCount - this._lastMeasuredIndex - 1; + const totalSizeOfUnmeasuredCells = numUnmeasuredCells * this._estimatedCellSize; + return totalSizeOfMeasuredCells + totalSizeOfUnmeasuredCells; + } + + getUpdatedOffsetForIndex({ align, containerSize, currentOffset, targetIndex }: GetUpdatedOffsetForIndex) { + if (currentOffset < 0) { + return 0; + } + const datum = this.getSizeAndPositionOfCell(targetIndex); + const maxOffset = datum.offset; + const minOffset = maxOffset - containerSize + datum.size; + let idealOffset = currentOffset; + switch (align) { + case 'start': + idealOffset = maxOffset; + break; + case 'end': + idealOffset = minOffset; + break; + case 'center': + idealOffset = maxOffset - (containerSize - datum.size) / 2; + break; + default: + idealOffset = Math.max(minOffset, Math.min(maxOffset, currentOffset)); + break; + } + const totalSize = this.getTotalSize(); + return Math.max(0, Math.min(idealOffset, totalSize - containerSize)); + } + + getVisibleCellRange(params: GetVisibleCellRangeParams): VisibleCellRange { + const containerSize = params.containerSize; + let offset = params.offset; + const totalSize = this.getTotalSize(); + if (totalSize === 0) { + return {}; + } + const maxOffset = offset + containerSize; + const start = this._findNearestCell(offset); + const datum = this.getSizeAndPositionOfCell(start); + + offset = datum.offset + datum.size; + + let stop = start; + while (offset < maxOffset && stop < this._cellCount - 1) { + stop++; + offset += this.getSizeAndPositionOfCell(stop).size; + } + return { + start, + stop, + }; + } + + private _binarySearch(high: number, low: number, offset: number): number { + while (low <= high) { + const middle = low + Math.floor((high - low) / 2); + const currentOffset = this.getSizeAndPositionOfCell(middle).offset; + + if (currentOffset === offset) { + return middle; + } else if (currentOffset < offset) { + low = middle + 1; + } else if (currentOffset > offset) { + high = middle - 1; + } + } + + if (low > 0) { + return low - 1; + } else { + return 0; + } + } + + resetCell(index: number): void { + this._lastMeasuredIndex = Math.min(this._lastMeasuredIndex, index - 1); + } + + private _exponentialSearch(index: number, offset: number): number { + let interval = 1; + + while (index < this._cellCount && this.getSizeAndPositionOfCell(index).offset < offset) { + index += interval; + interval *= 2; + } + + return this._binarySearch(Math.min(index, this._cellCount - 1), Math.floor(index / 2), offset); + } + + private _findNearestCell(offset: number) { + if (isNaN(offset)) { + throw new Error('offset is NaN'); + } + offset = Math.max(0, offset); + const lastMeasuredCellSizeAndPosition = this.getSizeAndPositionOfLastMeasuredCell(); + const lastMeasuredIndex = Math.max(0, this._lastMeasuredIndex); + + if (lastMeasuredCellSizeAndPosition.offset >= offset) { + return this._binarySearch(lastMeasuredIndex, 0, offset); + } else { + return this._exponentialSearch(lastMeasuredIndex, offset); + } + } +} diff --git a/packages/semi-json-viewer-core/src/view/virtualized/ScalingCellSizeAndPositionManager.ts b/packages/semi-json-viewer-core/src/view/virtualized/ScalingCellSizeAndPositionManager.ts new file mode 100644 index 0000000000..1533536afd --- /dev/null +++ b/packages/semi-json-viewer-core/src/view/virtualized/ScalingCellSizeAndPositionManager.ts @@ -0,0 +1,189 @@ +//reference from https://github.com/bvaughn/react-virtualized +import { CellSizeAndPositionManager } from './CellSizeAndPositionManager'; +import { CellSizeGetter, Alignment, VisibleCellRange } from './types'; + +type Params = { + maxScrollSize?: number; + cellCount: number; + cellSizeGetter: CellSizeGetter; + estimatedCellSize: number +}; + +type ContainerSizeAndOffset = { + containerSize: number; + offset: number +}; + +const DEFAULT_MAX_ELEMENT_SIZE = 1500000; +const CHROME_MAX_ELEMENT_SIZE = 1.67771e7; + +const isBrowser = () => typeof window !== 'undefined'; +// @ts-ignore +const isChrome = () => !!window.chrome; + +export const getMaxElementSize = (): number => { + if (isBrowser()) { + if (isChrome()) { + return CHROME_MAX_ELEMENT_SIZE; + } + } + return DEFAULT_MAX_ELEMENT_SIZE; +}; + +export class ScalingCellSizeAndPositionManager { + private _maxScrollSize: number; + private _cellSizeAndPositionManager: CellSizeAndPositionManager; + + constructor({ maxScrollSize = getMaxElementSize(), cellCount, cellSizeGetter, estimatedCellSize }: Params) { + this._maxScrollSize = maxScrollSize; + this._cellSizeAndPositionManager = new CellSizeAndPositionManager({ + cellCount, + cellSizeGetter, + estimatedCellSize, + }); + } + + areOffsetsAdjusted() { + return this._cellSizeAndPositionManager.getTotalSize() > this._maxScrollSize; + } + + configure(params: { cellCount: number; estimatedCellSize: number; cellSizeGetter: CellSizeGetter }) { + this._cellSizeAndPositionManager.configure(params); + } + + getCellCount(): number { + return this._cellSizeAndPositionManager.getCellCount(); + } + + getEstimatedCellSize(): number { + return this._cellSizeAndPositionManager.getEstimatedCellSize(); + } + + getLastMeasuredIndex(): number { + return this._cellSizeAndPositionManager.getLastMeasuredIndex(); + } + + getOffsetAdjustment({ containerSize, offset }: ContainerSizeAndOffset) { + const totalSize = this._cellSizeAndPositionManager.getTotalSize(); + + const safeTotalSize = this.getTotalSize(); + + const offsetPercentage = this._getOffsetPercentage({ + containerSize, + offset, + totalSize: safeTotalSize, + }); + return Math.round(offsetPercentage * (safeTotalSize - totalSize)); + } + + getTotalSize(): number { + return Math.min(this._maxScrollSize, this._cellSizeAndPositionManager.getTotalSize()); + } + getSizeAndPositionOfCell(index: number) { + return this._cellSizeAndPositionManager.getSizeAndPositionOfCell(index); + } + + getSizeAndPositionOfLastMeasuredCell() { + return this._cellSizeAndPositionManager.getSizeAndPositionOfLastMeasuredCell(); + } + + getVisibleCellRange({ + containerSize, + offset, // safe + }: ContainerSizeAndOffset): VisibleCellRange { + offset = this._safeOffsetToOffset({ + containerSize, + offset, + }); + + return this._cellSizeAndPositionManager.getVisibleCellRange({ + containerSize, + offset, + }); + } + + resetCell(index: number): void { + this._cellSizeAndPositionManager.resetCell(index); + } + + getUpdatedOffsetForIndex({ + align = 'auto', + containerSize, + currentOffset, // safe + targetIndex, + }: { + align: Alignment; + containerSize: number; + currentOffset: number; + targetIndex: number + }) { + currentOffset = this._safeOffsetToOffset({ + containerSize, + offset: currentOffset, + }); + + const offset = this._cellSizeAndPositionManager.getUpdatedOffsetForIndex({ + align, + containerSize, + currentOffset, + targetIndex, + }); + + return this._offsetToSafeOffset({ + containerSize, + offset, + }); + } + + private _getOffsetPercentage({ + containerSize, + offset, // safe + totalSize, + }: { + containerSize: number; + offset: number; + totalSize: number + }) { + return totalSize <= containerSize ? 0 : offset / (totalSize - containerSize); + } + + private _offsetToSafeOffset({ + containerSize, + offset, // unsafe + }: ContainerSizeAndOffset): number { + const totalSize = this._cellSizeAndPositionManager.getTotalSize(); + const safeTotalSize = this.getTotalSize(); + + if (totalSize === safeTotalSize) { + return offset; + } else { + const offsetPercentage = this._getOffsetPercentage({ + containerSize, + offset, + totalSize, + }); + + return Math.round(offsetPercentage * (safeTotalSize - containerSize)); + } + } + + private _safeOffsetToOffset({ + containerSize, + offset, // safe + }: ContainerSizeAndOffset): number { + const totalSize = this._cellSizeAndPositionManager.getTotalSize(); + const safeTotalSize = this.getTotalSize(); + + if (totalSize === safeTotalSize) { + return offset; + } else { + const offsetPercentage = this._getOffsetPercentage({ + containerSize, + offset, + totalSize: safeTotalSize, + }); + + return Math.round(offsetPercentage * (totalSize - containerSize)); + } + } +} diff --git a/packages/semi-json-viewer-core/src/view/virtualized/types.ts b/packages/semi-json-viewer-core/src/view/virtualized/types.ts new file mode 100644 index 0000000000..023b29f41b --- /dev/null +++ b/packages/semi-json-viewer-core/src/view/virtualized/types.ts @@ -0,0 +1,10 @@ +export type CellSizeGetter = (params: { index: number }) => number; + +export type CellSize = CellSizeGetter | number; + +export type Alignment = 'auto' | 'end' | 'start' | 'center'; + +export type VisibleCellRange = { + start?: number; + stop?: number +}; diff --git a/packages/semi-json-viewer-core/src/worker/json.worker.ts b/packages/semi-json-viewer-core/src/worker/json.worker.ts new file mode 100644 index 0000000000..4aaeb58cf6 --- /dev/null +++ b/packages/semi-json-viewer-core/src/worker/json.worker.ts @@ -0,0 +1,40 @@ +import { FormattingOptions } from 'jsonc-parser'; +import { JsonWorker } from './jsonWorker'; + +let jsonWorker: JsonWorker | null = null; + +self.onmessage = (e: MessageEvent) => { + const { method, params, messageId } = e.data; + + if (method === 'init') { + jsonWorker = new JsonWorker(params.value); + self.postMessage({ messageId, result: 'Worker initialized' }); + return; + } + + if (!jsonWorker) { + self.postMessage({ messageId, error: 'Worker not initialized' }); + return; + } + + let result; + switch (method) { + case 'updateModel': + jsonWorker.updateModel(params.op); + result = jsonWorker.getModel()?.getValue(); + break; + case 'format': + result = jsonWorker.format(params.options as FormattingOptions); + break; + case 'foldRange': + result = jsonWorker.foldRange(); + break; + case 'validate': + result = jsonWorker.validate(); + break; + default: + result = { error: 'Unknown method' }; + } + + self.postMessage({ messageId, result }); +}; diff --git a/packages/semi-json-viewer-core/src/worker/jsonWorker.ts b/packages/semi-json-viewer-core/src/worker/jsonWorker.ts new file mode 100644 index 0000000000..bc41fbcfbd --- /dev/null +++ b/packages/semi-json-viewer-core/src/worker/jsonWorker.ts @@ -0,0 +1,42 @@ +import { formatJson, getFoldingRanges, doValidate, parseJsonAst } from '../service/jsonService'; +import { JSONModel } from '../model/jsonModel'; +import { FormattingOptions } from 'jsonc-parser'; +import { createModel } from '../model'; +import { IModelContentChangeEvent } from '../common/emitterEvents'; + +export class JsonWorker { + private _model: JSONModel | null = null; + + constructor(value: string) { + this._model = createModel(value); + } + + getModel() { + return this._model; + } + + format(options: FormattingOptions) { + if (!this._model) throw new Error('Model not initialized'); + return formatJson(this._model, options); + } + + foldRange() { + if (!this._model) throw new Error('Model not initialized'); + return getFoldingRanges(this._model); + } + + validate() { + if (!this._model) throw new Error('Model not initialized'); + return doValidate(this._model); + } + + updateModel(op: IModelContentChangeEvent | IModelContentChangeEvent[]) { + this._model?.applyOperation(op); + return op; + } + + parse() { + if (!this._model) throw new Error('Model not initialized'); + return parseJsonAst(this._model); + } +} diff --git a/packages/semi-json-viewer-core/src/worker/jsonWorkerManager.ts b/packages/semi-json-viewer-core/src/worker/jsonWorkerManager.ts new file mode 100644 index 0000000000..ed764da377 --- /dev/null +++ b/packages/semi-json-viewer-core/src/worker/jsonWorkerManager.ts @@ -0,0 +1,100 @@ +import { IModelContentChangeEvent } from '../common/emitterEvents'; +import { FormattingOptions } from 'jsonc-parser'; +import { getCurrentNameSpaceId } from '../common/nameSpace'; + +//TODO 修改封装方式 + +/** + * JsonWorkerManager 类用于管理 JSON Worker + */ +type WorkerMethod = 'init' | 'updateModel' | 'format' | 'foldRange' | 'validate'; +type WorkerParams = { + value?: string; + options?: FormattingOptions; + op?: IModelContentChangeEvent | IModelContentChangeEvent[] +}; + +const workerManagerMap = new Map(); + +export class JsonWorkerManager { + private _worker: Worker; + private _callbacks: Map void>; + + constructor() { + const workerRaw = decodeURIComponent('%WORKER_RAW%'); + const blob = new Blob([workerRaw], { type: 'application/javascript' }); + const workerURL = URL.createObjectURL(blob); + this._worker = new Worker(workerURL); + this._callbacks = new Map(); + + this._worker.onmessage = this._handleWorkerMessage.bind(this); + } + + async init(value: string) { + await this._sendRequest('init', { value }); + } + + updateModel(op: IModelContentChangeEvent | IModelContentChangeEvent[]) { + return this._sendRequest('updateModel', { op }); + } + + formatJson(options: FormattingOptions) { + return this._sendRequest('format', { options }); + } + + foldRange() { + return this._sendRequest('foldRange', {}); + } + + validate() { + return this._sendRequest('validate', {}); + } + + private _sendRequest(method: WorkerMethod, params: WorkerParams): Promise { + return new Promise((resolve, reject) => { + const messageId = Date.now() + Math.random(); + this._callbacks.set(messageId, resolve); + this._worker.postMessage({ messageId, method, params }); + }); + } + + private _handleWorkerMessage(event: MessageEvent) { + const { messageId, result, error } = event.data; + const callback = this._callbacks.get(messageId); + if (callback) { + if (error) { + callback(new Error(error)); + } else { + callback(result); + } + this._callbacks.delete(messageId); + } + } + + public dispose() { + this._worker.terminate(); + this._callbacks.clear(); + } +} + +export function getJsonWorkerManager() { + const currentNameSpaceId = getCurrentNameSpaceId(); + if (!currentNameSpaceId) { + throw new Error('No active worker ID set'); + } + + let workerManager = workerManagerMap.get(currentNameSpaceId); + if (!workerManager) { + workerManager = new JsonWorkerManager(); + workerManagerMap.set(currentNameSpaceId, workerManager); + } + return workerManager; +} + +export function disposeWorkerManager(id: string) { + const workerManager = workerManagerMap.get(id); + if (workerManager) { + workerManagerMap.delete(id); + workerManager.dispose(); + } +} diff --git a/packages/semi-ui/index.ts b/packages/semi-ui/index.ts index bc205a6fda..687008f69c 100644 --- a/packages/semi-ui/index.ts +++ b/packages/semi-ui/index.ts @@ -123,4 +123,5 @@ export { ResizeGroup } from './resizable'; +export { default as JsonViewer } from './jsonViewer'; export { default as DragMove } from './dragMove'; diff --git a/packages/semi-ui/jsonViewer/_story/jsonViewer.stories.jsx b/packages/semi-ui/jsonViewer/_story/jsonViewer.stories.jsx new file mode 100644 index 0000000000..37461a87d3 --- /dev/null +++ b/packages/semi-ui/jsonViewer/_story/jsonViewer.stories.jsx @@ -0,0 +1,95 @@ +import React, { useState, useEffect, useRef } from 'react'; + +import JsonViewer from '../index'; +import Button from '../../button'; +export default { + title: 'JsonViewer', +}; + +const baseStr = `{ + "min_position": 1, + "has_more_items": true, + "items_html": "Bike", + "new_latent_count": 0, + "data": { + "length": 22, + "text": "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." + }, + "numericalArray": [23, 29, 28, 26, 23], + "StringArray": ["Oxygen", "Oxygen", "Oxygen", "Carbon"], + "multipleTypesArray": 3, + "objArray": [ + {}, + { + "class": "upper", + "name": "Mark", + "age": 7 + }, + { + "class": "upper", + "name": "Tom", + "age": 1 + }, + { + "class": "lower", + "name": "Jerry", + "age": 5 + }, + { + "class": "lower", + "name": "Alice", + "age": 3 + } + ] +}`; + +export const DefaultJsonViewer = () => { + const hoverHandler = (value, target) => { + const el = document.createElement('div'); + el.style.backgroundColor = '#f5f5f5'; + el.style.width = '100px'; + el.style.height = '100px'; + el.style.border = '1px solid #0080ff'; + if (value.startsWith('"http')) { + const img = document.createElement('img'); + const regex = /["']/g; + const src = value.replace(regex, ''); + img.src = src; + el.appendChild(img); + } else { + el.innerHTML = 'This is a self -defined rendering of the user'; + } + return el; + }; + + const onChangeHandler = value => { + console.log(value, 'value'); + }; + + const [autoWrap, setAutoWrap] = useState(true); + const [lineHeight, setLineHeight] = useState(20); + const jsonviewerRef = useRef(null); + + return ( + <> + + + + + + ); +}; diff --git a/packages/semi-ui/jsonViewer/_story/jsonViewer.stories.tsx b/packages/semi-ui/jsonViewer/_story/jsonViewer.stories.tsx new file mode 100644 index 0000000000..663de093f6 --- /dev/null +++ b/packages/semi-ui/jsonViewer/_story/jsonViewer.stories.tsx @@ -0,0 +1,59 @@ +import React from "react" +import JsonViewer from "../index" + + + +export default { + title: 'JsonViewer', +} + +const baseStr = `{ + "min_position": 9, + "has_more_items": true, + "items_html": "Bike", + "new_latent_count": 0, + "data": { + "length": 22, + "text": "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." + }, + "numericalArray": [ + 23, + 29, + 28, + 26, + 23 + ], + "StringArray": [ + "Oxygen", + "Oxygen", + "Oxygen", + "Carbon" + ], + "multipleTypesArray": 3, + "objArray": [ + { + + }, + { + "class": "upper", + "age": 7 + }, + { + "class": "upper", + "age": 1 + }, + { + "class": "lower", + "age": 5 + }, + { + "class": "lower", + "age": 3 + } + ] + }`; + +export const DefaultJsonViewer = () => { + + return +} \ No newline at end of file diff --git a/packages/semi-ui/jsonViewer/_story/utils.ts b/packages/semi-ui/jsonViewer/_story/utils.ts new file mode 100644 index 0000000000..2cb77a6b68 --- /dev/null +++ b/packages/semi-ui/jsonViewer/_story/utils.ts @@ -0,0 +1,61 @@ +type JsonValue = string | number | boolean | JsonObject | JsonArray; +interface JsonObject { + [key: string]: JsonValue +} +type JsonArray = Array; + +export function generateJsonString(count: number, nested: number): string { + function generateRandomString(): string { + const length = Math.floor(Math.random() * 20) + 5; + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + return Array.from({ length }, () => characters[Math.floor(Math.random() * characters.length)]).join(''); + } + + function generateRandomObject(depth: number): JsonObject { + const obj: JsonObject = {}; + const prefixes = ['id', 'name', 'value', 'data', 'item']; + + // 始终生成5个键值对,每种类型各一个 + obj[`${prefixes[0]}`] = Math.floor(Math.random() * 1000); // number + obj[`${prefixes[1]}`] = generateRandomString(); // string + obj[`${prefixes[2]}`] = Math.random() > 0.5; // boolean + if (depth < nested) { + obj[`${prefixes[3]}`] = generateJsonArray(depth + 1); // array + obj[`${prefixes[4]}`] = generateRandomObject(depth + 1); // object + } else { + // 在达到最大深度时,用基本类型替代 + obj[`${prefixes[3]}`] = Math.floor(Math.random() * 1000); // 用number替代array + obj[`${prefixes[4]}`] = generateRandomString(); // 用string替代object + } + return obj; + } + + function generateJsonArray(depth: number): JsonArray { + const array: JsonArray = []; + + // 始终生成5个元素,每种类型各一个 + array.push(Math.floor(Math.random() * 1000)); // number + array.push(generateRandomString()); // string + array.push(Math.random() > 0.5); // boolean + if (depth < nested) { + array.push(generateJsonArray(depth + 1)); // array + array.push(generateRandomObject(depth + 1)); // object + } else { + // 在达到最大深度时,用基本类型替代 + array.push(Math.floor(Math.random() * 1000)); // 用number替代array + array.push(generateRandomString()); // 用string替代object + } + return array; + } + + function generateJson(): JsonObject[] { + const json: JsonObject[] = []; + for (let i = 0; i < count; i++) { + json.push(generateRandomObject(1)); + } + return json; + } + + const json = generateJson(); + return JSON.stringify(json, null, 4); // 格式化输出 +} diff --git a/packages/semi-ui/jsonViewer/index.tsx b/packages/semi-ui/jsonViewer/index.tsx new file mode 100644 index 0000000000..facaf204c7 --- /dev/null +++ b/packages/semi-ui/jsonViewer/index.tsx @@ -0,0 +1,302 @@ +import React from 'react'; +import classNames from 'classnames'; +import JsonViewerFoundation, { + JsonViewerOptions, + JsonViewerAdapter, +} from '@douyinfe/semi-foundation/jsonViewer/foundation'; +import '@douyinfe/semi-foundation/jsonViewer/jsonViewer.scss'; +import { cssClasses } from '@douyinfe/semi-foundation/jsonViewer/constants'; +import ButtonGroup from '../button/buttonGroup'; +import Button from '../button'; +import Input from '../input'; +import DragMove from '../dragMove'; +import { + IconCaseSensitive, + IconChevronLeft, + IconChevronRight, + IconClose, + IconRegExp, + IconSearch, + IconWholeWord, +} from '@douyinfe/semi-icons'; +import BaseComponent, { BaseProps } from '../_base/baseComponent'; +const prefixCls = cssClasses.PREFIX; + +export { JsonViewerOptions }; +export interface JsonViewerProps extends BaseProps { + value: string; + width: number; + height: number; + className?: string; + style?: React.CSSProperties; + onChange?: (value: string) => void; + renderTooltip?: (value: string, el: HTMLElement) => HTMLElement; + options?: JsonViewerOptions +} + +export interface JsonViewerState { + searchOptions: SearchOptions; + showSearchBar: boolean +} + +interface SearchOptions { + caseSensitive: boolean; + wholeWord: boolean; + regex: boolean +} + +class JsonViewerCom extends BaseComponent { + static defaultProps: Partial = { + width: 400, + height: 400, + value: '', + }; + + private editorRef: React.RefObject; + private searchInputRef: React.RefObject; + private replaceInputRef: React.RefObject; + + foundation: JsonViewerFoundation; + + constructor(props: JsonViewerProps) { + super(props); + this.editorRef = React.createRef(); + this.searchInputRef = React.createRef(); + this.replaceInputRef = React.createRef(); + this.foundation = new JsonViewerFoundation(this.adapter); + this.state = { + searchOptions: { + caseSensitive: false, + wholeWord: false, + regex: false, + }, + showSearchBar: false, + }; + } + + componentDidMount() { + this.foundation.init(); + } + + componentDidUpdate(prevProps: JsonViewerProps): void { + if (prevProps.options !== this.props.options) { + this.foundation.jsonViewer.dispose(); + this.foundation.init(); + } + } + + get adapter(): JsonViewerAdapter { + return { + ...super.adapter, + getEditorRef: () => this.editorRef.current, + getSearchRef: () => this.searchInputRef.current, + notifyChange: value => { + this.props.onChange?.(value); + }, + notifyHover: (value, el) => { + const res = this.props.renderTooltip?.(value, el); + return res; + }, + setSearchOptions: (key: string) => { + this.setState( + { + searchOptions: { + ...this.state.searchOptions, + [key]: !this.state.searchOptions[key], + }, + }, + () => { + this.searchHandler(); + } + ); + }, + showSearchBar: () => { + this.setState({ showSearchBar: !this.state.showSearchBar }); + }, + }; + } + + getValue() { + return this.foundation.jsonViewer.getModel().getValue(); + } + + format() { + this.foundation.jsonViewer.format(); + } + + getStyle() { + const { width, height } = this.props; + return { + width, + height, + }; + } + + searchHandler = () => { + const value = this.searchInputRef.current?.value; + this.foundation.search(value); + }; + + changeSearchOptions = (key: string) => { + this.foundation.setSearchOptions(key); + }; + + renderSearchBox() { + return ( +
    + {this.renderSearchBar()} + {this.renderReplaceBar()} +
    + ); + } + + renderSearchOptions() { + const searchOptionItems = [ + { + key: 'caseSensitive', + icon: IconCaseSensitive, + }, + { + key: 'regex', + icon: IconRegExp, + }, + { + key: 'wholeWord', + icon: IconWholeWord, + }, + ]; + + return ( +
      + {searchOptionItems.map(({ key, icon: Icon }) => ( +
    • + this.changeSearchOptions(key)} /> +
    • + ))} +
    + ); + } + + renderSearchBar() { + return ( +
    + { + e.preventDefault(); + this.searchHandler(); + this.searchInputRef.current?.focus(); + }} + ref={this.searchInputRef} + /> + {this.renderSearchOptions()} + +
    + ); + } + + renderReplaceBar() { + return ( +
    + { + e.preventDefault(); + }} + ref={this.replaceInputRef} + /> + + +
    + ); + } + + render() { + let isDragging = false; + const { width, className, style, ...rest } = this.props; + return ( + <> +
    +
    + { + isDragging = false; + }} + onMouseMove={() => { + isDragging = true; + }} + > +
    + {!this.state.showSearchBar ? ( +
    +
    +
    + + ); + } +} + +export default JsonViewerCom; diff --git a/packages/semi-ui/package.json b/packages/semi-ui/package.json index 1cc0b77a90..01395610b3 100644 --- a/packages/semi-ui/package.json +++ b/packages/semi-ui/package.json @@ -32,6 +32,7 @@ "date-fns": "^2.29.3", "date-fns-tz": "^1.3.8", "fast-copy": "^3.0.1 ", + "jsonc-parser": "^3.3.1", "lodash": "^4.17.21", "prop-types": "^15.7.2", "react-resizable": "^3.0.5", diff --git a/src/images/docIcons/doc-jsonviewer.svg b/src/images/docIcons/doc-jsonviewer.svg new file mode 100644 index 0000000000..acda2cb650 --- /dev/null +++ b/src/images/docIcons/doc-jsonviewer.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/yarn.lock b/yarn.lock index 0985b19294..4f54a5615d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1585,25 +1585,11 @@ "@douyinfe/semi-animation-styled" "2.65.0" classnames "^2.2.6" -"@douyinfe/semi-animation-react@2.69.2": - version "2.69.2" - resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation-react/-/semi-animation-react-2.69.2.tgz#b47565c64dae7f4e1a7c5a9a21a244d59986b3fd" - integrity sha512-N6bdju90nnQdNHmnp5C8n8oqSDqqzgO6rzCPwwb6Ef4+aC/csdU1/Dsdp6JA6QKQ768oHGPT5YJs3QiKSGZZcw== - dependencies: - "@douyinfe/semi-animation" "2.69.2" - "@douyinfe/semi-animation-styled" "2.69.2" - classnames "^2.2.6" - "@douyinfe/semi-animation-styled@2.65.0": version "2.65.0" resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation-styled/-/semi-animation-styled-2.65.0.tgz#8c56047a5704a45b05cc9809a2a126cc24526ea1" integrity sha512-YFF8Ptcz/jwS0phm28XZV7ROqMQ233sjVR0Uy33FImCITr6EAPe5wcCeEmzVZoYS7x3tUFR30SF+0hSO01rQUg== -"@douyinfe/semi-animation-styled@2.69.2": - version "2.69.2" - resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation-styled/-/semi-animation-styled-2.69.2.tgz#18c16a959c92e908aa4fad521fe7e0fe83296034" - integrity sha512-HHHR2qS7BRCtP78qp9N/OL9RWPvoxxRg6uC6kUm8l4t5FCcr0QrdhkzYIpohAVa90BedpTPwhRHhh3aiXfnx9A== - "@douyinfe/semi-animation@2.65.0": version "2.65.0" resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation/-/semi-animation-2.65.0.tgz#f544a6b420c3e948c09836019e6b63f1382cd12c" @@ -1611,13 +1597,6 @@ dependencies: bezier-easing "^2.1.0" -"@douyinfe/semi-animation@2.69.2": - version "2.69.2" - resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation/-/semi-animation-2.69.2.tgz#4023340747eb202f5e3b2d48dfd4efea94d815cb" - integrity sha512-elut0fb5eKr5pnrZKgaOS97nw+KxkoL4N+tho4u099a3K5GFwzvyzVPOK0ALReCWnO0tSxUbwPpUQxMomG+vKA== - dependencies: - bezier-easing "^2.1.0" - "@douyinfe/semi-foundation@2.65.0": version "2.65.0" resolved "https://registry.yarnpkg.com/@douyinfe/semi-foundation/-/semi-foundation-2.65.0.tgz#20466a9b4baacdde2249930fb709ba035c5a7bea" @@ -1637,25 +1616,6 @@ remark-gfm "^4.0.0" scroll-into-view-if-needed "^2.2.24" -"@douyinfe/semi-foundation@2.69.2": - version "2.69.2" - resolved "https://registry.yarnpkg.com/@douyinfe/semi-foundation/-/semi-foundation-2.69.2.tgz#2a782760511e509410df87e473e956465e8b1a6f" - integrity sha512-qiN1uBxEs+ofIAGOw6oF7AgTXDlfgmbl9xYTDsS5D3tSH0p0hjAsqW2d59/lScr3P7dYzxKOXZ6aJrC5TXW3Wg== - dependencies: - "@douyinfe/semi-animation" "2.69.2" - "@mdx-js/mdx" "^3.0.1" - async-validator "^3.5.0" - classnames "^2.2.6" - date-fns "^2.29.3" - date-fns-tz "^1.3.8" - fast-copy "^3.0.1 " - lodash "^4.17.21" - lottie-web "^5.12.2" - memoize-one "^5.2.1" - prismjs "^1.29.0" - remark-gfm "^4.0.0" - scroll-into-view-if-needed "^2.2.24" - "@douyinfe/semi-icons@2.65.0", "@douyinfe/semi-icons@latest": version "2.65.0" resolved "https://registry.yarnpkg.com/@douyinfe/semi-icons/-/semi-icons-2.65.0.tgz#af39cbd5431ebccedcf7d9ce689646e54bebc432" @@ -1663,23 +1623,11 @@ dependencies: classnames "^2.2.6" -"@douyinfe/semi-icons@2.69.2", "@douyinfe/semi-icons@^2.0.0": - version "2.69.2" - resolved "https://registry.yarnpkg.com/@douyinfe/semi-icons/-/semi-icons-2.69.2.tgz#92ade6402237c1a98d4f39a6d447a874848ea066" - integrity sha512-0Wzb4bd5DYZjlcR9JS2Cv5D7LeSApy0TD4BMp8rHRStp9iNIGnB/Fob7OFAz9ZuceJ8nb+IN4uxQyf0GuNh/tA== - dependencies: - classnames "^2.2.6" - "@douyinfe/semi-illustrations@2.65.0": version "2.65.0" resolved "https://registry.yarnpkg.com/@douyinfe/semi-illustrations/-/semi-illustrations-2.65.0.tgz#9916c540c91222a1d9f48cd34a941d28b8a05d2f" integrity sha512-1IhOztyBYiSu8WrcvN+oWWtcJTC9+x6zbnYtufx4ToISs5UO1te1PQofABpkDzIJYFtW9yYLxg4uoL4wGjqYMA== -"@douyinfe/semi-illustrations@2.69.2": - version "2.69.2" - resolved "https://registry.yarnpkg.com/@douyinfe/semi-illustrations/-/semi-illustrations-2.69.2.tgz#aac0c5c65c1363c86ab6dcfd702a0d4d9a3a1c23" - integrity sha512-rdRB6ZZ2zo2c/e0Hkffq84i0w90BMmhBGskvei8AWDlfiuTJhXL6qZ3ixZiE+fjPQO12mk/QNG+LNv8Tr5yFfQ== - "@douyinfe/semi-scss-compile@2.23.2": version "2.23.2" resolved "https://registry.yarnpkg.com/@douyinfe/semi-scss-compile/-/semi-scss-compile-2.23.2.tgz#30884bb194ee9ae1e81877985e5663c3297c1ced" @@ -1751,38 +1699,6 @@ resolved "https://registry.yarnpkg.com/@douyinfe/semi-theme-default/-/semi-theme-default-2.61.0.tgz#a7e9bf9534721c12af1d0eeb5d5a2de615896a23" integrity sha512-obn/DOw4vZyKFAlWvZxHTpBLAK9FO9kygTSm2GROgvi+UDB2PPU6l20cuUCsdGUNWJRSqYlTTVZ1tNYIyFZ5Sg== -"@douyinfe/semi-theme-default@2.69.2": - version "2.69.2" - resolved "https://registry.yarnpkg.com/@douyinfe/semi-theme-default/-/semi-theme-default-2.69.2.tgz#6256c55f07e34b8f134e5a7a2f9fe85970387bf3" - integrity sha512-pyol1EFUwuErp6Tlw+VLs4nVjnj1PSEC5P3YB0MpIcDWZsH/ixRsgMAGuvWI/+owwZ6KnzRDA0t+XcGm+e17qA== - -"@douyinfe/semi-ui@^2.0.0": - version "2.69.2" - resolved "https://registry.yarnpkg.com/@douyinfe/semi-ui/-/semi-ui-2.69.2.tgz#9f74898cc865fb01c622aa58f6205cbd11f3aa47" - integrity sha512-oDI3jLlwugpF8vNx6R+ivwTh1hu7Sr8Yrb3+8nsxNlc9+C+RHA01uu4xGmORwHQdYe2+YRn+kFP8+tu2Ch90Nw== - dependencies: - "@dnd-kit/core" "^6.0.8" - "@dnd-kit/sortable" "^7.0.2" - "@dnd-kit/utilities" "^3.2.1" - "@douyinfe/semi-animation" "2.69.2" - "@douyinfe/semi-animation-react" "2.69.2" - "@douyinfe/semi-foundation" "2.69.2" - "@douyinfe/semi-icons" "2.69.2" - "@douyinfe/semi-illustrations" "2.69.2" - "@douyinfe/semi-theme-default" "2.69.2" - async-validator "^3.5.0" - classnames "^2.2.6" - copy-text-to-clipboard "^2.1.1" - date-fns "^2.29.3" - date-fns-tz "^1.3.8" - fast-copy "^3.0.1 " - lodash "^4.17.21" - prop-types "^15.7.2" - react-resizable "^3.0.5" - react-window "^1.8.2" - scroll-into-view-if-needed "^2.2.24" - utility-types "^3.10.0" - "@douyinfe/semi-ui@latest": version "2.65.0" resolved "https://registry.yarnpkg.com/@douyinfe/semi-ui/-/semi-ui-2.65.0.tgz#295eb0dd8e9e961adb4ddd7c7bbce3468d1b7430" @@ -1913,6 +1829,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz#d1bc06aedb6936b3b6d313bf809a5a40387d2b7f" integrity sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA== +"@esbuild/aix-ppc64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz#b57697945b50e99007b4c2521507dc613d4a648c" + integrity sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw== + "@esbuild/android-arm64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.16.17.tgz#cf91e86df127aa3d141744edafcba0abdc577d23" @@ -1933,6 +1854,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz#7ad65a36cfdb7e0d429c353e00f680d737c2aed4" integrity sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA== +"@esbuild/android-arm64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz#1add7e0af67acefd556e407f8497e81fddad79c0" + integrity sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w== + "@esbuild/android-arm@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.16.17.tgz#025b6246d3f68b7bbaa97069144fb5fb70f2fff2" @@ -1953,6 +1879,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.12.tgz#b0c26536f37776162ca8bde25e42040c203f2824" integrity sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w== +"@esbuild/android-arm@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.24.0.tgz#ab7263045fa8e090833a8e3c393b60d59a789810" + integrity sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew== + "@esbuild/android-x64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.16.17.tgz#c820e0fef982f99a85c4b8bfdd582835f04cd96e" @@ -1973,6 +1904,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.12.tgz#cb13e2211282012194d89bf3bfe7721273473b3d" integrity sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew== +"@esbuild/android-x64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.24.0.tgz#e8f8b196cfdfdd5aeaebbdb0110983460440e705" + integrity sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ== + "@esbuild/darwin-arm64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.16.17.tgz#edef4487af6b21afabba7be5132c26d22379b220" @@ -1993,6 +1929,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz#cbee41e988020d4b516e9d9e44dd29200996275e" integrity sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g== +"@esbuild/darwin-arm64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz#2d0d9414f2acbffd2d86e98253914fca603a53dd" + integrity sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw== + "@esbuild/darwin-x64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.16.17.tgz#42829168730071c41ef0d028d8319eea0e2904b4" @@ -2013,6 +1954,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz#e37d9633246d52aecf491ee916ece709f9d5f4cd" integrity sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A== +"@esbuild/darwin-x64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz#33087aab31a1eb64c89daf3d2cf8ce1775656107" + integrity sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA== + "@esbuild/freebsd-arm64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.17.tgz#1f4af488bfc7e9ced04207034d398e793b570a27" @@ -2033,6 +1979,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz#1ee4d8b682ed363b08af74d1ea2b2b4dbba76487" integrity sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA== +"@esbuild/freebsd-arm64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz#bb76e5ea9e97fa3c753472f19421075d3a33e8a7" + integrity sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA== + "@esbuild/freebsd-x64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.16.17.tgz#636306f19e9bc981e06aa1d777302dad8fddaf72" @@ -2053,6 +2004,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz#37a693553d42ff77cd7126764b535fb6cc28a11c" integrity sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg== +"@esbuild/freebsd-x64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz#e0e2ce9249fdf6ee29e5dc3d420c7007fa579b93" + integrity sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ== + "@esbuild/linux-arm64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.16.17.tgz#a003f7ff237c501e095d4f3a09e58fc7b25a4aca" @@ -2073,6 +2029,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz#be9b145985ec6c57470e0e051d887b09dddb2d4b" integrity sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA== +"@esbuild/linux-arm64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz#d1b2aa58085f73ecf45533c07c82d81235388e75" + integrity sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g== + "@esbuild/linux-arm@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.16.17.tgz#b591e6a59d9c4fe0eeadd4874b157ab78cf5f196" @@ -2093,6 +2054,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz#207ecd982a8db95f7b5279207d0ff2331acf5eef" integrity sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w== +"@esbuild/linux-arm@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz#8e4915df8ea3e12b690a057e77a47b1d5935ef6d" + integrity sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw== + "@esbuild/linux-ia32@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.16.17.tgz#24333a11027ef46a18f57019450a5188918e2a54" @@ -2113,6 +2079,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz#d0d86b5ca1562523dc284a6723293a52d5860601" integrity sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA== +"@esbuild/linux-ia32@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz#8200b1110666c39ab316572324b7af63d82013fb" + integrity sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA== + "@esbuild/linux-loong64@0.14.54": version "0.14.54" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz#de2a4be678bd4d0d1ffbb86e6de779cde5999028" @@ -2138,6 +2109,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz#9a37f87fec4b8408e682b528391fa22afd952299" integrity sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA== +"@esbuild/linux-loong64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz#6ff0c99cf647504df321d0640f0d32e557da745c" + integrity sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g== + "@esbuild/linux-mips64el@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.16.17.tgz#4e5967a665c38360b0a8205594377d4dcf9c3726" @@ -2158,6 +2134,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz#4ddebd4e6eeba20b509d8e74c8e30d8ace0b89ec" integrity sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w== +"@esbuild/linux-mips64el@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz#3f720ccd4d59bfeb4c2ce276a46b77ad380fa1f3" + integrity sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA== + "@esbuild/linux-ppc64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.16.17.tgz#206443a02eb568f9fdf0b438fbd47d26e735afc8" @@ -2178,6 +2159,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz#adb67dadb73656849f63cd522f5ecb351dd8dee8" integrity sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg== +"@esbuild/linux-ppc64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz#9d6b188b15c25afd2e213474bf5f31e42e3aa09e" + integrity sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ== + "@esbuild/linux-riscv64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.16.17.tgz#c351e433d009bf256e798ad048152c8d76da2fc9" @@ -2198,6 +2184,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz#11bc0698bf0a2abf8727f1c7ace2112612c15adf" integrity sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg== +"@esbuild/linux-riscv64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz#f989fdc9752dfda286c9cd87c46248e4dfecbc25" + integrity sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw== + "@esbuild/linux-s390x@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.16.17.tgz#661f271e5d59615b84b6801d1c2123ad13d9bd87" @@ -2218,6 +2209,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz#e86fb8ffba7c5c92ba91fc3b27ed5a70196c3cc8" integrity sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg== +"@esbuild/linux-s390x@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz#29ebf87e4132ea659c1489fce63cd8509d1c7319" + integrity sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g== + "@esbuild/linux-x64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.16.17.tgz#e4ba18e8b149a89c982351443a377c723762b85f" @@ -2238,6 +2234,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz#5f37cfdc705aea687dfe5dfbec086a05acfe9c78" integrity sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg== +"@esbuild/linux-x64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz#4af48c5c0479569b1f359ffbce22d15f261c0cef" + integrity sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA== + "@esbuild/netbsd-x64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.16.17.tgz#7d4f4041e30c5c07dd24ffa295c73f06038ec775" @@ -2258,6 +2259,16 @@ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz#29da566a75324e0d0dd7e47519ba2f7ef168657b" integrity sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA== +"@esbuild/netbsd-x64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz#1ae73d23cc044a0ebd4f198334416fb26c31366c" + integrity sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg== + +"@esbuild/openbsd-arm64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz#5d904a4f5158c89859fd902c427f96d6a9e632e2" + integrity sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg== + "@esbuild/openbsd-x64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.16.17.tgz#970fa7f8470681f3e6b1db0cc421a4af8060ec35" @@ -2278,6 +2289,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz#306c0acbdb5a99c95be98bdd1d47c916e7dc3ff0" integrity sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw== +"@esbuild/openbsd-x64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz#4c8aa88c49187c601bae2971e71c6dc5e0ad1cdf" + integrity sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q== + "@esbuild/sunos-x64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.16.17.tgz#abc60e7c4abf8b89fb7a4fe69a1484132238022c" @@ -2298,6 +2314,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz#0933eaab9af8b9b2c930236f62aae3fc593faf30" integrity sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA== +"@esbuild/sunos-x64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz#8ddc35a0ea38575fa44eda30a5ee01ae2fa54dd4" + integrity sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA== + "@esbuild/win32-arm64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.16.17.tgz#7b0ff9e8c3265537a7a7b1fd9a24e7bd39fcd87a" @@ -2318,6 +2339,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz#773bdbaa1971b36db2f6560088639ccd1e6773ae" integrity sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A== +"@esbuild/win32-arm64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz#6e79c8543f282c4539db684a207ae0e174a9007b" + integrity sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA== + "@esbuild/win32-ia32@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.16.17.tgz#e90fe5267d71a7b7567afdc403dfd198c292eb09" @@ -2338,6 +2364,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz#000516cad06354cc84a73f0943a4aa690ef6fd67" integrity sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ== +"@esbuild/win32-ia32@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz#057af345da256b7192d18b676a02e95d0fa39103" + integrity sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw== + "@esbuild/win32-x64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.16.17.tgz#c5a1a4bfe1b57f0c3e61b29883525c6da3e5c091" @@ -2358,6 +2389,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz#c57c8afbb4054a3ab8317591a0b7320360b444ae" integrity sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA== +"@esbuild/win32-x64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz#168ab1c7e1c318b922637fad8f339d48b01e1244" + integrity sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA== + "@eslint/eslintrc@^0.4.3": version "0.4.3" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" @@ -11567,6 +11603,36 @@ esbuild-windows-arm64@0.14.54: resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz#937d15675a15e4b0e4fafdbaa3a01a776a2be982" integrity sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg== +esbuild@0.24.0, esbuild@^0.24.0: + version "0.24.0" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.24.0.tgz#f2d470596885fcb2e91c21eb3da3b3c89c0b55e7" + integrity sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ== + optionalDependencies: + "@esbuild/aix-ppc64" "0.24.0" + "@esbuild/android-arm" "0.24.0" + "@esbuild/android-arm64" "0.24.0" + "@esbuild/android-x64" "0.24.0" + "@esbuild/darwin-arm64" "0.24.0" + "@esbuild/darwin-x64" "0.24.0" + "@esbuild/freebsd-arm64" "0.24.0" + "@esbuild/freebsd-x64" "0.24.0" + "@esbuild/linux-arm" "0.24.0" + "@esbuild/linux-arm64" "0.24.0" + "@esbuild/linux-ia32" "0.24.0" + "@esbuild/linux-loong64" "0.24.0" + "@esbuild/linux-mips64el" "0.24.0" + "@esbuild/linux-ppc64" "0.24.0" + "@esbuild/linux-riscv64" "0.24.0" + "@esbuild/linux-s390x" "0.24.0" + "@esbuild/linux-x64" "0.24.0" + "@esbuild/netbsd-x64" "0.24.0" + "@esbuild/openbsd-arm64" "0.24.0" + "@esbuild/openbsd-x64" "0.24.0" + "@esbuild/sunos-x64" "0.24.0" + "@esbuild/win32-arm64" "0.24.0" + "@esbuild/win32-ia32" "0.24.0" + "@esbuild/win32-x64" "0.24.0" + esbuild@^0.14.47: version "0.14.54" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.54.tgz#8b44dcf2b0f1a66fc22459943dccf477535e9aa2" @@ -11915,11 +11981,6 @@ eslint-plugin-react@^7.20.6, eslint-plugin-react@^7.24.0: string.prototype.matchall "^4.0.11" string.prototype.repeat "^1.0.0" -eslint-plugin-semi-design@^2.33.0: - version "2.69.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-semi-design/-/eslint-plugin-semi-design-2.69.2.tgz#971d43053d9201a816881a124f149c8abb0181e8" - integrity sha512-veVWSt17xITeAk8ztbc2EihiTfkMQk8P0U60he83tjRf5Qm4W6AAtecFOnZE9lauz4+D5FaDFjGkSfrOMuEwww== - eslint-rule-composer@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz#79320c927b0c5c0d3d3d2b76c8b4a488f25bbaf9" @@ -17095,6 +17156,11 @@ json5@^2.1.2, json5@^2.1.3, json5@^2.2.0, json5@^2.2.3: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== +jsonc-parser@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.3.1.tgz#f2a524b4f7fd11e3d791e559977ad60b98b798b4" + integrity sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ== + jsonfile@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" @@ -27906,6 +27972,14 @@ worker-farm@^1.7.0: dependencies: errno "~0.1.7" +worker-loader@^3.0.8: + version "3.0.8" + resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-3.0.8.tgz#5fc5cda4a3d3163d9c274a4e3a811ce8b60dbb37" + integrity sha512-XQyQkIFeRVC7f7uRhFdNMe/iJOdO6zxAaR3EWbDp45v3mDhrTi+++oswKNxShUNjPC/1xUp5DB29YKLhFo129g== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"