diff --git a/package.json b/package.json index 08757eff..fc4a88ba 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "pre-commit": "npx pretty-quick --staged" }, "dependencies": { - "dt-sql-parser": "4.0.2" + "dt-sql-parser": "4.1.0-beta.4" }, "peerDependencies": { "monaco-editor": ">=0.31.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d53512fd..1c3b1e83 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: dt-sql-parser: - specifier: 4.0.2 - version: 4.0.2(antlr4ng-cli@1.0.7) + specifier: 4.1.0-beta.4 + version: 4.1.0-beta.4(antlr4ng-cli@1.0.7) devDependencies: '@commitlint/cli': specifier: ^17.7.2 @@ -714,8 +714,9 @@ packages: resolution: {integrity: sha512-sCm11ak2oY6DglEPpCB8TixLjWAxd3kJTs6UIcSasNYxXdFPV+YKlye92c8H4kKFqV5qYMIh7d+cYecEg0dIkA==} engines: {node: '>=6'} - dt-sql-parser@4.0.2: - resolution: {integrity: sha512-8D/kfYLW+wgz7Cwf5K+OCtex7QHiCyIuI18pw0a5vjSXRKCpfQqNQeG7tU5vp4D0RQEZJiMBuKJPBYwoqWxoAA==} + dt-sql-parser@4.1.0-beta.4: + resolution: {integrity: sha512-L+Qsw+lv7enkMuhy0XXOm7H63gaajwX7X0RUGCNU8h5xw9Pj5DEWvLcKTS0R+YmO4FzVXOpEzH9e1KkqQaKFaQ==} + engines: {node: '>=18'} email-addresses@3.1.0: resolution: {integrity: sha512-k0/r7GrWVL32kZlGwfPNgB2Y/mMXVTq/decgLczm/j34whdaspNrZO8CnXPf1laaHxI6ptUlsnAxN+UAPw+fzg==} @@ -2060,7 +2061,7 @@ snapshots: '@types/node': 20.5.1 chalk: 4.1.2 cosmiconfig: 8.3.6(typescript@5.5.4) - cosmiconfig-typescript-loader: 4.4.0(@types/node@20.5.1)(cosmiconfig@8.3.6(typescript@5.5.4))(ts-node@10.9.2(@types/node@20.14.14)(typescript@5.5.4))(typescript@5.5.4) + cosmiconfig-typescript-loader: 4.4.0(@types/node@20.5.1)(cosmiconfig@8.3.6(typescript@5.5.4))(ts-node@10.9.2(@types/node@20.5.1)(typescript@5.5.4))(typescript@5.5.4) lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 lodash.uniq: 4.5.0 @@ -2576,7 +2577,7 @@ snapshots: core-util-is@1.0.3: {} - cosmiconfig-typescript-loader@4.4.0(@types/node@20.5.1)(cosmiconfig@8.3.6(typescript@5.5.4))(ts-node@10.9.2(@types/node@20.14.14)(typescript@5.5.4))(typescript@5.5.4): + cosmiconfig-typescript-loader@4.4.0(@types/node@20.5.1)(cosmiconfig@8.3.6(typescript@5.5.4))(ts-node@10.9.2(@types/node@20.5.1)(typescript@5.5.4))(typescript@5.5.4): dependencies: '@types/node': 20.5.1 cosmiconfig: 8.3.6(typescript@5.5.4) @@ -2702,7 +2703,7 @@ snapshots: find-up: 3.0.0 minimatch: 3.1.2 - dt-sql-parser@4.0.2(antlr4ng-cli@1.0.7): + dt-sql-parser@4.1.0-beta.4(antlr4ng-cli@1.0.7): dependencies: antlr4-c3: 3.3.7(antlr4ng-cli@1.0.7) antlr4ng: 2.0.11(antlr4ng-cli@1.0.7) diff --git a/src/baseSQLWorker.ts b/src/baseSQLWorker.ts index 3d3029cf..916ba4df 100644 --- a/src/baseSQLWorker.ts +++ b/src/baseSQLWorker.ts @@ -2,6 +2,7 @@ import { BasicSQL } from 'dt-sql-parser/dist/parser/common/basicSQL'; import { worker } from './fillers/monaco-editor-core'; import { Suggestions, ParseError, EntityContext } from 'dt-sql-parser'; import { Position } from './fillers/monaco-editor-core'; +import type { SerializedTreeNode } from './languageService'; export interface ICreateData { languageId: string; @@ -23,16 +24,6 @@ export abstract class BaseSQLWorker { return Promise.resolve([]); } - async parserTreeToString(code: string): Promise { - if (code) { - const parser = this.parser.createParser(code); - const parseTree = parser.program(); - const result = parseTree.toStringTree(parser); - return Promise.resolve(result); - } - return Promise.resolve(''); - } - async doCompletion(code: string, position: Position): Promise { code = code || this.getTextDocument(); if (code) { @@ -67,6 +58,43 @@ export abstract class BaseSQLWorker { return Promise.resolve(null); } + async getSerializedParseTree(code: string): Promise { + if (!code) return Promise.resolve(null); + + const parser = this.parser.createParser(code); + const parseTree = parser.program(); + const ruleNames = parser.ruleNames; + const symbolicNames: string[] = (parser as any).symbolicNames || []; + + // 只保留必要信息, 避免worker通信传输失败 + function serializeNode(node: any): SerializedTreeNode | null { + if (!node) return null; + + const isRuleNode = !node.symbol; + const text = isRuleNode + ? '' + : (symbolicNames[node.symbol?.type] ? symbolicNames[node.symbol.type] + ': ' : '') + + node.symbol?.text; + + const serializedNode: SerializedTreeNode = { + ruleName: isRuleNode ? ruleNames[node.ruleIndex] : node.constructor.name, + text, + children: [] + }; + + for (let i = 0; i < node.getChildCount(); i++) { + const child = node.getChild(i); + if (child) { + serializedNode.children.push(serializeNode(child)!); + } + } + + return serializedNode; + } + + return Promise.resolve(serializeNode(parseTree)); + } + private getTextDocument(): string { const model = this._ctx.getMirrorModels()[0]; // When there are multiple files open, this will be an array return model && model.getValue(); diff --git a/src/languageService.ts b/src/languageService.ts index d5ac2233..20342e3e 100644 --- a/src/languageService.ts +++ b/src/languageService.ts @@ -7,6 +7,12 @@ import { WorkerManager } from './workerManager'; import { BaseSQLWorker } from './baseSQLWorker'; import { Position, Uri, editor } from './fillers/monaco-editor-core'; +export interface SerializedTreeNode { + ruleName: string; + text?: string; + children: SerializedTreeNode[]; +} + export class LanguageService { private workerClients: Map> = new Map(); @@ -20,13 +26,13 @@ export class LanguageService { }); } - public parserTreeToString(language: string, model: editor.IReadOnlyModel | string) { + public getSerializedParseTree(language: string, model: editor.IReadOnlyModel | string) { const text = typeof model === 'string' ? model : model.getValue(); const uri = typeof model === 'string' ? void 0 : model.uri; const clientWorker = this.getClientWorker(language, uri as Uri); return clientWorker.then((worker) => { - return worker.parserTreeToString(text); + return worker.getSerializedParseTree(text); }); } diff --git a/src/main.ts b/src/main.ts index f743ce30..45036c2a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -13,6 +13,9 @@ export type { Suggestions, TextSlice, ParseError, + StmtContext, EntityContext, - StmtContext + CommonEntityContext, + ColumnEntityContext, + FuncEntityContext } from 'dt-sql-parser'; diff --git a/website/package.json b/website/package.json index f13eba7b..42215932 100644 --- a/website/package.json +++ b/website/package.json @@ -12,6 +12,9 @@ "dependencies": { "@dtinsight/molecule": "^1.3.4", "@jcubic/lips": "^0.20.3", + "@types/dagre": "^0.7.52", + "@xyflow/react": "^12.4.2", + "dagre": "^0.8.5", "monaco-editor": "0.31.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/website/pnpm-lock.yaml b/website/pnpm-lock.yaml index c1dde192..ead157c6 100644 --- a/website/pnpm-lock.yaml +++ b/website/pnpm-lock.yaml @@ -14,6 +14,15 @@ importers: '@jcubic/lips': specifier: ^0.20.3 version: 0.20.3 + '@types/dagre': + specifier: ^0.7.52 + version: 0.7.52 + '@xyflow/react': + specifier: ^12.4.2 + version: 12.4.2(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + dagre: + specifier: ^0.8.5 + version: 0.8.5 monaco-editor: specifier: 0.31.0 version: 0.31.0 @@ -389,6 +398,27 @@ packages: '@types/babel__traverse@7.20.6': resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + + '@types/d3-transition@3.0.9': + resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} + + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + + '@types/dagre@0.7.52': + resolution: {integrity: sha512-XKJdy+OClLk3hketHi9Qg6gTfe1F3y+UFnHxKA2rn9Dw+oXa4Gb378Ztz9HlMgZKSxpPmn4BNVh9wgkpvrK1uw==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -483,6 +513,15 @@ packages: '@vscode/codicons@0.0.26': resolution: {integrity: sha512-GrYFJPbZ+hRM3NUVdAIpDepWkYCizVb13a6pJDAhckElDvaf4UCmNpuBS4MSydXNK63Ccts0XpvJ6JOW+/aU1g==} + '@xyflow/react@12.4.2': + resolution: {integrity: sha512-AFJKVc/fCPtgSOnRst3xdYJwiEcUN9lDY7EO/YiRvFHYCJGgfzg+jpvZjkTOnBLGyrMJre9378pRxAc3fsR06A==} + peerDependencies: + react: '>=17' + react-dom: '>=17' + + '@xyflow/system@0.0.50': + resolution: {integrity: sha512-HVUZd4LlY88XAaldFh2nwVxDOcdIBxGpQ5txzwfJPf+CAjj2BfYug1fHs2p4yS7YO8H6A3EFJQovBE8YuHkAdg==} + JSONStream@1.3.5: resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} hasBin: true @@ -573,6 +612,9 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + classcat@5.0.5: + resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==} + classnames@2.5.1: resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} @@ -692,6 +734,47 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + dagre@0.8.5: + resolution: {integrity: sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==} + dargs@7.0.0: resolution: {integrity: sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==} engines: {node: '>=8'} @@ -952,6 +1035,9 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + graphlib@2.1.8: + resolution: {integrity: sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==} + handlebars@4.7.8: resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} engines: {node: '>=0.4.7'} @@ -1713,6 +1799,11 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + use-sync-external-store@1.4.0: + resolution: {integrity: sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -1795,6 +1886,21 @@ packages: zoom-level@2.5.0: resolution: {integrity: sha512-7UlRWU4Q3uCMCeDVMOm7eBrIu145OqsIJ3p6zq58l8UsSYwKWxc6zEapC5YA9tIeh0oheb4cT9Kk2Wq353loFg==} + zustand@4.5.6: + resolution: {integrity: sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + snapshots: '@ampproject/remapping@2.3.0': @@ -2122,6 +2228,29 @@ snapshots: dependencies: '@babel/types': 7.25.2 + '@types/d3-color@3.1.3': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-selection@3.0.11': {} + + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + + '@types/dagre@0.7.52': {} + '@types/json-schema@7.0.15': {} '@types/minimist@1.2.5': {} @@ -2244,6 +2373,27 @@ snapshots: '@vscode/codicons@0.0.26': {} + '@xyflow/react@12.4.2(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@xyflow/system': 0.0.50 + classcat: 5.0.5 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + zustand: 4.5.6(@types/react@18.3.3)(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - immer + + '@xyflow/system@0.0.50': + dependencies: + '@types/d3-drag': 3.0.7 + '@types/d3-selection': 3.0.11 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + d3-drag: 3.0.0 + d3-selection: 3.0.0 + d3-zoom: 3.0.0 + JSONStream@1.3.5: dependencies: jsonparse: 1.3.1 @@ -2327,6 +2477,8 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + classcat@5.0.5: {} + classnames@2.5.1: {} cliui@7.0.4: @@ -2489,6 +2641,47 @@ snapshots: csstype@3.1.3: {} + d3-color@3.1.0: {} + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-ease@3.0.1: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-selection@3.0.0: {} + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + dagre@0.8.5: + dependencies: + graphlib: 2.1.8 + lodash: 4.17.21 + dargs@7.0.0: {} dateformat@3.0.3: {} @@ -2791,6 +2984,10 @@ snapshots: graphemer@1.4.0: {} + graphlib@2.1.8: + dependencies: + lodash: 4.17.21 + handlebars@4.7.8: dependencies: minimist: 1.2.8 @@ -3500,6 +3697,10 @@ snapshots: dependencies: punycode: 2.3.1 + use-sync-external-store@1.4.0(react@18.3.1): + dependencies: + react: 18.3.1 + util-deprecate@1.0.2: {} validate-npm-package-license@3.0.4: @@ -3555,3 +3756,10 @@ snapshots: yocto-queue@0.1.0: {} zoom-level@2.5.0: {} + + zustand@4.5.6(@types/react@18.3.3)(react@18.3.1): + dependencies: + use-sync-external-store: 1.4.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + react: 18.3.1 diff --git a/website/src/extensions/workbench/index.tsx b/website/src/extensions/workbench/index.tsx index 833d20af..c0349384 100644 --- a/website/src/extensions/workbench/index.tsx +++ b/website/src/extensions/workbench/index.tsx @@ -4,6 +4,11 @@ import { IExtension } from '@dtinsight/molecule/esm/model/extension'; import Sidebar from './sidebar'; import { defaultEditorTab, defaultLanguageStatusItem } from './common'; +export const defaultParseTreePanel = { + id: 'ParseTreePanel', + name: 'Parse Tree Visualizer' +}; + export const ExtendsWorkbench: IExtension = { id: 'ExtendWorkbench', name: 'ExtendWorkbench', @@ -42,6 +47,8 @@ export const ExtendsWorkbench: IExtension = { selected: parserActivityBarItem.id }); + molecule.panel.add(defaultParseTreePanel); + molecule.activityBar.onClick((id) => { if (id === githubPageActivityBarItem.id) { window.location.href = 'https://github.com/DTStack/monaco-sql-languages'; diff --git a/website/src/extensions/workbench/sidebar.tsx b/website/src/extensions/workbench/sidebar.tsx index 8b39574e..403646cd 100644 --- a/website/src/extensions/workbench/sidebar.tsx +++ b/website/src/extensions/workbench/sidebar.tsx @@ -1,7 +1,6 @@ import React from 'react'; import * as monaco from 'monaco-editor'; -import lips from '@jcubic/lips'; import molecule from '@dtinsight/molecule'; import { Button } from '@dtinsight/molecule/esm/components'; import { Select, Option } from '@dtinsight/molecule/esm/components/select'; @@ -10,6 +9,8 @@ import { IEditorTab, IProblemsItem, MarkerSeverity } from '@dtinsight/molecule/e import { defaultLanguage, defaultEditorTab, defaultLanguageStatusItem, languages } from './common'; import { LanguageService, ParseError } from 'monaco-sql-languages/esm/languageService'; import { debounce } from './utils'; +import TreeVisualizerPanel from './treeVisualizerPanel'; +import { defaultParseTreePanel } from '.'; export default class Sidebar extends React.Component { private _language = defaultLanguage; @@ -20,9 +21,16 @@ export default class Sidebar extends React.Component { } componentDidMount() { - molecule.editor.onUpdateTab(this.analyseProblems); + molecule.editor.onUpdateTab((tab) => { + this.analyseProblems(tab); + this.updateParseTree(); + }); monaco.editor.setTheme('sql-dark'); + + setTimeout(() => { + this.updateParseTree(); + }, 500); } private get language(): string { @@ -112,31 +120,26 @@ export default class Sidebar extends React.Component { sortIndex: 3 }); this.analyseProblems(nextTab); + this.updateParseTree(); molecule.statusBar.update(nextStatusItem); } - parse = () => { - this.setupOutputLanguage(); + updateParseTree = debounce(() => { + if (!molecule.panel.getPanel(defaultParseTreePanel.id)) return; + const sql = molecule.editor.editorInstance.getValue(); - molecule.panel.cleanOutput(); - - this.languageService.parserTreeToString(this.language, sql).then((res) => { - const pre = res?.replace(/(\(|\))/g, '$1\n'); - const format = new lips.Formatter(pre); - const formatted = format.format({ - indent: 2, - offset: 2 - }); - const panel = - molecule.panel.getPanel(molecule.builtin.getConstants().PANEL_OUTPUT ?? '') ?? - ({} as any); + + this.languageService.getSerializedParseTree(this.language, sql).then((tree) => { molecule.panel.update({ - ...panel, - data: formatted + ...defaultParseTreePanel, + renderPane: () => ( +
+ {tree ? : null} +
+ ) }); - molecule.panel.appendOutput(''); }); - }; + }, 400); async setupOutputLanguage() { const model = await molecule.panel.outputEditorInstance?.getModel(); @@ -172,7 +175,18 @@ export default class Sidebar extends React.Component { Select a language:{' '} {this.renderColorThemes()} - + ); diff --git a/website/src/extensions/workbench/treeVisualizerPanel.tsx b/website/src/extensions/workbench/treeVisualizerPanel.tsx new file mode 100644 index 00000000..ac488098 --- /dev/null +++ b/website/src/extensions/workbench/treeVisualizerPanel.tsx @@ -0,0 +1,334 @@ +import { memo, useCallback, useEffect, useState, useTransition } from 'react'; +import { + ReactFlow, + Node, + Edge, + useNodesState, + useEdgesState, + Position, + ConnectionMode, + Background, + BackgroundVariant, + Controls, + ReactFlowProvider +} from '@xyflow/react'; +import dagre from 'dagre'; +import { SerializedTreeNode } from 'monaco-sql-languages/esm/languageService'; + +import '@xyflow/react/dist/style.css'; + +interface TreeVisualizerPanelProps { + parseTree: SerializedTreeNode; +} + +enum NodeDisplayType { + TerminalNode = 'TerminalNode', + ErrorNode = 'ErrorNode', + RuleNode = 'RuleNode' +} + +interface NodeData { + label: string; + displayType: NodeDisplayType; + [key: string]: string; +} + +interface NodeStyleProps { + displayType: NodeDisplayType; + label: string; + isSelected?: boolean; + isChild?: boolean; + hasSelection?: boolean; +} + +// 计算文本宽度的辅助函数 +const calculateTextWidth = (text: string): number => { + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + if (!context) return 120; + + context.font = '13px Monaco, monospace'; + const metrics = context.measureText(text); + // 添加内边距和一些缓冲空间 + return Math.max(120, Math.ceil(metrics.width + 40)); +}; + +// 自定义节点样式 +const getNodeStyle = ({ + displayType, + label, + isSelected = false, + isChild = false, + hasSelection = false +}: NodeStyleProps) => { + const width = calculateTextWidth(label); + + return { + padding: '8px 12px', + border: '2px solid #4a90e2', + borderRadius: '6px', + backgroundColor: + displayType === NodeDisplayType.TerminalNode + ? 'rgb(136 205 255)' + : displayType === NodeDisplayType.ErrorNode + ? 'rgb(255 205 210)' + : '#fff', + color: '#2c3e50', + fontSize: '13px', + width: width, + textAlign: 'center' as const, + transition: 'all 0.2s ease', + opacity: hasSelection && !isSelected && !isChild ? 0.3 : 1, + boxShadow: isSelected + ? '0 0 10px #4a90e2' + : isChild + ? '0 0 6px #4a90e2' + : '0 2px 6px rgba(0,0,0,0.1)' + }; +}; + +const edgeStyle = { + stroke: '#4a90e2', + strokeWidth: 2 +}; + +// 设置布局方向为从上到下 +const getLayoutedElements = >( + nodes: Node[], + edges: Edge[], + direction = 'TB' +) => { + // 每次都创建新的 dagre 图实例 + const dagreGraph = new dagre.graphlib.Graph(); + dagreGraph.setDefaultEdgeLabel(() => ({})); + + // 设置布局参数 + dagreGraph.setGraph({ + rankdir: direction, + nodesep: 50, // 同一行节点之间的间距 + ranksep: 50, // 不同行之间的间距 + edgesep: 10, // 边之间的间距 + marginx: 20, // 水平边距 + marginy: 20, // 垂直边距 + acyclicer: 'greedy', // 处理循环的算法 + ranker: 'network-simplex' // 布局算法 + }); + + nodes.forEach((node) => { + const label = node.data.label as string; + const width = calculateTextWidth(label); + dagreGraph.setNode(node.id, { width, height: 40 }); + }); + + edges.forEach((edge) => { + dagreGraph.setEdge(edge.source, edge.target); + }); + + // 计算布局 + dagre.layout(dagreGraph); + + // 应用计算后的位置 + const layoutedNodes = nodes.map((node) => { + const nodeWithPosition = dagreGraph.node(node.id); + return { + ...node, + position: { + x: nodeWithPosition.x - nodeWithPosition.width / 2, + y: nodeWithPosition.y - nodeWithPosition.height / 2 + } + }; + }); + + return { nodes: layoutedNodes, edges }; +}; + +const TreeVisualizerContent = ({ parseTree }: TreeVisualizerPanelProps) => { + const [nodes, setNodes, onNodesChange] = useNodesState>([]); + const [edges, setEdges, onEdgesChange] = useEdgesState([]); + const [selectedNodeId, setSelectedNodeId] = useState(null); + const [_, startTransition] = useTransition(); + + // 获取节点的所有子节点ID + const getChildNodeIds = (nodeId: string | null): string[] => { + if (!nodeId) return []; + const childIds: string[] = []; + const queue = [nodeId]; + + while (queue.length > 0) { + const currentId = queue.shift()!; + edges.forEach((edge) => { + if (edge.source === currentId) { + childIds.push(edge.target); + queue.push(edge.target); + } + }); + } + + return childIds; + }; + + const handleNodeClick = (_event: React.MouseEvent, node: Node) => { + setSelectedNodeId(node.id); + }; + + const handlePaneClick = () => { + setSelectedNodeId(null); + }; + + const convertTreeToElements = useCallback((tree: SerializedTreeNode) => { + const newNodes: Node[] = []; + const newEdges: Edge[] = []; + let nodeId = 0; + let rootNodeId: string | null = null; + + const processNode = (node: SerializedTreeNode, parentId?: string): string => { + const currentId = `node-${nodeId++}`; + + if (rootNodeId === null) { + rootNodeId = currentId; + } + + const nodeDisplayType = [ + NodeDisplayType.TerminalNode, + NodeDisplayType.ErrorNode + ]?.includes(node.ruleName as any) + ? node.ruleName + : NodeDisplayType.RuleNode; + + const label = node.text ? node.text : node.ruleName; + + newNodes.push({ + id: currentId, + type: 'default', + data: { + label, + displayType: nodeDisplayType as NodeDisplayType + }, + position: { x: 0, y: 0 }, + style: getNodeStyle({ + displayType: nodeDisplayType as NodeDisplayType, + label, + isSelected: false, + isChild: false, + hasSelection: false + }), + sourcePosition: Position.Bottom, + targetPosition: Position.Top, + draggable: false + }); + + if (parentId) { + newEdges.push({ + id: `edge-${parentId}-${currentId}`, + source: parentId, + target: currentId, + type: 'smoothstep', + animated: true, + style: edgeStyle + }); + } + + node.children?.forEach((child) => { + processNode(child, currentId); + }); + + return currentId; + }; + + processNode(tree); + return { nodes: newNodes, edges: newEdges, rootNodeId }; + }, []); + + useEffect(() => { + if (!parseTree) { + setEdges([]); + setNodes([]); + return; + } + + const elements = convertTreeToElements(parseTree); + const layoutedElements = getLayoutedElements(elements.nodes, elements.edges); + + startTransition(() => { + setNodes(layoutedElements.nodes); + setEdges(layoutedElements.edges); + setSelectedNodeId(null); + }); + }, [parseTree]); + + useEffect(() => { + const childIds = getChildNodeIds(selectedNodeId); + + setNodes((nodes) => + nodes.map((node) => ({ + ...node, + style: getNodeStyle({ + displayType: node.data.displayType, + label: node.data.label, + isSelected: node.id === selectedNodeId, + isChild: childIds.includes(node.id), + hasSelection: selectedNodeId !== null // 只有当有选中节点时才降低其他节点亮度 + }) + })) + ); + }, [selectedNodeId]); + + return ( +
+ + + + +
+ ); +}; + +const TreeVisualizerPanel = memo((props: TreeVisualizerPanelProps) => { + return ( + + + + ); +}); + +export default TreeVisualizerPanel;