/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 (
+ <>
+
+ setAutoWrap(!autoWrap)}>
+ {autoWrap ? 'Disable' : 'Enable'} Auto Wrap
+
+ setLineHeight(lineHeight + 5)}>
+ Increase Line Height
+
+ console.log(jsonviewerRef.current.jsonViewer.current.getValue())}>
+ Get Value
+
+ >
+ );
+};
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()}
+
+ }
+ onClick={e => {
+ e.preventDefault();
+ this.foundation.prevSearch();
+ }}
+ />
+ }
+ onClick={e => {
+ e.preventDefault();
+ this.foundation.nextSearch();
+ }}
+ />
+
+ }
+ size="small"
+ theme={'borderless'}
+ type={'tertiary'}
+ onClick={() => this.foundation.showSearchBar()}
+ />
+
+ );
+ }
+
+ renderReplaceBar() {
+ return (
+
+ {
+ e.preventDefault();
+ }}
+ ref={this.replaceInputRef}
+ />
+ {
+ const value = this.replaceInputRef.current?.value;
+ this.foundation.replace(value);
+ }}
+ >
+ 替换
+
+ {
+ const value = this.replaceInputRef.current?.value;
+ this.foundation.replaceAll(value);
+ }}
+ >
+ 全部替换
+
+
+ );
+ }
+
+ render() {
+ let isDragging = false;
+ const { width, className, style, ...rest } = this.props;
+ return (
+ <>
+
+
+
{
+ isDragging = false;
+ }}
+ onMouseMove={() => {
+ isDragging = true;
+ }}
+ >
+
+ {!this.state.showSearchBar ? (
+ {
+ e.preventDefault();
+ if (isDragging) {
+ e.stopPropagation();
+ e.preventDefault();
+ return;
+ }
+ this.foundation.showSearchBar();
+ }}
+ icon={ }
+ />
+ ) : (
+ this.renderSearchBox()
+ )}
+
+
+
+ >
+ );
+ }
+}
+
+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"