diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3941872..4aafb22 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -42,6 +42,8 @@ "@lezer/lr": "^1.4.2", "@prettier/plugin-xml": "^3.4.2", "@reteps/dockerfmt": "^0.3.6", + "@toml-tools/lexer": "^1.0.0", + "@toml-tools/parser": "^1.0.0", "codemirror": "^6.0.2", "codemirror-lang-elixir": "^4.0.0", "colors-named": "^1.0.2", @@ -52,14 +54,12 @@ "java-parser": "^3.0.1", "jinx-rust": "^0.1.6", "jsox": "^1.2.123", - "lezer": "^0.13.5", "linguist-languages": "^9.0.0", "node-sql-parser": "^5.3.12", "php-parser": "^3.2.5", "pinia": "^3.0.3", "pinia-plugin-persistedstate": "^4.5.0", "prettier": "^3.6.2", - "prettier-plugin-toml": "^2.0.6", "remarkable": "^2.0.1", "sass": "^1.92.1", "sh-syntax": "^0.5.8", @@ -2236,19 +2236,23 @@ "win32" ] }, - "node_modules/@taplo/core": { - "version": "0.2.0", - "resolved": "https://registry.npmmirror.com/@taplo/core/-/core-0.2.0.tgz", - "integrity": "sha512-r8bl54Zj1In3QLkiW/ex694bVzpPJ9EhwqT9xkcUVODnVUGirdB1JTsmiIv0o1uwqZiwhi8xNnTOQBRQCpizrQ==", - "license": "MIT" - }, - "node_modules/@taplo/lib": { - "version": "0.5.0", - "resolved": "https://registry.npmmirror.com/@taplo/lib/-/lib-0.5.0.tgz", - "integrity": "sha512-+xIqpQXJco3T+VGaTTwmhxLa51qpkQxCjRwezjFZgr+l21ExlywJFcDfTrNmL6lG6tqb0h8GyJKO3UPGPtSCWg==", + "node_modules/@toml-tools/lexer": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/@toml-tools/lexer/-/lexer-1.0.0.tgz", + "integrity": "sha512-rVoOC9FibF2CICwCBWQnYcjAEOmLCJExer178K2AsY0Nk9FjJNVoVJuR5UAtuq42BZOajvH+ainf6Gj2GpCnXQ==", "license": "MIT", "dependencies": { - "@taplo/core": "^0.2.0" + "chevrotain": "^11.0.1" + } + }, + "node_modules/@toml-tools/parser": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/@toml-tools/parser/-/parser-1.0.0.tgz", + "integrity": "sha512-j8cd3A3ccLHppGoWI69urbiVJslrpwI6sZ61ySDUPxM/FTkQWRx/JkkF8aipnl0Ds0feWXyjyvmWzn70mIohYg==", + "license": "MIT", + "dependencies": { + "@toml-tools/lexer": "^1.0.0", + "chevrotain": "^11.0.1" } }, "node_modules/@types/estree": { @@ -5127,16 +5131,6 @@ "node": ">= 0.8.0" } }, - "node_modules/lezer": { - "version": "0.13.5", - "resolved": "https://registry.npmmirror.com/lezer/-/lezer-0.13.5.tgz", - "integrity": "sha512-cAiMQZGUo2BD8mpcz7Nv1TlKzWP7YIdIRrX41CiP5bk5t4GHxskOxWUx2iAOuHlz8dO+ivbuXr0J1bfHsWD+lQ==", - "deprecated": "This package has been replaced by @lezer/lr", - "license": "MIT", - "dependencies": { - "lezer-tree": "^0.13.2" - } - }, "node_modules/lezer-elixir": { "version": "1.1.2", "resolved": "https://registry.npmmirror.com/lezer-elixir/-/lezer-elixir-1.1.2.tgz", @@ -5146,13 +5140,6 @@ "@lezer/lr": "^1.3.0" } }, - "node_modules/lezer-tree": { - "version": "0.13.2", - "resolved": "https://registry.npmmirror.com/lezer-tree/-/lezer-tree-0.13.2.tgz", - "integrity": "sha512-15ZxW8TxVNAOkHIo43Iouv4zbSkQQ5chQHBpwXcD2bBFz46RB4jYLEEww5l1V0xyIx9U2clSyyrLes+hAUFrGQ==", - "deprecated": "This package has been replaced by @lezer/common", - "license": "MIT" - }, "node_modules/linguist-languages": { "version": "9.0.0", "resolved": "https://registry.npmmirror.com/linguist-languages/-/linguist-languages-9.0.0.tgz", @@ -6004,24 +5991,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/prettier-plugin-toml": { - "version": "2.0.6", - "resolved": "https://registry.npmmirror.com/prettier-plugin-toml/-/prettier-plugin-toml-2.0.6.tgz", - "integrity": "sha512-12N/wBuHa9jd/KVy9pRP20NMKxQfQLMseQCt66lIbLaPLItvGUcSIryE1eZZMJ7loSws6Ig3M2Elc2EreNh76w==", - "license": "MIT", - "dependencies": { - "@taplo/lib": "^0.5.0" - }, - "engines": { - "node": ">=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/unts" - }, - "peerDependencies": { - "prettier": "^3.0.3" - } - }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmmirror.com/process/-/process-0.11.10.tgz", diff --git a/frontend/package.json b/frontend/package.json index 7ca1cac..ac9cd5e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -46,6 +46,8 @@ "@lezer/lr": "^1.4.2", "@prettier/plugin-xml": "^3.4.2", "@reteps/dockerfmt": "^0.3.6", + "@toml-tools/lexer": "^1.0.0", + "@toml-tools/parser": "^1.0.0", "codemirror": "^6.0.2", "codemirror-lang-elixir": "^4.0.0", "colors-named": "^1.0.2", @@ -56,14 +58,12 @@ "java-parser": "^3.0.1", "jinx-rust": "^0.1.6", "jsox": "^1.2.123", - "lezer": "^0.13.5", "linguist-languages": "^9.0.0", "node-sql-parser": "^5.3.12", "php-parser": "^3.2.5", "pinia": "^3.0.3", "pinia-plugin-persistedstate": "^4.5.0", "prettier": "^3.6.2", - "prettier-plugin-toml": "^2.0.6", "remarkable": "^2.0.1", "sass": "^1.92.1", "sh-syntax": "^0.5.8", diff --git a/frontend/public/go.wasm b/frontend/public/go.wasm index 9d16000..10a967e 100644 Binary files a/frontend/public/go.wasm and b/frontend/public/go.wasm differ diff --git a/frontend/src/common/prettier/plugins/go/go.mjs b/frontend/src/common/prettier/plugins/go/go.mjs index 7bba50c..18ebd57 100644 --- a/frontend/src/common/prettier/plugins/go/go.mjs +++ b/frontend/src/common/prettier/plugins/go/go.mjs @@ -79,22 +79,32 @@ const initGoRuntimeNode = async () => { // Browser Go runtime initialization const initGoRuntimeBrowser = async () => { - if (globalThis.Go) return; - + // 总是重新初始化,因为可能存在版本不兼容问题 try { - // Load wasm_exec.js dynamically in browser + // 移除旧的 Go 运行时 + delete globalThis.Go; + + // 动态导入本地的 wasm_exec.js 内容 + const wasmExecResponse = await fetch('/wasm_exec.js'); + if (!wasmExecResponse.ok) { + throw new Error(`Failed to fetch wasm_exec.js: ${wasmExecResponse.status}`); + } + + const wasmExecCode = await wasmExecResponse.text(); + + // 在全局作用域中执行 wasm_exec.js 代码 const script = document.createElement('script'); - script.src = '/wasm_exec.js'; + script.textContent = wasmExecCode; document.head.appendChild(script); - - await new Promise((resolve, reject) => { - script.onload = resolve; - script.onerror = () => reject(new Error('Failed to load wasm_exec.js')); - }); + + // 等待一小段时间确保脚本执行完成 + await new Promise(resolve => setTimeout(resolve, 100)); if (!globalThis.Go) { - throw new Error('Go WASM runtime not available after loading wasm_exec.js'); + throw new Error('Go WASM runtime not available after executing wasm_exec.js'); } + + console.log('Go runtime initialized successfully'); } catch (error) { console.error('Browser Go runtime initialization failed:', error); throw error; @@ -108,37 +118,70 @@ const initialize = async () => { initializePromise = (async () => { let wasmBuffer; + console.log('Starting Go WASM initialization...'); + // Environment-specific initialization if (isNode()) { + console.log('Initializing for Node.js environment'); await initGoRuntimeNode(); wasmBuffer = await loadWasmNode(); } else if (isBrowser()) { + console.log('Initializing for Browser environment'); await initGoRuntimeBrowser(); wasmBuffer = await loadWasmBrowser(); } else { throw new Error('Unsupported environment: neither Node.js nor Browser detected'); } + console.log('Creating Go instance...'); const go = new globalThis.Go(); - const { instance } = await WebAssembly.instantiate(wasmBuffer, go.importObject); - - // Run Go program (don't await as it's a long-running service) - go.run(instance).catch(err => { - console.error('Go WASM program exit error:', err); - }); + + // 详细检查 importObject + console.log('Go import object keys:', Object.keys(go.importObject)); + if (go.importObject.gojs) { + console.log('gojs import keys:', Object.keys(go.importObject.gojs)); + console.log('scheduleTimeoutEvent type:', typeof go.importObject.gojs['runtime.scheduleTimeoutEvent']); + } + + console.log('Instantiating WebAssembly module...'); + + try { + const { instance } = await WebAssembly.instantiate(wasmBuffer, go.importObject); + console.log('WebAssembly instantiation successful'); + + console.log('Running Go program...'); + // Run Go program (don't await as it's a long-running service) + go.run(instance).catch(err => { + console.error('Go WASM program exit error:', err); + }); + } catch (instantiateError) { + console.error('WebAssembly instantiation failed:', instantiateError); + console.error('Error details:', { + message: instantiateError.message, + name: instantiateError.name, + stack: instantiateError.stack + }); + throw instantiateError; + } // Wait for Go program to initialize and expose formatGo function + console.log('Waiting for formatGo function to be available...'); let retries = 0; - const maxRetries = 10; + const maxRetries = 20; // 增加重试次数 while (typeof globalThis.formatGo !== 'function' && retries < maxRetries) { - await new Promise(resolve => setTimeout(resolve, 100)); + await new Promise(resolve => setTimeout(resolve, 200)); // 增加等待时间 retries++; + if (retries % 5 === 0) { + console.log(`Waiting for formatGo function... (${retries}/${maxRetries})`); + } } if (typeof globalThis.formatGo !== 'function') { - throw new Error('Go WASM module not properly initialized - formatGo function not available'); + throw new Error('Go WASM module not properly initialized - formatGo function not available after 20 retries'); } + + console.log('Go WASM initialization completed successfully'); })(); return initializePromise; @@ -147,14 +190,14 @@ const initialize = async () => { export const languages = [ { name: "Go", - parsers: ["go"], + parsers: ["go-format"], extensions: [".go"], vscodeLanguageIds: ["go"], }, ]; export const parsers = { - go: { + "go-format": { parse: (text) => text, astFormat: "go-format", locStart: (node) => 0, @@ -164,23 +207,35 @@ export const parsers = { export const printers = { "go-format": { - print: async (path) => { - await initialize(); + print: (path) => { const text = path.getValue(); if (typeof globalThis.formatGo !== 'function') { - throw new Error('Go WASM module not properly initialized - formatGo function missing'); + // 如果 formatGo 函数不可用,尝试初始化 + initialize().then(() => { + // 初始化完成后,formatGo 应该可用 + }).catch(err => { + console.error('Go WASM initialization failed:', err); + }); + + // 如果还是不可用,返回原始文本 + return text; } try { return globalThis.formatGo(text); } catch (error) { - throw new Error(`Go formatting failed: ${error.message}`); + console.error('Go formatting failed:', error); + // 返回原始文本而不是抛出错误 + return text; } }, }, }; +// Export initialize function for manual initialization +export { initialize }; + // Default export for Prettier plugin compatibility export default { languages, diff --git a/frontend/src/common/prettier/plugins/go/go.wasm b/frontend/src/common/prettier/plugins/go/go.wasm index 9d16000..10a967e 100644 Binary files a/frontend/src/common/prettier/plugins/go/go.wasm and b/frontend/src/common/prettier/plugins/go/go.wasm differ diff --git a/frontend/src/common/prettier/plugins/powershell/ast.ts b/frontend/src/common/prettier/plugins/powershell/ast.ts new file mode 100644 index 0000000..177c16b --- /dev/null +++ b/frontend/src/common/prettier/plugins/powershell/ast.ts @@ -0,0 +1,391 @@ +/** + * PowerShell AST 节点定义 + * 定义抽象语法树的各种节点类型 + */ + +import { Token } from './lexer'; + +export interface ASTNode { + type: string; + start: number; + end: number; + line: number; + column: number; +} + +export interface ScriptBlockAst extends ASTNode { + type: 'ScriptBlock'; + statements: StatementAst[]; +} + +export interface StatementAst extends ASTNode { + type: string; +} + +export interface ExpressionAst extends ASTNode { + type: string; +} + +// 管道表达式 +export interface PipelineAst extends StatementAst { + type: 'Pipeline'; + elements: PipelineElementAst[]; +} + +export interface PipelineElementAst extends ASTNode { + type: 'PipelineElement'; + expression: ExpressionAst; +} + +// 命令表达式 +export interface CommandAst extends ExpressionAst { + type: 'Command'; + commandName: string; + parameters: ParameterAst[]; + arguments: ExpressionAst[]; +} + +export interface ParameterAst extends ASTNode { + type: 'Parameter'; + name: string; + value?: ExpressionAst; +} + +// 赋值表达式 +export interface AssignmentAst extends StatementAst { + type: 'Assignment'; + left: ExpressionAst; + operator: string; + right: ExpressionAst; +} + +// 变量表达式 +export interface VariableAst extends ExpressionAst { + type: 'Variable'; + name: string; +} + +// 字面量表达式 +export interface LiteralAst extends ExpressionAst { + type: 'Literal'; + value: any; + literalType: 'String' | 'Number' | 'Boolean' | 'Null'; +} + +// 数组表达式 +export interface ArrayAst extends ExpressionAst { + type: 'Array'; + elements: ExpressionAst[]; +} + +// 哈希表表达式 +export interface HashtableAst extends ExpressionAst { + type: 'Hashtable'; + entries: HashtableEntryAst[]; +} + +export interface HashtableEntryAst extends ASTNode { + type: 'HashtableEntry'; + key: ExpressionAst; + value: ExpressionAst; +} + +// 函数定义 +export interface FunctionDefinitionAst extends StatementAst { + type: 'FunctionDefinition'; + name: string; + parameters: ParameterAst[]; + body: ScriptBlockAst; +} + +// 控制流结构 +export interface IfStatementAst extends StatementAst { + type: 'IfStatement'; + condition: ExpressionAst; + ifBody: ScriptBlockAst; + elseIfClauses: ElseIfClauseAst[]; + elseBody?: ScriptBlockAst; +} + +export interface ElseIfClauseAst extends ASTNode { + type: 'ElseIfClause'; + condition: ExpressionAst; + body: ScriptBlockAst; +} + +export interface WhileStatementAst extends StatementAst { + type: 'WhileStatement'; + condition: ExpressionAst; + body: ScriptBlockAst; +} + +export interface ForStatementAst extends StatementAst { + type: 'ForStatement'; + initializer?: ExpressionAst; + condition?: ExpressionAst; + iterator?: ExpressionAst; + body: ScriptBlockAst; +} + +export interface ForEachStatementAst extends StatementAst { + type: 'ForEachStatement'; + variable: VariableAst; + iterable: ExpressionAst; + body: ScriptBlockAst; +} + +export interface SwitchStatementAst extends StatementAst { + type: 'SwitchStatement'; + value: ExpressionAst; + clauses: SwitchClauseAst[]; +} + +export interface SwitchClauseAst extends ASTNode { + type: 'SwitchClause'; + pattern: ExpressionAst; + body: ScriptBlockAst; +} + +export interface TryStatementAst extends StatementAst { + type: 'TryStatement'; + body: ScriptBlockAst; + catchClauses: CatchClauseAst[]; + finallyClause?: FinallyClauseAst; +} + +export interface CatchClauseAst extends ASTNode { + type: 'CatchClause'; + exceptionType?: string; + body: ScriptBlockAst; +} + +export interface FinallyClauseAst extends ASTNode { + type: 'FinallyClause'; + body: ScriptBlockAst; +} + +// 二元操作表达式 +export interface BinaryExpressionAst extends ExpressionAst { + type: 'BinaryExpression'; + left: ExpressionAst; + operator: string; + right: ExpressionAst; +} + +// 一元操作表达式 +export interface UnaryExpressionAst extends ExpressionAst { + type: 'UnaryExpression'; + operator: string; + operand: ExpressionAst; +} + +// 括号表达式 +export interface ParenthesizedExpressionAst extends ExpressionAst { + type: 'ParenthesizedExpression'; + expression: ExpressionAst; +} + +// 方法调用表达式 +export interface MethodCallAst extends ExpressionAst { + type: 'MethodCall'; + object: ExpressionAst; + methodName: string; + arguments: ExpressionAst[]; +} + +// 属性访问表达式 +export interface PropertyAccessAst extends ExpressionAst { + type: 'PropertyAccess'; + object: ExpressionAst; + propertyName: string; +} + +// 索引访问表达式 +export interface IndexAccessAst extends ExpressionAst { + type: 'IndexAccess'; + object: ExpressionAst; + index: ExpressionAst; +} + +// 注释节点 +export interface CommentAst extends ASTNode { + type: 'Comment'; + text: string; + isMultiline: boolean; +} + +// 空白节点 +export interface WhitespaceAst extends ASTNode { + type: 'Whitespace'; + text: string; +} + +// 工厂函数,用于创建AST节点 +export class ASTNodeFactory { + static createScriptBlock(statements: StatementAst[], start: number, end: number, line: number, column: number): ScriptBlockAst { + return { + type: 'ScriptBlock', + statements, + start, + end, + line, + column + }; + } + + static createPipeline(elements: PipelineElementAst[], start: number, end: number, line: number, column: number): PipelineAst { + return { + type: 'Pipeline', + elements, + start, + end, + line, + column + }; + } + + static createCommand(commandName: string, parameters: ParameterAst[], args: ExpressionAst[], start: number, end: number, line: number, column: number): CommandAst { + return { + type: 'Command', + commandName, + parameters, + arguments: args, + start, + end, + line, + column + }; + } + + static createAssignment(left: ExpressionAst, operator: string, right: ExpressionAst, start: number, end: number, line: number, column: number): AssignmentAst { + return { + type: 'Assignment', + left, + operator, + right, + start, + end, + line, + column + }; + } + + static createVariable(name: string, start: number, end: number, line: number, column: number): VariableAst { + return { + type: 'Variable', + name, + start, + end, + line, + column + }; + } + + static createLiteral(value: any, literalType: 'String' | 'Number' | 'Boolean' | 'Null', start: number, end: number, line: number, column: number): LiteralAst { + return { + type: 'Literal', + value, + literalType, + start, + end, + line, + column + }; + } + + static createBinaryExpression(left: ExpressionAst, operator: string, right: ExpressionAst, start: number, end: number, line: number, column: number): BinaryExpressionAst { + return { + type: 'BinaryExpression', + left, + operator, + right, + start, + end, + line, + column + }; + } + + static createIfStatement(condition: ExpressionAst, ifBody: ScriptBlockAst, elseIfClauses: ElseIfClauseAst[], elseBody: ScriptBlockAst | undefined, start: number, end: number, line: number, column: number): IfStatementAst { + return { + type: 'IfStatement', + condition, + ifBody, + elseIfClauses, + elseBody, + start, + end, + line, + column + }; + } + + static createFunctionDefinition(name: string, parameters: ParameterAst[], body: ScriptBlockAst, start: number, end: number, line: number, column: number): FunctionDefinitionAst { + return { + type: 'FunctionDefinition', + name, + parameters, + body, + start, + end, + line, + column + }; + } + + static createComment(text: string, isMultiline: boolean, start: number, end: number, line: number, column: number): CommentAst { + return { + type: 'Comment', + text, + isMultiline, + start, + end, + line, + column + }; + } +} + +// AST访问者模式接口 +export interface ASTVisitor { + visitScriptBlock(node: ScriptBlockAst): T; + visitPipeline(node: PipelineAst): T; + visitCommand(node: CommandAst): T; + visitAssignment(node: AssignmentAst): T; + visitVariable(node: VariableAst): T; + visitLiteral(node: LiteralAst): T; + visitBinaryExpression(node: BinaryExpressionAst): T; + visitIfStatement(node: IfStatementAst): T; + visitFunctionDefinition(node: FunctionDefinitionAst): T; + visitComment(node: CommentAst): T; +} + +// AST遍历工具类 +export class ASTTraverser { + static traverse(node: ASTNode, visitor: Partial>): T | undefined { + switch (node.type) { + case 'ScriptBlock': + return visitor.visitScriptBlock?.(node as ScriptBlockAst); + case 'Pipeline': + return visitor.visitPipeline?.(node as PipelineAst); + case 'Command': + return visitor.visitCommand?.(node as CommandAst); + case 'Assignment': + return visitor.visitAssignment?.(node as AssignmentAst); + case 'Variable': + return visitor.visitVariable?.(node as VariableAst); + case 'Literal': + return visitor.visitLiteral?.(node as LiteralAst); + case 'BinaryExpression': + return visitor.visitBinaryExpression?.(node as BinaryExpressionAst); + case 'IfStatement': + return visitor.visitIfStatement?.(node as IfStatementAst); + case 'FunctionDefinition': + return visitor.visitFunctionDefinition?.(node as FunctionDefinitionAst); + case 'Comment': + return visitor.visitComment?.(node as CommentAst); + default: + return undefined; + } + } +} diff --git a/frontend/src/common/prettier/plugins/powershell/code-generator.ts b/frontend/src/common/prettier/plugins/powershell/code-generator.ts new file mode 100644 index 0000000..ee80bf1 --- /dev/null +++ b/frontend/src/common/prettier/plugins/powershell/code-generator.ts @@ -0,0 +1,566 @@ +/** + * PowerShell 代码生成器 + * 遍历AST并根据格式化规则生成格式化的PowerShell代码 + */ + +import { + ASTNode, + ScriptBlockAst, + StatementAst, + ExpressionAst, + PipelineAst, + CommandAst, + AssignmentAst, + VariableAst, + LiteralAst, + BinaryExpressionAst, + IfStatementAst, + FunctionDefinitionAst, + ParameterAst, + CommentAst, + PipelineElementAst, + ElseIfClauseAst, + ASTTraverser +} from './ast'; +import { FormatterRules, FormatterOptions } from './formatter-rules'; + +export class PowerShellCodeGenerator { + private rules: FormatterRules; + private indentLevel: number = 0; + private output: string[] = []; + private currentLineLength: number = 0; + private needsNewline: boolean = false; + private lastWasComment: boolean = false; + + constructor(options: Partial = {}) { + this.rules = new FormatterRules(options); + } + + /** + * 生成格式化的PowerShell代码 + */ + public generate(ast: ScriptBlockAst, comments: CommentAst[] = []): string { + this.output = []; + this.indentLevel = 0; + this.currentLineLength = 0; + this.needsNewline = false; + this.lastWasComment = false; + + // 首先处理文档开头的注释 + this.generateLeadingComments(comments); + + // 生成主体代码 + this.generateScriptBlock(ast); + + // 处理文档末尾 + this.handleFinalNewline(); + + const result = this.output.join(''); + return this.postProcess(result); + } + + private generateScriptBlock(node: ScriptBlockAst): void { + for (let i = 0; i < node.statements.length; i++) { + const statement = node.statements[i]; + const nextStatement = i < node.statements.length - 1 ? node.statements[i + 1] : null; + + this.generateStatement(statement); + + // 在语句之间添加适当的空行 + if (nextStatement) { + this.addStatementSeparation(statement, nextStatement); + } + } + } + + private generateStatement(statement: StatementAst): void { + switch (statement.type) { + case 'Pipeline': + this.generatePipeline(statement as PipelineAst); + break; + case 'Assignment': + this.generateAssignment(statement as AssignmentAst); + break; + case 'IfStatement': + this.generateIfStatement(statement as IfStatementAst); + break; + case 'FunctionDefinition': + this.generateFunctionDefinition(statement as FunctionDefinitionAst); + break; + case 'RawText': + // 处理解析失败时的原始文本 + this.append((statement as any).value); + return; // 不需要添加额外的换行 + default: + this.append(`/* Unsupported statement type: ${statement.type} */`); + break; + } + + this.ensureNewline(); + } + + private generatePipeline(pipeline: PipelineAst): void { + if (!this.rules.formatPipelines) { + // 简单连接所有元素 + for (let i = 0; i < pipeline.elements.length; i++) { + if (i > 0) { + this.append(' | '); + } + this.generatePipelineElement(pipeline.elements[i]); + } + return; + } + + const style = this.rules.getPipelineStyle(pipeline.elements.length); + + if (style === 'multiline') { + this.generateMultilinePipeline(pipeline); + } else { + this.generateOnelinePipeline(pipeline); + } + } + + private generateOnelinePipeline(pipeline: PipelineAst): void { + for (let i = 0; i < pipeline.elements.length; i++) { + if (i > 0) { + this.append(' | '); + } + this.generatePipelineElement(pipeline.elements[i]); + } + } + + private generateMultilinePipeline(pipeline: PipelineAst): void { + for (let i = 0; i < pipeline.elements.length; i++) { + if (i > 0) { + this.appendLine(' |'); + this.appendIndent(); + } + this.generatePipelineElement(pipeline.elements[i]); + } + } + + private generatePipelineElement(element: PipelineElementAst): void { + this.generateExpression(element.expression); + } + + private generateExpression(expression: ExpressionAst): void { + switch (expression.type) { + case 'Command': + this.generateCommand(expression as CommandAst); + break; + case 'Variable': + this.generateVariable(expression as VariableAst); + break; + case 'Literal': + this.generateLiteral(expression as LiteralAst); + break; + case 'BinaryExpression': + this.generateBinaryExpression(expression as BinaryExpressionAst); + break; + case 'ParenthesizedExpression': + this.append('('); + this.generateExpression((expression as any).expression); + this.append(')'); + break; + case 'Array': + this.generateArray(expression as any); + break; + case 'Hashtable': + this.generateHashtable(expression as any); + break; + case 'ScriptBlockExpression': + this.generateScriptBlockExpression(expression as any); + break; + default: + this.append(`/* Unsupported expression type: ${expression.type} */`); + break; + } + } + + private generateCommand(command: CommandAst): void { + // 保持cmdlet名称的连字符,不进行破坏性的格式化 + let commandName = command.commandName; + + // 只有在明确指定要改变大小写时才进行格式化 + // 但绝对不能删除连字符 + if (this.rules.shouldFormatCommandCase()) { + commandName = this.rules.formatCommandCase(commandName); + } + + this.append(commandName); + + // 生成参数 + for (const param of command.parameters) { + this.append(' '); + this.generateParameter(param); + } + + // 生成位置参数 + for (const arg of command.arguments) { + this.append(' '); + this.generateExpression(arg); + } + } + + private generateParameter(parameter: ParameterAst): void { + const paramName = this.rules.formatParameterCase(parameter.name); + this.append(paramName); + + if (parameter.value) { + this.append(' '); + this.generateExpression(parameter.value); + } + } + + private generateVariable(variable: VariableAst): void { + const formattedName = this.rules.formatVariableCase(variable.name); + this.append(formattedName); + } + + private generateLiteral(literal: LiteralAst): void { + if (literal.literalType === 'String') { + const formattedString = this.rules.formatQuotes(literal.value as string); + this.append(formattedString); + } else { + this.append(String(literal.value)); + } + } + + private generateBinaryExpression(expression: BinaryExpressionAst): void { + this.generateExpression(expression.left); + + // 根据PowerShell官方规范,属性访问操作符绝对不能加空格 + if (expression.operator === '.' || + expression.operator === '::' || + expression.operator === '[' || + expression.operator === ']' || + expression.operator === '@{') { + // 属性访问是PowerShell面向对象的核心,必须保持紧凑 + this.append(expression.operator); + } else { + // 使用格式化规则处理其他操作符 + const formattedOperator = this.rules.formatOperatorSpacing(expression.operator); + this.append(formattedOperator); + } + + this.generateExpression(expression.right); + } + + private generateAssignment(assignment: AssignmentAst): void { + this.generateExpression(assignment.left); + + const formattedOperator = this.rules.formatOperatorSpacing(assignment.operator); + this.append(formattedOperator); + + this.generateExpression(assignment.right); + } + + private generateIfStatement(ifStmt: IfStatementAst): void { + // if 条件 + this.append('if '); + this.append(this.rules.formatParentheses('')); + this.append('('); + this.generateExpression(ifStmt.condition); + this.append(')'); + + // if 主体 + this.append(this.rules.getBraceStart()); + this.appendLine(''); + this.indent(); + this.generateScriptBlock(ifStmt.ifBody); + this.outdent(); + this.appendIndent(); + this.append('}'); + + // elseif 子句 + for (const elseIfClause of ifStmt.elseIfClauses) { + this.generateElseIfClause(elseIfClause); + } + + // else 子句 + if (ifStmt.elseBody) { + this.append(' else'); + this.append(this.rules.getBraceStart()); + this.appendLine(''); + this.indent(); + this.generateScriptBlock(ifStmt.elseBody); + this.outdent(); + this.appendIndent(); + this.append('}'); + } + } + + private generateElseIfClause(elseIf: ElseIfClauseAst): void { + this.append(' elseif ('); + this.generateExpression(elseIf.condition); + this.append(')'); + this.append(this.rules.getBraceStart()); + this.appendLine(''); + this.indent(); + this.generateScriptBlock(elseIf.body); + this.outdent(); + this.appendIndent(); + this.append('}'); + } + + private generateFunctionDefinition(func: FunctionDefinitionAst): void { + // 函数前的空行 + if (this.rules.blankLinesAroundFunctions > 0) { + for (let i = 0; i < this.rules.blankLinesAroundFunctions; i++) { + this.appendLine(''); + } + } + + this.append('function '); + this.append(func.name); + + // 参数列表 + if (func.parameters.length > 0) { + this.append('('); + for (let i = 0; i < func.parameters.length; i++) { + if (i > 0) { + this.append(this.rules.formatComma()); + } + this.generateParameter(func.parameters[i]); + } + this.append(')'); + } + + // 函数体 + this.append(this.rules.getBraceStart()); + this.appendLine(''); + this.indent(); + this.generateScriptBlock(func.body); + this.outdent(); + this.appendIndent(); + this.append('}'); + + // 函数后的空行 + if (this.rules.blankLinesAroundFunctions > 0) { + for (let i = 0; i < this.rules.blankLinesAroundFunctions; i++) { + this.appendLine(''); + } + } + } + + private generateLeadingComments(comments: CommentAst[]): void { + const leadingComments = comments.filter(c => this.isLeadingComment(c)); + for (const comment of leadingComments) { + this.generateComment(comment); + this.appendLine(''); + } + } + + private generateComment(comment: CommentAst): void { + if (!this.rules.formatComments) { + this.append(comment.text); + return; + } + + if (comment.isMultiline) { + this.generateMultilineComment(comment.text); + } else { + this.generateSingleLineComment(comment.text); + } + + this.lastWasComment = true; + } + + private generateArray(arrayExpr: any): void { + this.append('@('); + if (arrayExpr.elements && arrayExpr.elements.length > 0) { + for (let i = 0; i < arrayExpr.elements.length; i++) { + if (i > 0) { + this.append(this.rules.formatComma()); + } + this.generateExpression(arrayExpr.elements[i]); + } + } + this.append(')'); + } + + private generateHashtable(hashtableExpr: any): void { + this.append('@{'); + + if (hashtableExpr.entries && hashtableExpr.entries.length > 0) { + // 强制使用紧凑格式,避免换行问题 + for (let i = 0; i < hashtableExpr.entries.length; i++) { + const entry = hashtableExpr.entries[i]; + + this.generateExpression(entry.key); + this.append('='); + this.generateExpression(entry.value); + + // 如果不是最后一个条目,添加分号和空格 + if (i < hashtableExpr.entries.length - 1) { + this.append('; '); + } + } + } + + this.append('}'); + } + + private generateScriptBlockExpression(scriptBlockExpr: any): void { + this.append('{'); + + // 对原始内容应用基本的格式化规则 + if (scriptBlockExpr.rawContent) { + const formattedContent = this.formatScriptBlockContent(scriptBlockExpr.rawContent); + this.append(formattedContent); + } else if (scriptBlockExpr.expression) { + // 兼容旧格式 + this.generateExpression(scriptBlockExpr.expression); + } + + this.append('}'); + } + + private formatScriptBlockContent(content: string): string { + if (!content || !content.trim()) { + return content; + } + + // 应用PowerShell官方规范的格式化规则 + let formatted = content.trim(); + + // 1. 保护所有属性访问操作符 - 这是最关键的 + // 匹配所有形式的属性访问:$var.Property, $_.Property, $obj.Method.Property等 + formatted = formatted.replace(/(\$[a-zA-Z_][a-zA-Z0-9_]*|\$_)\s*\.\s*([a-zA-Z_][a-zA-Z0-9_]*)/g, '$1.$2'); + + // 2. 保护方法调用中的点号 + formatted = formatted.replace(/(\w)\s*\.\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g, '$1.$2('); + + // 3. 确保数字单位不被分离 + formatted = formatted.replace(/(\d+)\s*(KB|MB|GB|TB|PB)/gi, '$1$2'); + + // 4. PowerShell比较和逻辑操作符需要前后空格 + const powershellOps = [ + '-eq', '-ne', '-lt', '-le', '-gt', '-ge', + '-like', '-notlike', '-match', '-notmatch', + '-contains', '-notcontains', '-in', '-notin', + '-is', '-isnot', '-as', '-and', '-or', '-not', '-xor' + ]; + + for (const op of powershellOps) { + const regex = new RegExp(`\\s*${op.replace('-', '\\-')}\\s*`, 'gi'); + formatted = formatted.replace(regex, ` ${op} `); + } + + // 5. 清理多余空格,但保护属性访问 + formatted = formatted.replace(/\s{2,}/g, ' ').trim(); + + // 6. 最终检查:确保没有属性访问被破坏 + formatted = formatted.replace(/(\$\w+|\$_)\s+\.\s*/g, '$1.'); + + return formatted; + } + + + private generateSingleLineComment(text: string): void { + // 确保单行注释以 # 开头 + const cleanText = text.startsWith('#') ? text : `# ${text}`; + this.append(cleanText); + } + + private generateMultilineComment(text: string): void { + // 多行注释保持原格式 + this.append(text); + } + + private isLeadingComment(comment: CommentAst): boolean { + // 简单判断:如果注释在文档开头,就认为是前导注释 + return comment.line <= 3; + } + + private addStatementSeparation(current: StatementAst, next: StatementAst): void { + // 函数之间添加空行 + if (current.type === 'FunctionDefinition' || next.type === 'FunctionDefinition') { + this.appendLine(''); + } + + // 控制结构前添加空行 + if (next.type === 'IfStatement' && !this.lastWasComment) { + this.appendLine(''); + } + } + + private handleFinalNewline(): void { + if (this.rules.insertFinalNewline && this.output.length > 0) { + const lastLine = this.output[this.output.length - 1]; + if (!lastLine.endsWith(this.rules.getNewline())) { + this.appendLine(''); + } + } + } + + private postProcess(code: string): string { + let result = code; + + // 清理多余的空行 + if (this.rules.maxConsecutiveEmptyLines >= 0) { + const maxEmpty = this.rules.maxConsecutiveEmptyLines; + const emptyLinePattern = new RegExp(`(${this.rules.getNewline()}){${maxEmpty + 2},}`, 'g'); + const replacement = this.rules.getNewline().repeat(maxEmpty + 1); + result = result.replace(emptyLinePattern, replacement); + } + + // 清理行尾空白 + if (this.rules.trimTrailingWhitespace) { + result = result.replace(/ +$/gm, ''); + } + + return result; + } + + // 辅助方法 + private append(text: string): void { + this.output.push(text); + this.currentLineLength += text.length; + this.needsNewline = false; + this.lastWasComment = false; + } + + private appendLine(text: string): void { + this.output.push(text + this.rules.getNewline()); + this.currentLineLength = 0; + this.needsNewline = false; + this.lastWasComment = false; + } + + private appendIndent(): void { + const indent = this.rules.getIndent(this.indentLevel); + this.append(indent); + } + + private ensureNewline(): void { + if (!this.needsNewline) { + this.appendLine(''); + this.needsNewline = true; + } + } + + private indent(): void { + this.indentLevel++; + } + + private outdent(): void { + this.indentLevel = Math.max(0, this.indentLevel - 1); + } + + private shouldWrapLine(): boolean { + return this.currentLineLength > this.rules.printWidth; + } +} + +/** + * 便捷函数:格式化PowerShell AST + */ +export function formatPowerShellAST( + ast: ScriptBlockAst, + comments: CommentAst[] = [], + options: Partial = {} +): string { + const generator = new PowerShellCodeGenerator(options); + return generator.generate(ast, comments); +} diff --git a/frontend/src/common/prettier/plugins/powershell/formatter-rules.ts b/frontend/src/common/prettier/plugins/powershell/formatter-rules.ts new file mode 100644 index 0000000..b589d76 --- /dev/null +++ b/frontend/src/common/prettier/plugins/powershell/formatter-rules.ts @@ -0,0 +1,440 @@ +/** + * PowerShell 格式化规则引擎 + * 定义各种可配置的代码格式化规则和策略 + */ + +export interface FormatterOptions { + // 基本格式化选项 + indentSize: number; // 缩进大小 + useTabsForIndentation: boolean; // 使用制表符还是空格 + printWidth: number; // 行最大长度 + endOfLine: 'lf' | 'crlf' | 'cr' | 'auto'; // 行尾符类型 + + // 空格和间距 + spaceAroundOperators: boolean; // 操作符周围的空格 + spaceAfterCommas: boolean; // 逗号后的空格 + spaceAfterSemicolons: boolean; // 分号后的空格 + spaceInsideParentheses: boolean; // 括号内的空格 + spaceInsideBrackets: boolean; // 方括号内的空格 + spaceInsideBraces: boolean; // 大括号内的空格 + + // 换行和空行 + maxConsecutiveEmptyLines: number; // 最大连续空行数 + insertFinalNewline: boolean; // 文件末尾插入换行符 + trimTrailingWhitespace: boolean; // 删除行尾空白 + blankLinesAroundFunctions: number; // 函数前后的空行数 + blankLinesAroundClasses: number; // 类前后的空行数 + blankLinesAroundIfStatements: boolean; // if语句前后的空行 + + // 括号和大括号 + braceStyle: 'allman' | 'otbs' | 'stroustrup'; // 大括号风格 + alwaysParenthesizeArrowFunctions: boolean; // 箭头函数总是用括号 + + // PowerShell特定选项 + formatPipelines: boolean; // 格式化管道 + pipelineStyle: 'oneline' | 'multiline' | 'auto'; // 管道风格 + formatParameters: boolean; // 格式化参数 + parameterAlignment: 'left' | 'right' | 'auto'; // 参数对齐方式 + formatHashtables: boolean; // 格式化哈希表 + hashtableStyle: 'compact' | 'expanded'; // 哈希表风格 + formatArrays: boolean; // 格式化数组 + arrayStyle: 'compact' | 'expanded'; // 数组风格 + formatComments: boolean; // 格式化注释 + commentAlignment: 'left' | 'preserve'; // 注释对齐方式 + + // 命名和大小写 + preferredCommandCase: 'lowercase' | 'uppercase' | 'pascalcase' | 'preserve'; // 命令大小写 + preferredParameterCase: 'lowercase' | 'uppercase' | 'pascalcase' | 'preserve'; // 参数大小写 + preferredVariableCase: 'camelcase' | 'pascalcase' | 'preserve'; // 变量大小写 + + // 引号和字符串 + quotestyle: 'single' | 'double' | 'preserve'; // 引号风格 + escapeNonAscii: boolean; // 转义非ASCII字符 + + // 长度和换行 + wrapLongLines: boolean; // 自动换行长行 + wrapParameters: boolean; // 换行长参数列表 + wrapArrays: boolean; // 换行长数组 + wrapHashtables: boolean; // 换行长哈希表 +} + +export const DEFAULT_OPTIONS: FormatterOptions = { + // 基本选项 + indentSize: 4, + useTabsForIndentation: false, + printWidth: 120, + endOfLine: 'auto', + + // 空格设置 + spaceAroundOperators: true, + spaceAfterCommas: true, + spaceAfterSemicolons: true, + spaceInsideParentheses: false, + spaceInsideBrackets: false, + spaceInsideBraces: true, + + // 空行设置 + maxConsecutiveEmptyLines: 2, + insertFinalNewline: true, + trimTrailingWhitespace: true, + blankLinesAroundFunctions: 1, + blankLinesAroundClasses: 1, + blankLinesAroundIfStatements: false, + + // 括号风格 + braceStyle: 'otbs', // One True Brace Style + alwaysParenthesizeArrowFunctions: false, + + // PowerShell特定 + formatPipelines: true, + pipelineStyle: 'auto', + formatParameters: true, + parameterAlignment: 'left', + formatHashtables: true, + hashtableStyle: 'compact', + formatArrays: true, + arrayStyle: 'compact', + formatComments: true, + commentAlignment: 'preserve', + + // 命名约定 + preferredCommandCase: 'pascalcase', + preferredParameterCase: 'preserve', + preferredVariableCase: 'preserve', + + // 字符串设置 + quotestyle: 'preserve', + escapeNonAscii: false, + + // 长度处理 + wrapLongLines: true, + wrapParameters: true, + wrapArrays: true, + wrapHashtables: true +}; + +/** + * 格式化规则类,包含各种格式化策略的实现 + */ +export class FormatterRules { + private options: FormatterOptions; + + constructor(options: Partial = {}) { + this.options = { ...DEFAULT_OPTIONS, ...options }; + } + + /** + * 获取缩进字符串 + */ + getIndent(level: number): string { + if (level <= 0) return ''; + + const indentChar = this.options.useTabsForIndentation ? '\t' : ' '; + const indentSize = this.options.useTabsForIndentation ? 1 : this.options.indentSize; + + return indentChar.repeat(level * indentSize); + } + + /** + * 获取换行符 + */ + getNewline(): string { + switch (this.options.endOfLine) { + case 'lf': return '\n'; + case 'crlf': return '\r\n'; + case 'cr': return '\r'; + case 'auto': + default: + // 在浏览器环境中默认使用 LF + return '\n'; + } + } + + /** + * 格式化操作符周围的空格 + */ + formatOperatorSpacing(operator: string): string { + if (!this.options.spaceAroundOperators) { + return operator; + } + + // PowerShell语法中绝对不能加空格的操作符(官方规范) + const noSpaceOperators = [ + '.', '::', // 属性访问和静态成员访问 - 这是PowerShell面向对象的核心 + '[', ']', // 数组索引和类型转换 + '(', ')', '{', '}', // 括号 + '@{', // 哈希表字面量开始 + ';', // 哈希表和语句分隔符 + '-', // cmdlet连字符(Get-ChildItem中的-) + '::' // 静态成员访问 + ]; + + if (noSpaceOperators.includes(operator)) { + return operator; + } + + // PowerShell比较操作符需要空格 + const powershellOperators = ['-eq', '-ne', '-lt', '-le', '-gt', '-ge', + '-like', '-notlike', '-match', '-notmatch', + '-contains', '-notcontains', '-in', '-notin', + '-is', '-isnot', '-as', '-and', '-or', '-not', '-xor']; + + if (powershellOperators.some(op => operator.toLowerCase() === op)) { + return ` ${operator} `; + } + + // 算术和赋值操作符需要空格 + const spaceOperators = ['=', '+=', '-=', '*=', '/=', '%=', '+', '*', '/', '%']; + if (spaceOperators.includes(operator)) { + return ` ${operator} `; + } + + return operator; + } + + /** + * 格式化逗号后的空格 + */ + formatComma(): string { + return this.options.spaceAfterCommas ? ', ' : ','; + } + + /** + * 格式化分号后的空格 + */ + formatSemicolon(): string { + return this.options.spaceAfterSemicolons ? '; ' : ';'; + } + + /** + * 格式化括号内的空格 + */ + formatParentheses(content: string): string { + if (this.options.spaceInsideParentheses) { + return `( ${content} )`; + } + return `(${content})`; + } + + /** + * 格式化方括号内的空格 + */ + formatBrackets(content: string): string { + if (this.options.spaceInsideBrackets) { + return `[ ${content} ]`; + } + return `[${content}]`; + } + + /** + * 格式化大括号内的空格 + */ + formatBraces(content: string): string { + if (this.options.spaceInsideBraces) { + return `{ ${content} }`; + } + return `{${content}}`; + } + + /** + * 获取大括号的开始位置 + */ + getBraceStart(): string { + switch (this.options.braceStyle) { + case 'allman': + return this.getNewline() + '{'; + case 'stroustrup': + return this.getNewline() + '{'; + case 'otbs': + default: + return ' {'; + } + } + + /** + * 格式化命令名的大小写 + */ + formatCommandCase(command: string): string { + switch (this.options.preferredCommandCase) { + case 'lowercase': + return command.toLowerCase(); + case 'uppercase': + return command.toUpperCase(); + case 'pascalcase': + return this.toPascalCasePreservingHyphens(command); + case 'preserve': + default: + return command; + } + } + + /** + * 检查是否应该格式化命令大小写 + */ + shouldFormatCommandCase(): boolean { + return this.options.preferredCommandCase !== 'preserve'; + } + + /** + * 格式化参数名的大小写 + */ + formatParameterCase(parameter: string): string { + switch (this.options.preferredParameterCase) { + case 'lowercase': + return parameter.toLowerCase(); + case 'uppercase': + return parameter.toUpperCase(); + case 'pascalcase': + return this.toPascalCase(parameter); + case 'preserve': + default: + return parameter; + } + } + + /** + * 格式化变量名的大小写 + */ + formatVariableCase(variable: string): string { + if (!variable.startsWith('$')) { + return variable; + } + + const variableName = variable.substring(1); + let formattedName: string; + + switch (this.options.preferredVariableCase) { + case 'camelcase': + formattedName = this.toCamelCase(variableName); + break; + case 'pascalcase': + formattedName = this.toPascalCase(variableName); + break; + case 'preserve': + default: + formattedName = variableName; + break; + } + + return '$' + formattedName; + } + + /** + * 格式化字符串引号 + */ + formatQuotes(value: string): string { + if (this.options.quotestyle === 'preserve') { + return value; + } + + const content = this.extractStringContent(value); + + switch (this.options.quotestyle) { + case 'single': + return `'${content.replace(/'/g, "''")}'`; + case 'double': + return `"${content.replace(/"/g, '""')}"`; + default: + return value; + } + } + + /** + * 检查是否需要换行 + */ + shouldWrapLine(line: string): boolean { + return this.options.wrapLongLines && line.length > this.options.printWidth; + } + + /** + * 获取管道样式 + */ + getPipelineStyle(elementCount: number): 'oneline' | 'multiline' { + switch (this.options.pipelineStyle) { + case 'oneline': + return 'oneline'; + case 'multiline': + return 'multiline'; + case 'auto': + default: + return elementCount > 2 ? 'multiline' : 'oneline'; + } + } + + /** + * 获取哈希表样式 + */ + getHashtableStyle(entryCount: number): 'compact' | 'expanded' { + if (this.options.hashtableStyle === 'compact') { + return 'compact'; + } + if (this.options.hashtableStyle === 'expanded') { + return 'expanded'; + } + // auto logic: 对于小型哈希表默认使用compact,避免不必要的换行 + return entryCount > 5 ? 'expanded' : 'compact'; + } + + /** + * 获取数组样式 + */ + getArrayStyle(elementCount: number): 'compact' | 'expanded' { + if (this.options.arrayStyle === 'compact') { + return 'compact'; + } + if (this.options.arrayStyle === 'expanded') { + return 'expanded'; + } + // auto logic could be added here + return elementCount > 5 ? 'expanded' : 'compact'; + } + + // 辅助方法 + private toPascalCase(str: string): string { + return str.split(/[-_\s]/) + .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(''); + } + + /** + * 转换为PascalCase但保留连字符(专门用于PowerShell cmdlet) + */ + private toPascalCasePreservingHyphens(str: string): string { + return str.split('-') + .map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()) + .join('-'); + } + + private toCamelCase(str: string): string { + const pascalCase = this.toPascalCase(str); + return pascalCase.charAt(0).toLowerCase() + pascalCase.slice(1); + } + + private extractStringContent(str: string): string { + if ((str.startsWith('"') && str.endsWith('"')) || + (str.startsWith("'") && str.endsWith("'"))) { + return str.slice(1, -1); + } + return str; + } + + // Getter methods for options + get indentSize(): number { return this.options.indentSize; } + get printWidth(): number { return this.options.printWidth; } + get maxConsecutiveEmptyLines(): number { return this.options.maxConsecutiveEmptyLines; } + get insertFinalNewline(): boolean { return this.options.insertFinalNewline; } + get trimTrailingWhitespace(): boolean { return this.options.trimTrailingWhitespace; } + get blankLinesAroundFunctions(): number { return this.options.blankLinesAroundFunctions; } + get formatPipelines(): boolean { return this.options.formatPipelines; } + get formatParameters(): boolean { return this.options.formatParameters; } + get formatHashtables(): boolean { return this.options.formatHashtables; } + get formatArrays(): boolean { return this.options.formatArrays; } + get formatComments(): boolean { return this.options.formatComments; } + + /** + * 创建规则的副本,可以重写部分选项 + */ + withOptions(overrides: Partial): FormatterRules { + return new FormatterRules({ ...this.options, ...overrides }); + } +} diff --git a/frontend/src/common/prettier/plugins/powershell/index.ts b/frontend/src/common/prettier/plugins/powershell/index.ts index 49d9023..7d595be 100644 --- a/frontend/src/common/prettier/plugins/powershell/index.ts +++ b/frontend/src/common/prettier/plugins/powershell/index.ts @@ -1,20 +1,21 @@ /** - * Prettier Plugin for PowerShell file formatting + * Prettier Plugin for PowerShell file formatting - Modular Version * * This plugin provides support for formatting PowerShell files (.ps1, .psm1, .psd1) - * using PowerShell's native AST parser for accurate syntax analysis. + * using a modular architecture with lexer, parser, AST, and code generator. */ import type { Plugin, Parser, Printer, AstPath, Doc } from 'prettier'; -import parse, { formatPowerShellCode } from './parse'; +import { PowerShellLexer } from './lexer'; +import { PowerShellParser } from './parser'; +import { ScriptBlockAst, CommentAst } from './ast'; +import { formatPowerShellAST } from './code-generator'; +import { FormatterOptions, DEFAULT_OPTIONS } from './formatter-rules'; -// PowerShell AST节点接口 -interface PowerShellAstNode { - type: string; - value: string; - start?: number; - end?: number; - parent?: PowerShellAstNode; - extent?: any; +// PowerShell格式化结果接口 +interface PowerShellParseResult { + ast: ScriptBlockAst; + comments: CommentAst[]; + originalText: string; } const parserName = 'powershell'; @@ -35,62 +36,159 @@ const languages = [ ]; // 解析器配置 -const powershellParser: Parser = { - parse, +const powershellParser: Parser = { + parse: parseCode, astFormat: 'powershell', - locStart: () => 0, - locEnd: () => 0, + locStart: (node: PowerShellParseResult) => 0, + locEnd: (node: PowerShellParseResult) => node.originalText.length, }; -const printPosh = (path: AstPath, options: any, print: any): Doc => { - const pathNode = path.node; - - if (Array.isArray(pathNode)) { - return pathNode.map(node => handleAst(node, path, options, print)).join('\n'); - } - - return handleAst(pathNode, path, options, print); -}; - -const handleAst = (node: PowerShellAstNode, path: AstPath, options: any, print: any): Doc => { - if (typeof node === 'undefined') { - return ''; - } - - // 使用修复后的PowerShell格式化器 +/** + * 解析PowerShell代码 + */ +async function parseCode(text: string, parsers?: any, options?: any): Promise { try { - const formattedCode = formatPowerShellCode(node.value, { - indentSize: options.tabWidth || 4, - useTabsForIndentation: options.useTabs || false, - printWidth: options.printWidth || 120, + // 词法分析 + const lexer = new PowerShellLexer(text); + const tokens = lexer.tokenize(); + + // 语法分析 + const parser = new PowerShellParser(tokens, text); + const ast = parser.parse(); + const comments = parser.getComments(); + + return { + ast, + comments, + originalText: text + }; + } catch (error) { + console.warn('PowerShell parsing failed, using fallback:', error); + + // 解析失败时,创建一个包含原始文本的简单AST + // 这样可以确保格式化失败时返回原始代码而不是空内容 + return { + ast: { + type: 'ScriptBlock', + statements: [{ + type: 'RawText', + value: text, + start: 0, + end: text.length, + line: 1, + column: 1 + } as any], + start: 0, + end: text.length, + line: 1, + column: 1 + }, + comments: [], + originalText: text + }; + } +} + +/** + * PowerShell代码打印器 + */ +const printPowerShell = (path: AstPath, options: any): Doc => { + const parseResult = path.node; + + try { + // 构建格式化选项 - 优先保持原有格式,避免破坏PowerShell语法 + const formatterOptions: Partial = { + indentSize: options.tabWidth || DEFAULT_OPTIONS.indentSize, + useTabsForIndentation: options.useTabs || DEFAULT_OPTIONS.useTabsForIndentation, + printWidth: options.printWidth || DEFAULT_OPTIONS.printWidth, spaceAroundOperators: true, - formatComments: true, - removeExtraBlankLines: true, formatPipelines: true, - formatParentheses: true, - formatArraysAndHashtables: true, + formatParameters: true, + formatHashtables: true, + hashtableStyle: 'compact', // 强制使用紧凑格式,避免不必要的换行 + formatArrays: true, + arrayStyle: 'compact', + formatComments: true, maxConsecutiveEmptyLines: 1, - addBlankLinesAroundBlocks: true, - formatLongLines: true, - formatFunctionDefinitions: true, - formatPowerShellSyntax: true, - }); + insertFinalNewline: true, + trimTrailingWhitespace: true, + blankLinesAroundFunctions: 1, + braceStyle: 'otbs', + preferredCommandCase: 'preserve', // 保持原有命令大小写,不破坏语法 + preferredParameterCase: 'preserve', + preferredVariableCase: 'preserve', + quotestyle: 'preserve', + wrapLongLines: true + }; + + // 使用新的模块化格式化器 + const formattedCode = formatPowerShellAST( + parseResult.ast, + parseResult.comments, + formatterOptions + ); return formattedCode; } catch (error) { console.warn('PowerShell formatting failed, returning original code:', error); - return node.value || ''; + return parseResult.originalText; } }; // 打印器配置 -const powershellPrinter: Printer = { - print: printPosh, +const powershellPrinter: Printer = { + print: printPowerShell, }; -// 插件选项 +// 插件选项配置 const options = { - + // PowerShell特定格式化选项 + powershellBraceStyle: { + type: 'choice' as const, + category: 'PowerShell', + default: DEFAULT_OPTIONS.braceStyle, + description: 'PowerShell大括号样式', + choices: [ + { value: 'allman', description: 'Allman风格(大括号另起一行)' }, + { value: 'otbs', description: '1TBS风格(大括号同行)' }, + { value: 'stroustrup', description: 'Stroustrup风格' } + ] + }, + powershellCommandCase: { + type: 'choice' as const, + category: 'PowerShell', + default: DEFAULT_OPTIONS.preferredCommandCase, + description: 'PowerShell命令大小写风格', + choices: [ + { value: 'lowercase', description: '小写' }, + { value: 'uppercase', description: '大写' }, + { value: 'pascalcase', description: 'Pascal大小写' }, + { value: 'preserve', description: '保持原样' } + ] + }, + powershellPipelineStyle: { + type: 'choice' as const, + category: 'PowerShell', + default: DEFAULT_OPTIONS.pipelineStyle, + description: 'PowerShell管道样式', + choices: [ + { value: 'oneline', description: '单行' }, + { value: 'multiline', description: '多行' }, + { value: 'auto', description: '自动' } + ] + }, + powershellSpaceAroundOperators: { + type: 'boolean' as const, + category: 'PowerShell', + default: DEFAULT_OPTIONS.spaceAroundOperators, + description: '在操作符周围添加空格' + }, + powershellMaxEmptyLines: { + type: 'int' as const, + category: 'PowerShell', + default: DEFAULT_OPTIONS.maxConsecutiveEmptyLines, + description: '最大连续空行数' + } }; const powershellPlugin: Plugin = { diff --git a/frontend/src/common/prettier/plugins/powershell/lexer.ts b/frontend/src/common/prettier/plugins/powershell/lexer.ts new file mode 100644 index 0000000..1913594 --- /dev/null +++ b/frontend/src/common/prettier/plugins/powershell/lexer.ts @@ -0,0 +1,722 @@ +/** + * PowerShell 词法分析器 (Lexer) + * 将PowerShell代码分解为tokens,用于后续的语法分析和格式化 + */ + +export enum TokenType { + // 字面量 + STRING = 'STRING', + NUMBER = 'NUMBER', + VARIABLE = 'VARIABLE', + + // 关键字 + KEYWORD = 'KEYWORD', + FUNCTION = 'FUNCTION', + + // 操作符 + OPERATOR = 'OPERATOR', + ASSIGNMENT = 'ASSIGNMENT', + COMPARISON = 'COMPARISON', + LOGICAL = 'LOGICAL', + ARITHMETIC = 'ARITHMETIC', + + // 分隔符 + LEFT_PAREN = 'LEFT_PAREN', + RIGHT_PAREN = 'RIGHT_PAREN', + LEFT_BRACE = 'LEFT_BRACE', + RIGHT_BRACE = 'RIGHT_BRACE', + LEFT_BRACKET = 'LEFT_BRACKET', + RIGHT_BRACKET = 'RIGHT_BRACKET', + SEMICOLON = 'SEMICOLON', + COMMA = 'COMMA', + DOT = 'DOT', + PIPE = 'PIPE', + + // 特殊 + WHITESPACE = 'WHITESPACE', + NEWLINE = 'NEWLINE', + COMMENT = 'COMMENT', + MULTILINE_COMMENT = 'MULTILINE_COMMENT', + HERE_STRING = 'HERE_STRING', + + // 控制结构 + IF = 'IF', + ELSE = 'ELSE', + ELSEIF = 'ELSEIF', + WHILE = 'WHILE', + FOR = 'FOR', + FOREACH = 'FOREACH', + SWITCH = 'SWITCH', + TRY = 'TRY', + CATCH = 'CATCH', + FINALLY = 'FINALLY', + + // 其他 + IDENTIFIER = 'IDENTIFIER', + CMDLET = 'CMDLET', + PARAMETER = 'PARAMETER', + EOF = 'EOF', + UNKNOWN = 'UNKNOWN' +} + +export interface Token { + type: TokenType; + value: string; + line: number; + column: number; + startIndex: number; + endIndex: number; +} + +export class PowerShellLexer { + private code: string; + private position: number = 0; + private line: number = 1; + private column: number = 1; + private tokens: Token[] = []; + + // PowerShell关键字 + private readonly keywords = new Set([ + 'if', 'else', 'elseif', 'switch', 'while', 'for', 'foreach', 'do', + 'try', 'catch', 'finally', 'throw', 'return', 'break', 'continue', + 'function', 'filter', 'param', 'begin', 'process', 'end', + 'class', 'enum', 'using', 'namespace', 'workflow', 'configuration', + 'dynamicparam', 'exit' + ]); + + // PowerShell比较操作符 + private readonly comparisonOperators = new Set([ + '-eq', '-ne', '-lt', '-le', '-gt', '-ge', + '-like', '-notlike', '-match', '-notmatch', + '-contains', '-notcontains', '-in', '-notin', + '-is', '-isnot', '-as' + ]); + + // PowerShell逻辑操作符 + private readonly logicalOperators = new Set([ + '-and', '-or', '-not', '-xor', '-band', '-bor', '-bxor', '-bnot' + ]); + + constructor(code: string) { + this.code = code; + } + + /** + * 对代码进行词法分析,返回token数组 + */ + public tokenize(): Token[] { + this.position = 0; + this.line = 1; + this.column = 1; + this.tokens = []; + + while (this.position < this.code.length) { + this.skipWhitespace(); + + if (this.position >= this.code.length) { + break; + } + + const token = this.nextToken(); + if (token) { + this.tokens.push(token); + } + } + + this.tokens.push({ + type: TokenType.EOF, + value: '', + line: this.line, + column: this.column, + startIndex: this.position, + endIndex: this.position + }); + + return this.tokens; + } + + private nextToken(): Token | null { + const startPos = this.position; + const startLine = this.line; + const startColumn = this.column; + + const char = this.code[this.position]; + + // 处理换行 + if (char === '\n') { + this.advance(); + return this.createToken(TokenType.NEWLINE, '\n', startPos, startLine, startColumn); + } + + // 处理注释 + if (char === '#') { + return this.tokenizeComment(startPos, startLine, startColumn); + } + + // 处理多行注释 + if (char === '<' && this.peek() === '#') { + return this.tokenizeMultilineComment(startPos, startLine, startColumn); + } + + // 处理字符串 + if (char === '"' || char === "'") { + return this.tokenizeString(startPos, startLine, startColumn); + } + + // 处理Here-String + if (char === '@' && (this.peek() === '"' || this.peek() === "'")) { + return this.tokenizeHereString(startPos, startLine, startColumn); + } + + // 处理哈希表字面量 @{ + if (char === '@' && this.peek() === '{') { + this.advance(); // skip '@' + this.advance(); // skip '{' + return this.createToken(TokenType.LEFT_BRACE, '@{', startPos, startLine, startColumn); + } + + // 处理变量 + if (char === '$') { + return this.tokenizeVariable(startPos, startLine, startColumn); + } + + // 处理数字 + if (this.isDigit(char) || (char === '.' && this.isDigit(this.peek()))) { + return this.tokenizeNumber(startPos, startLine, startColumn); + } + + // 处理操作符和分隔符 + const operatorToken = this.tokenizeOperator(startPos, startLine, startColumn); + if (operatorToken) { + return operatorToken; + } + + // 优先处理PowerShell比较操作符(以-开头) + if (char === '-' && this.isIdentifierStart(this.peek())) { + const potentialOperator = this.peekPowerShellOperator(); + if (potentialOperator) { + return this.tokenizePowerShellOperator(startPos, startLine, startColumn); + } + // 如果不是操作符,可能是参数 + return this.tokenizeParameter(startPos, startLine, startColumn); + } + + // 处理标识符(包括cmdlet和关键字) + if (this.isIdentifierStart(char)) { + return this.tokenizeIdentifier(startPos, startLine, startColumn); + } + + // 处理PowerShell特殊字符 + if (char === '?') { + this.advance(); + return this.createToken(TokenType.OPERATOR, char, startPos, startLine, startColumn); + } + + // 处理独立的减号(可能是负数或减法) + if (char === '-') { + this.advance(); + return this.createToken(TokenType.ARITHMETIC, char, startPos, startLine, startColumn); + } + + // 处理其他可能的特殊字符,作为标识符处理而不是未知字符 + if (this.isPrintableChar(char)) { + this.advance(); + return this.createToken(TokenType.IDENTIFIER, char, startPos, startLine, startColumn); + } + + // 真正的未知字符(非打印字符等) + this.advance(); + return this.createToken(TokenType.UNKNOWN, char, startPos, startLine, startColumn); + } + + private tokenizeComment(startPos: number, startLine: number, startColumn: number): Token { + let value = ''; + while (this.position < this.code.length && this.code[this.position] !== '\n') { + value += this.code[this.position]; + this.advance(); + } + return this.createToken(TokenType.COMMENT, value, startPos, startLine, startColumn); + } + + private tokenizeMultilineComment(startPos: number, startLine: number, startColumn: number): Token { + let value = ''; + this.advance(); // skip '<' + this.advance(); // skip '#' + value += '<#'; + + while (this.position < this.code.length - 1) { + if (this.code[this.position] === '#' && this.code[this.position + 1] === '>') { + value += '#>'; + this.advance(); + this.advance(); + break; + } + value += this.code[this.position]; + this.advance(); + } + + return this.createToken(TokenType.MULTILINE_COMMENT, value, startPos, startLine, startColumn); + } + + private tokenizeString(startPos: number, startLine: number, startColumn: number): Token { + const quote = this.code[this.position]; + let value = quote; + this.advance(); + + while (this.position < this.code.length) { + const char = this.code[this.position]; + value += char; + + if (char === quote) { + this.advance(); + break; + } + + // 处理转义字符 + if (char === '`' && quote === '"') { + this.advance(); + if (this.position < this.code.length) { + value += this.code[this.position]; + this.advance(); + } + } else { + this.advance(); + } + } + + return this.createToken(TokenType.STRING, value, startPos, startLine, startColumn); + } + + private tokenizeHereString(startPos: number, startLine: number, startColumn: number): Token { + const quote = this.code[this.position + 1]; // " or ' + let value = `@${quote}`; + this.advance(); // skip '@' + this.advance(); // skip quote + + while (this.position < this.code.length - 1) { + if (this.code[this.position] === quote && this.code[this.position + 1] === '@') { + value += `${quote}@`; + this.advance(); + this.advance(); + break; + } + value += this.code[this.position]; + this.advance(); + } + + return this.createToken(TokenType.HERE_STRING, value, startPos, startLine, startColumn); + } + + private tokenizeVariable(startPos: number, startLine: number, startColumn: number): Token { + let value = '$'; + this.advance(); // skip '$' + + // 处理特殊变量如 $_, $$, $^ + const specialVars = ['_', '$', '^', '?']; + if (specialVars.includes(this.code[this.position])) { + value += this.code[this.position]; + this.advance(); + return this.createToken(TokenType.VARIABLE, value, startPos, startLine, startColumn); + } + + // 处理大括号变量 ${variable name} + if (this.code[this.position] === '{') { + this.advance(); // skip '{' + value += '{'; + while (this.position < this.code.length && this.code[this.position] !== '}') { + value += this.code[this.position]; + this.advance(); + } + if (this.position < this.code.length) { + value += '}'; + this.advance(); // skip '}' + } + return this.createToken(TokenType.VARIABLE, value, startPos, startLine, startColumn); + } + + // 普通变量名 + while (this.position < this.code.length && this.isIdentifierChar(this.code[this.position])) { + value += this.code[this.position]; + this.advance(); + } + + return this.createToken(TokenType.VARIABLE, value, startPos, startLine, startColumn); + } + + private tokenizeNumber(startPos: number, startLine: number, startColumn: number): Token { + let value = ''; + let hasDecimal = false; + + while (this.position < this.code.length) { + const char = this.code[this.position]; + + if (this.isDigit(char)) { + value += char; + this.advance(); + } else if (char === '.' && !hasDecimal && this.isDigit(this.peek())) { + hasDecimal = true; + value += char; + this.advance(); + } else { + break; + } + } + + // 检查是否有PowerShell数字单位后缀(KB, MB, GB, TB, PB) + const unitPattern = /^(KB|MB|GB|TB|PB)/i; + const remainingCode = this.code.substring(this.position); + const unitMatch = remainingCode.match(unitPattern); + + if (unitMatch) { + value += unitMatch[0]; // 使用 [0] 获取完整匹配 + // 移动position到单位后面 + for (let i = 0; i < unitMatch[0].length; i++) { + this.advance(); + } + } + + return this.createToken(TokenType.NUMBER, value, startPos, startLine, startColumn); + } + + private tokenizeOperator(startPos: number, startLine: number, startColumn: number): Token | null { + const char = this.code[this.position]; + + // 双字符操作符 + const twoChar = this.code.substring(this.position, this.position + 2); + const doubleOperators = ['==', '!=', '<=', '>=', '++', '--', '+=', '-=', '*=', '/=', '%=']; + + if (doubleOperators.includes(twoChar)) { + this.advance(); + this.advance(); + return this.createToken(TokenType.OPERATOR, twoChar, startPos, startLine, startColumn); + } + + // 单字符操作符 + switch (char) { + case '=': + this.advance(); + return this.createToken(TokenType.ASSIGNMENT, char, startPos, startLine, startColumn); + case '+': + case '*': + case '/': + case '%': + this.advance(); + return this.createToken(TokenType.ARITHMETIC, char, startPos, startLine, startColumn); + case '-': + // 不在这里处理'-',让PowerShell操作符检查优先处理 + return null; + case '(': + this.advance(); + return this.createToken(TokenType.LEFT_PAREN, char, startPos, startLine, startColumn); + case ')': + this.advance(); + return this.createToken(TokenType.RIGHT_PAREN, char, startPos, startLine, startColumn); + case '{': + this.advance(); + return this.createToken(TokenType.LEFT_BRACE, char, startPos, startLine, startColumn); + case '}': + this.advance(); + return this.createToken(TokenType.RIGHT_BRACE, char, startPos, startLine, startColumn); + case '[': + // 检查是否是PowerShell类型转换 [type] + const typePattern = this.peekTypeConversion(); + if (typePattern) { + return this.tokenizeTypeConversion(startPos, startLine, startColumn); + } + this.advance(); + return this.createToken(TokenType.LEFT_BRACKET, char, startPos, startLine, startColumn); + case ']': + this.advance(); + return this.createToken(TokenType.RIGHT_BRACKET, char, startPos, startLine, startColumn); + case ';': + this.advance(); + return this.createToken(TokenType.SEMICOLON, char, startPos, startLine, startColumn); + case ',': + this.advance(); + return this.createToken(TokenType.COMMA, char, startPos, startLine, startColumn); + case '.': + this.advance(); + return this.createToken(TokenType.DOT, char, startPos, startLine, startColumn); + case '|': + this.advance(); + return this.createToken(TokenType.PIPE, char, startPos, startLine, startColumn); + } + + return null; + } + + private tokenizeIdentifier(startPos: number, startLine: number, startColumn: number): Token { + let value = ''; + + // 改进的标识符识别,支持PowerShell cmdlet格式(动词-名词) + while (this.position < this.code.length) { + const char = this.code[this.position]; + + if (this.isIdentifierChar(char)) { + value += char; + this.advance(); + } else if (char === '-' && value.length > 0 && this.isIdentifierStart(this.peek())) { + // 检查是否是cmdlet格式(动词-名词) + const nextPart = this.peekIdentifierPart(); + if (nextPart && !this.isPowerShellOperator('-' + nextPart)) { + // 这是cmdlet名字的一部分,继续 + value += char; + this.advance(); + } else { + // 这可能是操作符,停止 + break; + } + } else { + break; + } + } + + const lowerValue = value.toLowerCase(); + + // 检查是否是关键字 + if (this.keywords.has(lowerValue)) { + return this.createToken(this.getKeywordTokenType(lowerValue), value, startPos, startLine, startColumn); + } + + // 检查是否是函数(以动词-名词格式) + if (this.isCmdletName(value)) { + return this.createToken(TokenType.CMDLET, value, startPos, startLine, startColumn); + } + + return this.createToken(TokenType.IDENTIFIER, value, startPos, startLine, startColumn); + } + + private tokenizeParameter(startPos: number, startLine: number, startColumn: number): Token { + let value = ''; + + while (this.position < this.code.length && (this.isIdentifierChar(this.code[this.position]) || this.code[this.position] === '-')) { + value += this.code[this.position]; + this.advance(); + } + + const lowerValue = value.toLowerCase(); + + // 检查是否是比较操作符 + if (this.comparisonOperators.has(lowerValue)) { + return this.createToken(TokenType.COMPARISON, value, startPos, startLine, startColumn); + } + + // 检查是否是逻辑操作符 + if (this.logicalOperators.has(lowerValue)) { + return this.createToken(TokenType.LOGICAL, value, startPos, startLine, startColumn); + } + + return this.createToken(TokenType.PARAMETER, value, startPos, startLine, startColumn); + } + + private getKeywordTokenType(keyword: string): TokenType { + switch (keyword) { + case 'if': return TokenType.IF; + case 'else': return TokenType.ELSE; + case 'elseif': return TokenType.ELSEIF; + case 'while': return TokenType.WHILE; + case 'for': return TokenType.FOR; + case 'foreach': return TokenType.FOREACH; + case 'switch': return TokenType.SWITCH; + case 'try': return TokenType.TRY; + case 'catch': return TokenType.CATCH; + case 'finally': return TokenType.FINALLY; + case 'function': return TokenType.FUNCTION; + default: return TokenType.KEYWORD; + } + } + + private isCmdletName(name: string): boolean { + // PowerShell cmdlet通常遵循 Verb-Noun 格式,可能包含多个连字符 + const verbNounPattern = /^[A-Za-z]+(-[A-Za-z]+)+$/; + return verbNounPattern.test(name); + } + + private peekPowerShellOperator(): string | null { + // 检查是否是PowerShell比较或逻辑操作符 + const operatorPatterns = [ + '-eq', '-ne', '-lt', '-le', '-gt', '-ge', + '-like', '-notlike', '-match', '-notmatch', + '-contains', '-notcontains', '-in', '-notin', + '-is', '-isnot', '-as', + '-and', '-or', '-not', '-xor', + '-band', '-bor', '-bxor', '-bnot' + ]; + + for (const op of operatorPatterns) { + if (this.matchesOperator(op)) { + return op; + } + } + return null; + } + + private matchesOperator(operator: string): boolean { + if (this.position + operator.length > this.code.length) { + return false; + } + + const substr = this.code.substring(this.position, this.position + operator.length); + if (substr.toLowerCase() !== operator.toLowerCase()) { + return false; + } + + // 确保操作符后面不是字母数字字符(避免匹配部分单词) + const nextChar = this.position + operator.length < this.code.length + ? this.code[this.position + operator.length] + : ' '; + return !this.isIdentifierChar(nextChar); + } + + private tokenizePowerShellOperator(startPos: number, startLine: number, startColumn: number): Token { + const operator = this.peekPowerShellOperator(); + if (!operator) { + // 如果不是操作符,作为参数处理 + return this.tokenizeParameter(startPos, startLine, startColumn); + } + + // 消费操作符字符 + for (let i = 0; i < operator.length; i++) { + this.advance(); + } + + const lowerOp = operator.toLowerCase(); + + // 确定操作符类型 + if (this.comparisonOperators.has(lowerOp)) { + return this.createToken(TokenType.COMPARISON, operator, startPos, startLine, startColumn); + } else if (this.logicalOperators.has(lowerOp)) { + return this.createToken(TokenType.LOGICAL, operator, startPos, startLine, startColumn); + } else { + return this.createToken(TokenType.OPERATOR, operator, startPos, startLine, startColumn); + } + } + + private peekIdentifierPart(): string | null { + if (this.position + 1 >= this.code.length) { + return null; + } + + let result = ''; + let pos = this.position + 1; // 跳过连字符 + + while (pos < this.code.length && this.isIdentifierChar(this.code[pos])) { + result += this.code[pos]; + pos++; + } + + return result.length > 0 ? result : null; + } + + private isPowerShellOperator(text: string): boolean { + const lowerText = text.toLowerCase(); + return this.comparisonOperators.has(lowerText) || this.logicalOperators.has(lowerText); + } + + private peekTypeConversion(): string | null { + // 检查是否是PowerShell类型转换,如 [int], [string], [datetime] 等 + if (this.code[this.position] !== '[') { + return null; + } + + let pos = this.position + 1; // 跳过 '[' + let typeContent = ''; + + // 查找类型名称 + while (pos < this.code.length && this.code[pos] !== ']') { + typeContent += this.code[pos]; + pos++; + } + + if (pos >= this.code.length || this.code[pos] !== ']') { + return null; // 没有找到匹配的 ']' + } + + // 检查是否是有效的PowerShell类型 + const validTypes = [ + 'int', 'int32', 'int64', 'string', 'bool', 'boolean', 'char', 'byte', + 'double', 'float', 'decimal', 'long', 'short', 'datetime', 'timespan', + 'array', 'hashtable', 'object', 'psobject', 'xml', 'scriptblock', + 'guid', 'uri', 'version', 'regex', 'mailaddress', 'ipaddress' + ]; + + const lowerType = typeContent.toLowerCase().trim(); + if (validTypes.includes(lowerType) || lowerType.includes('.')) { + return `[${typeContent}]`; + } + + return null; + } + + private tokenizeTypeConversion(startPos: number, startLine: number, startColumn: number): Token { + const typeConversion = this.peekTypeConversion(); + if (!typeConversion) { + // 这不应该发生,但作为安全措施 + this.advance(); + return this.createToken(TokenType.LEFT_BRACKET, '[', startPos, startLine, startColumn); + } + + // 消费整个类型转换 + for (let i = 0; i < typeConversion.length; i++) { + this.advance(); + } + + return this.createToken(TokenType.IDENTIFIER, typeConversion, startPos, startLine, startColumn); + } + + private isIdentifierStart(char: string): boolean { + return /[a-zA-Z_]/.test(char); + } + + private isIdentifierChar(char: string): boolean { + return /[a-zA-Z0-9_]/.test(char); + } + + private isDigit(char: string): boolean { + return char >= '0' && char <= '9'; + } + + private isPrintableChar(char: string): boolean { + // 检查是否为可打印字符(非控制字符) + const charCode = char.charCodeAt(0); + return charCode >= 32 && charCode <= 126; + } + + private advance(): void { + if (this.position < this.code.length) { + if (this.code[this.position] === '\n') { + this.line++; + this.column = 1; + } else { + this.column++; + } + this.position++; + } + } + + private peek(): string { + return this.position + 1 < this.code.length ? this.code[this.position + 1] : ''; + } + + private skipWhitespace(): void { + while (this.position < this.code.length) { + const char = this.code[this.position]; + if (char === ' ' || char === '\t' || char === '\r') { + this.advance(); + } else { + break; + } + } + } + + private createToken(type: TokenType, value: string, startPos: number, line: number, column: number): Token { + return { + type, + value, + line, + column, + startIndex: startPos, + endIndex: this.position + }; + } +} diff --git a/frontend/src/common/prettier/plugins/powershell/parse.ts b/frontend/src/common/prettier/plugins/powershell/parse.ts deleted file mode 100644 index 8f0cddb..0000000 --- a/frontend/src/common/prettier/plugins/powershell/parse.ts +++ /dev/null @@ -1,787 +0,0 @@ -/** - * PowerShell 代码解析器和格式化器 - */ - -// PowerShell AST节点类型 -interface PowerShellAstNode { - type: string; - value: string; - start?: number; - end?: number; - parent?: PowerShellAstNode; - extent?: any; -} - -// 解析器函数类型 -type PowerShellParserResult = PowerShellAstNode | PowerShellAstNode[]; - -// PowerShell格式化选项 -export interface PowerShellFormatterOptions { - /** 缩进大小,默认为4 */ - indentSize?: number; - /** 使用制表符还是空格,默认为空格 */ - useTabsForIndentation?: boolean; - /** 行最大长度,默认为120 */ - printWidth?: number; - /** 是否在操作符周围添加空格,默认为true */ - spaceAroundOperators?: boolean; - /** 是否格式化注释,默认为true */ - formatComments?: boolean; - /** 是否去除多余的空行,默认为true */ - removeExtraBlankLines?: boolean; - /** 是否格式化管道符,默认为true */ - formatPipelines?: boolean; - /** 是否格式化括号内空格,默认为true */ - formatParentheses?: boolean; - /** 是否格式化数组和哈希表,默认为true */ - formatArraysAndHashtables?: boolean; - /** 最大连续空行数,默认为1 */ - maxConsecutiveEmptyLines?: number; - /** 是否在代码块前后添加空行,默认为true */ - addBlankLinesAroundBlocks?: boolean; - /** 是否格式化长行(自动换行),默认为true */ - formatLongLines?: boolean; - /** 是否格式化函数定义,默认为true */ - formatFunctionDefinitions?: boolean; - /** 是否格式化PowerShell特有语法(switch、try-catch、param等),默认为true */ - formatPowerShellSyntax?: boolean; -} - -/** - * PowerShell代码格式化器 - 修复版本 - */ -class PowerShellFormatter { - private options: Required; - - constructor(options: PowerShellFormatterOptions = {}) { - this.options = { - indentSize: options.indentSize ?? 4, - useTabsForIndentation: options.useTabsForIndentation ?? false, - printWidth: options.printWidth ?? 120, - spaceAroundOperators: options.spaceAroundOperators ?? true, - formatComments: options.formatComments ?? true, - removeExtraBlankLines: options.removeExtraBlankLines ?? true, - formatPipelines: options.formatPipelines ?? true, - formatParentheses: options.formatParentheses ?? true, - formatArraysAndHashtables: options.formatArraysAndHashtables ?? true, - maxConsecutiveEmptyLines: options.maxConsecutiveEmptyLines ?? 1, - addBlankLinesAroundBlocks: options.addBlankLinesAroundBlocks ?? true, - formatLongLines: options.formatLongLines ?? true, - formatFunctionDefinitions: options.formatFunctionDefinitions ?? true, - formatPowerShellSyntax: options.formatPowerShellSyntax ?? true, - }; - } - - /** - * 格式化PowerShell代码 - */ - format(code: string): string { - if (!code || code.trim().length === 0) { - return code; - } - - try { - const lines = code.split('\n'); - let formattedLines = this.formatLines(lines); - - // 处理多余空行 - if (this.options.removeExtraBlankLines) { - formattedLines = this.removeExtraBlankLines(formattedLines); - } - - // 在代码块前后添加适当的空行 - if (this.options.addBlankLinesAroundBlocks) { - formattedLines = this.addBlankLinesAroundBlocks(formattedLines); - } - - return formattedLines.join('\n'); - } catch (error) { - console.warn('PowerShell formatting failed:', error); - return code; // 返回原始代码 - } - } - - private formatLines(lines: string[]): string[] { - const result: string[] = []; - let indentLevel = 0; - let inMultiLineComment = false; - let inHereString = false; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - const trimmedLine = line.trim(); - - // 检查 Here-String (@"..."@ 或 @'...'@) - if (trimmedLine.startsWith('@"') || trimmedLine.startsWith("@'")) { - if (!trimmedLine.endsWith('"@') && !trimmedLine.endsWith("'@")) { - inHereString = true; - } - } - if (inHereString && (trimmedLine.endsWith('"@') || trimmedLine.endsWith("'@"))) { - inHereString = false; - result.push(line); // Here-String 结束行保持原样 - continue; - } - - // 在 Here-String 内部不处理 - if (inHereString) { - result.push(line); - continue; - } - - // 检查多行注释 - if (trimmedLine.includes('<#')) { - inMultiLineComment = true; - } - if (trimmedLine.includes('#>')) { - inMultiLineComment = false; - result.push(this.getIndent(indentLevel) + trimmedLine); - continue; - } - if (inMultiLineComment) { - result.push(this.getIndent(indentLevel) + trimmedLine); - continue; - } - - // 处理空行 - if (trimmedLine.length === 0) { - result.push(''); - continue; - } - - // 检查是否需要减少缩进级别 - if (this.shouldDecreaseIndent(trimmedLine)) { - indentLevel = Math.max(0, indentLevel - 1); - } - - // 格式化当前行 - const formattedLine = this.formatLine(trimmedLine); - result.push(this.getIndent(indentLevel) + formattedLine); - - // 检查是否需要增加缩进级别 - if (this.shouldIncreaseIndent(trimmedLine)) { - indentLevel++; - } - } - - return result; - } - - private formatLine(line: string): string { - let formatted = line; - - // 处理操作符周围的空格 - if (this.options.spaceAroundOperators) { - formatted = this.addSpacesAroundOperators(formatted); - } - - // 格式化注释 - if (this.options.formatComments) { - formatted = this.formatComment(formatted); - } - - // 格式化管道符 - if (this.options.formatPipelines) { - formatted = this.formatPipelines(formatted); - } - - // 格式化括号 - if (this.options.formatParentheses) { - formatted = this.formatParentheses(formatted); - } - - // 格式化数组和哈希表 - if (this.options.formatArraysAndHashtables) { - formatted = this.formatArraysAndHashtables(formatted); - } - - // 格式化函数定义 - if (this.options.formatFunctionDefinitions) { - formatted = this.formatFunctionDefinitions(formatted); - } - - // 格式化PowerShell特有语法 - if (this.options.formatPowerShellSyntax) { - formatted = this.formatPowerShellSyntax(formatted); - } - - // 格式化长行(如果需要) - if (this.options.formatLongLines && formatted.length > this.options.printWidth) { - formatted = this.formatLongLine(formatted); - } - - return formatted; - } - - private addSpacesAroundOperators(line: string): string { - // 定义PowerShell操作符映射 - 修复版本 - const operatorMappings = [ - // 赋值操作符 - { pattern: /\s*=\s*/g, replacement: ' = ' }, - { pattern: /\s*\+=\s*/g, replacement: ' += ' }, - { pattern: /\s*-=\s*/g, replacement: ' -= ' }, - { pattern: /\s*\*=\s*/g, replacement: ' *= ' }, - { pattern: /\s*\/=\s*/g, replacement: ' /= ' }, - { pattern: /\s*%=\s*/g, replacement: ' %= ' }, - - // 算术操作符 (避免与参数冲突) - { pattern: /(\w)\s*\+\s*(\w)/g, replacement: '$1 + $2' }, - { pattern: /(\w)\s*-\s*(\w)/g, replacement: '$1 - $2' }, - { pattern: /(\w)\s*\*\s*(\w)/g, replacement: '$1 * $2' }, - { pattern: /(\w)\s*\/\s*(\w)/g, replacement: '$1 / $2' }, - { pattern: /(\w)\s*%\s*(\w)/g, replacement: '$1 % $2' }, - - // 比较操作符 - { pattern: /\s*-eq\s*/g, replacement: ' -eq ' }, - { pattern: /\s*-ne\s*/g, replacement: ' -ne ' }, - { pattern: /\s*-lt\s*/g, replacement: ' -lt ' }, - { pattern: /\s*-le\s*/g, replacement: ' -le ' }, - { pattern: /\s*-gt\s*/g, replacement: ' -gt ' }, - { pattern: /\s*-ge\s*/g, replacement: ' -ge ' }, - { pattern: /\s*-like\s*/g, replacement: ' -like ' }, - { pattern: /\s*-notlike\s*/g, replacement: ' -notlike ' }, - { pattern: /\s*-match\s*/g, replacement: ' -match ' }, - { pattern: /\s*-notmatch\s*/g, replacement: ' -notmatch ' }, - { pattern: /\s*-contains\s*/g, replacement: ' -contains ' }, - { pattern: /\s*-notcontains\s*/g, replacement: ' -notcontains ' }, - { pattern: /\s*-in\s*/g, replacement: ' -in ' }, - { pattern: /\s*-notin\s*/g, replacement: ' -notin ' }, - - // 逻辑操作符 - { pattern: /\s*-and\s*/g, replacement: ' -and ' }, - { pattern: /\s*-or\s*/g, replacement: ' -or ' }, - { pattern: /\s*-not\s*/g, replacement: ' -not ' }, - { pattern: /\s*-xor\s*/g, replacement: ' -xor ' }, - ]; - - let result = line; - - // 先保护字符串字面量 - const strings: string[] = []; - let stringIndex = 0; - - // 保护双引号字符串 - result = result.replace(/"([^"\\]*(\\.[^"\\]*)*)"/g, (match) => { - const placeholder = `__STRING_${stringIndex++}__`; - strings.push(match); - return placeholder; - }); - - // 保护单引号字符串 - result = result.replace(/'([^'\\]*(\\.[^'\\]*)*)'/g, (match) => { - const placeholder = `__STRING_${stringIndex++}__`; - strings.push(match); - return placeholder; - }); - - // 应用操作符格式化 - operatorMappings.forEach(({ pattern, replacement }) => { - result = result.replace(pattern, replacement); - }); - - // 清理多余的空格,但不要合并所有空格 - result = result.replace(/\s{2,}/g, ' '); - - // 还原字符串 - strings.forEach((str, index) => { - result = result.replace(`__STRING_${index}__`, str); - }); - - return result.trim(); - } - - private removeExtraBlankLines(lines: string[]): string[] { - const result: string[] = []; - let consecutiveEmptyLines = 0; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - const isEmptyLine = line.trim() === ''; - - if (isEmptyLine) { - consecutiveEmptyLines++; - // 只保留指定数量的连续空行 - if (consecutiveEmptyLines <= this.options.maxConsecutiveEmptyLines) { - result.push(line); - } - } else { - consecutiveEmptyLines = 0; - result.push(line); - } - } - - // 去除文件开头和结尾的空行 - while (result.length > 0 && result[0].trim() === '') { - result.shift(); - } - while (result.length > 0 && result[result.length - 1].trim() === '') { - result.pop(); - } - - return result; - } - - private formatPipelines(line: string): string { - // 格式化管道符 | - let result = line; - - // 保护字符串字面量 - const strings: string[] = []; - let stringIndex = 0; - - result = result.replace(/(["'])(?:(?=(\\?))\2.)*?\1/g, (match) => { - const placeholder = `__STRING_${stringIndex++}__`; - strings.push(match); - return placeholder; - }); - - // 格式化管道符,确保前后有空格 - result = result.replace(/\s*\|\s*/g, ' | '); - - // 还原字符串 - strings.forEach((str, index) => { - result = result.replace(`__STRING_${index}__`, str); - }); - - return result; - } - - private formatParentheses(line: string): string { - // 格式化括号内的空格 - let result = line; - - // 保护字符串字面量 - const strings: string[] = []; - let stringIndex = 0; - - result = result.replace(/(["'])(?:(?=(\\?))\2.)*?\1/g, (match) => { - const placeholder = `__STRING_${stringIndex++}__`; - strings.push(match); - return placeholder; - }); - - // 格式化括号:( 后和 ) 前不要多余空格,但参数之间要有空格 - result = result.replace(/\(\s+/g, '('); - result = result.replace(/\s+\)/g, ')'); - - // 格式化逗号:逗号后加空格 - result = result.replace(/,\s*/g, ', '); - - // 格式化分号:分号后加空格 - result = result.replace(/;\s*/g, '; '); - - // 还原字符串 - strings.forEach((str, index) => { - result = result.replace(`__STRING_${index}__`, str); - }); - - return result; - } - - private formatArraysAndHashtables(line: string): string { - // 格式化数组 @() 和哈希表 @{} - let result = line; - - // 保护字符串字面量 - const strings: string[] = []; - let stringIndex = 0; - - result = result.replace(/(["'])(?:(?=(\\?))\2.)*?\1/g, (match) => { - const placeholder = `__STRING_${stringIndex++}__`; - strings.push(match); - return placeholder; - }); - - // 格式化数组符号 - result = result.replace(/@\(\s*/g, '@('); - result = result.replace(/\s*\)/g, ')'); - - // 格式化哈希表符号 - result = result.replace(/@\{\s*/g, '@{'); - result = result.replace(/\s*\}/g, '}'); - - // 格式化方括号 - result = result.replace(/\[\s+/g, '['); - result = result.replace(/\s+\]/g, ']'); - - // 还原字符串 - strings.forEach((str, index) => { - result = result.replace(`__STRING_${index}__`, str); - }); - - return result; - } - - private addBlankLinesAroundBlocks(lines: string[]): string[] { - const result: string[] = []; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - const trimmedLine = line.trim(); - const previousLine = i > 0 ? lines[i - 1].trim() : ''; - const nextLine = i < lines.length - 1 ? lines[i + 1].trim() : ''; - - // 在函数定义前添加空行(除非是文件开头或前面已经有空行) - if (this.isFunctionDefinition(trimmedLine)) { - if (i > 0 && previousLine !== '' && result.length > 0) { - result.push(''); - } - } - - // 在控制结构前添加空行(if, while, for, foreach, switch等) - if (this.isControlStructure(trimmedLine)) { - if (i > 0 && previousLine !== '' && !this.isElseOrElseIf(trimmedLine) && result.length > 0) { - result.push(''); - } - } - - result.push(line); - - // 在函数定义后的右大括号后添加空行 - if (trimmedLine === '}' && this.isPreviousLineFunctionEnd(lines, i)) { - if (i < lines.length - 1 && nextLine !== '') { - result.push(''); - } - } - } - - return result; - } - - private formatFunctionDefinitions(line: string): string { - // 格式化函数定义 - let result = line; - - // 函数定义的模式匹配 - const functionPattern = /^(\s*)(function\s+)([^\s\(]+)(\s*)(\(.*?\))(\s*)(\{?)(.*)$/i; - const match = result.match(functionPattern); - - if (match) { - const [, indent, funcKeyword, funcName, , params, , openBrace, rest] = match; - - // 标准化函数定义格式:function FunctionName(Parameters) { - if (openBrace === '{' && rest.trim() === '') { - // 单独一行的开括号 - result = `${indent}${funcKeyword}${funcName}${params} {`; - } else if (openBrace === '' && rest.trim().startsWith('{')) { - // 括号在下一行,移到同一行 - result = `${indent}${funcKeyword}${funcName}${params} {`; - } else { - // 标准格式化 - result = `${indent}${funcKeyword}${funcName}${params} {`; - } - } - - return result; - } - - private formatLongLine(line: string): string { - // 处理长行,在适当的位置换行 - if (line.length <= this.options.printWidth) { - return line; - } - - const indent = line.match(/^\s*/)?.[0] || ''; - const content = line.trim(); - - // 查找可以换行的位置:管道符、逗号、操作符等 - const breakPoints = [ - { char: ' | ', priority: 1 }, - { char: ', ', priority: 2 }, - { char: ' -and ', priority: 3 }, - { char: ' -or ', priority: 3 }, - { char: ' = ', priority: 4 }, - { char: ' + ', priority: 5 }, - { char: ' -', priority: 6 } - ]; - - // 寻找最佳换行点 - for (const breakPoint of breakPoints) { - const index = content.lastIndexOf(breakPoint.char, this.options.printWidth - indent.length); - if (index > 0) { - const firstPart = content.substring(0, index + breakPoint.char.length).trim(); - const secondPart = content.substring(index + breakPoint.char.length).trim(); - - if (secondPart.length > 0) { - const continuationIndent = indent + ' '; // 额外缩进 - return `${indent}${firstPart}\n${continuationIndent}${secondPart}`; - } - } - } - - return line; // 如果找不到合适的断点,保持原样 - } - - private isFunctionDefinition(line: string): boolean { - return /^\s*function\s+\w+/i.test(line); - } - - private isControlStructure(line: string): boolean { - const controlKeywords = [ - /^\s*if\s*\(/i, - /^\s*while\s*\(/i, - /^\s*for\s*\(/i, - /^\s*foreach\s*\(/i, - /^\s*switch\s*\(/i, - /^\s*try\s*\{?$/i, - /^\s*do\s*\{?$/i - ]; - - return controlKeywords.some(pattern => pattern.test(line)); - } - - private isElseOrElseIf(line: string): boolean { - return /^\s*(else|elseif)\b/i.test(line); - } - - private isPreviousLineFunctionEnd(lines: string[], currentIndex: number): boolean { - // 检查是否是函数结束的大括号 - let braceCount = 0; - let foundFunction = false; - - for (let i = currentIndex - 1; i >= 0; i--) { - const line = lines[i].trim(); - - if (line.includes('}')) { - braceCount += (line.match(/\}/g) || []).length; - } - if (line.includes('{')) { - braceCount -= (line.match(/\{/g) || []).length; - } - - if (this.isFunctionDefinition(line)) { - foundFunction = true; - break; - } - - if (braceCount < 0) break; // 遇到不匹配的括号 - } - - return foundFunction && braceCount === 1; - } - - private formatPowerShellSyntax(line: string): string { - let result = line; - - // 格式化switch语句 - result = this.formatSwitchStatement(result); - - // 格式化try-catch-finally语句 - result = this.formatTryCatchFinally(result); - - // 格式化param块 - result = this.formatParamBlock(result); - - // 格式化PowerShell操作符和关键字 - result = this.formatPowerShellKeywords(result); - - // 格式化变量声明 - result = this.formatVariableDeclarations(result); - - return result; - } - - private formatSwitchStatement(line: string): string { - let result = line; - - // 格式化switch语句: switch ($variable) { - const switchPattern = /^(\s*)(switch)\s*(\(.*?\))\s*(\{?)(.*)$/i; - const match = result.match(switchPattern); - - if (match) { - const [, indent, switchKeyword, condition, brace, rest] = match; - result = `${indent}${switchKeyword} ${condition} {`; - } - - // 格式化switch case标签 - if (/^\s*\{?\s*[^}]*\s*\{\s*$/.test(result) || /^\s*default\s*\{\s*$/.test(result)) { - result = result.replace(/\s*\{\s*$/, ' {'); - } - - return result; - } - - private formatTryCatchFinally(line: string): string { - let result = line; - - // 格式化try语句 - if (/^\s*try\s*\{?/i.test(result)) { - result = result.replace(/^(\s*)(try)\s*\{?(.*)$/i, '$1$2 {$3'); - } - - // 格式化catch语句 - const catchPattern = /^(\s*)(catch)\s*(\[[^\]]*\])?\s*\{?(.*)$/i; - const catchMatch = result.match(catchPattern); - if (catchMatch) { - const [, indent, catchKeyword, exceptionType, rest] = catchMatch; - const exception = exceptionType || ''; - result = `${indent}${catchKeyword}${exception} {${rest}`; - } - - // 格式化finally语句 - if (/^\s*finally\s*\{?/i.test(result)) { - result = result.replace(/^(\s*)(finally)\s*\{?(.*)$/i, '$1$2 {$3'); - } - - return result; - } - - private formatParamBlock(line: string): string { - let result = line; - - // 格式化param块开始 - if (/^\s*param\s*\(/i.test(result)) { - result = result.replace(/^(\s*)(param)\s*\(/i, '$1$2('); - } - - // 格式化参数属性 - const paramAttributePattern = /^\s*(\[Parameter\([^\]]*\)\])\s*(.*)$/i; - const attrMatch = result.match(paramAttributePattern); - if (attrMatch) { - const [, attribute, rest] = attrMatch; - result = ` ${attribute}\n ${rest}`; - } - - return result; - } - - private formatPowerShellKeywords(line: string): string { - let result = line; - - // PowerShell关键字列表 - const keywords = [ - 'begin', 'process', 'end', 'filter', 'class', 'enum', - 'using', 'namespace', 'return', 'throw', 'break', 'continue', - 'exit', 'param', 'dynamicparam', 'workflow', 'configuration' - ]; - - // 确保关键字后有适当的空格 - keywords.forEach(keyword => { - const pattern = new RegExp(`\\b(${keyword})\\s*`, 'gi'); - result = result.replace(pattern, `$1 `); - }); - - // 特殊处理某些关键字 - result = result.replace(/\breturn\s+/gi, 'return '); - result = result.replace(/\bthrow\s+/gi, 'throw '); - - return result; - } - - private formatVariableDeclarations(line: string): string { - let result = line; - - // 格式化变量类型声明: [string]$variable = value - const typeVarPattern = /^(\s*)(\[[^\]]+\])\s*(\$\w+)\s*(=.*)?$/; - const match = result.match(typeVarPattern); - - if (match) { - const [, indent, type, variable, assignment] = match; - result = `${indent}${type}${variable}${assignment || ''}`; - } - - return result; - } - - private formatComment(line: string): string { - // 单行注释格式化 - if (line.includes('#') && !line.includes('<#') && !line.includes('#>')) { - const commentIndex = line.indexOf('#'); - const beforeComment = line.substring(0, commentIndex).trim(); - const comment = line.substring(commentIndex).trim(); - - if (beforeComment.length > 0) { - // 确保注释前有适当的空格 - return `${beforeComment} ${comment}`; - } - return comment; - } - return line; - } - - private shouldIncreaseIndent(line: string): boolean { - // PowerShell 块开始模式 - const blockStartPatterns = [ - /\{\s*$/, // 单独的大括号 - /\bif\s*\([^)]*\)\s*\{\s*$/i, - /\belseif\s*\([^)]*\)\s*\{\s*$/i, - /\belse\s*\{\s*$/i, - /\bwhile\s*\([^)]*\)\s*\{\s*$/i, - /\bfor\s*\([^)]*\)\s*\{\s*$/i, - /\bforeach\s*\([^)]*\)\s*\{\s*$/i, - /\bdo\s*\{\s*$/i, - /\btry\s*\{\s*$/i, - /\bcatch\s*(\[[^\]]*\])?\s*\{\s*$/i, - /\bfinally\s*\{\s*$/i, - /\bfunction\s+\w+.*\{\s*$/i, - ]; - - return blockStartPatterns.some(pattern => pattern.test(line)); - } - - private shouldDecreaseIndent(line: string): boolean { - // PowerShell 块结束模式 - const blockEndPatterns = [ - /^\s*\}\s*$/, // 单独的右大括号 - /^\s*\}\s*else/i, - /^\s*\}\s*elseif/i, - /^\s*\}\s*catch/i, - /^\s*\}\s*finally/i, - ]; - - return blockEndPatterns.some(pattern => pattern.test(line)); - } - - private getIndent(level: number): string { - if (level <= 0) return ''; - const indentChar = this.options.useTabsForIndentation ? '\t' : ' '; - const indentSize = this.options.useTabsForIndentation ? 1 : this.options.indentSize; - return indentChar.repeat(level * indentSize); - } -} - -/** - * 便捷的格式化函数 - */ -export function formatPowerShellCode( - code: string, - options?: PowerShellFormatterOptions -): string { - const formatter = new PowerShellFormatter(options); - return formatter.format(code); -} - -/** - * PowerShell代码解析器 - */ -const parser = (scriptContent: string): Promise => { - return new Promise((resolve) => { - try { - const astNode: PowerShellAstNode = { - type: 'ScriptBlockAst', - value: scriptContent, - start: 0, - end: scriptContent.length - }; - - resolve(astNode); - } catch (error) { - console.warn('PowerShell parsing fallback used:', error); - resolve({ - type: 'ScriptBlockAst', - value: scriptContent, - start: 0, - end: scriptContent.length - }); - } - }); -}; - -/** - * Prettier解析函数 - */ -const parse = async (scriptContent: string, parsers?: any, opts?: any): Promise => { - return await parser(scriptContent); -}; - -export default parse; \ No newline at end of file diff --git a/frontend/src/common/prettier/plugins/powershell/parser.ts b/frontend/src/common/prettier/plugins/powershell/parser.ts new file mode 100644 index 0000000..a4a7424 --- /dev/null +++ b/frontend/src/common/prettier/plugins/powershell/parser.ts @@ -0,0 +1,821 @@ +/** + * PowerShell 语法分析器 (Parser) + * 将词法分析器产生的tokens转换为抽象语法树(AST) + */ + +import { Token, TokenType } from './lexer'; +import { + ASTNode, + ScriptBlockAst, + StatementAst, + ExpressionAst, + PipelineAst, + CommandAst, + AssignmentAst, + VariableAst, + LiteralAst, + BinaryExpressionAst, + IfStatementAst, + FunctionDefinitionAst, + ParameterAst, + ASTNodeFactory, + CommentAst, + PipelineElementAst, + ElseIfClauseAst, + UnaryExpressionAst, + ParenthesizedExpressionAst +} from './ast'; + +export class PowerShellParser { + private tokens: Token[]; + private currentIndex: number = 0; + private comments: CommentAst[] = []; + + private originalCode: string; + + constructor(tokens: Token[], originalCode: string = '') { + this.tokens = tokens; + this.currentIndex = 0; + this.originalCode = originalCode; + } + + /** + * 解析tokens生成AST + */ + public parse(): ScriptBlockAst { + const statements: StatementAst[] = []; + + while (!this.isAtEnd()) { + // 跳过空白和换行 + this.skipWhitespaceAndNewlines(); + + if (this.isAtEnd()) { + break; + } + + // 处理注释 + if (this.match(TokenType.COMMENT, TokenType.MULTILINE_COMMENT)) { + const comment = this.parseComment(); + this.comments.push(comment); + continue; + } + + const statement = this.parseStatement(); + if (statement) { + statements.push(statement); + } + } + + const start = this.tokens.length > 0 ? this.tokens[0].startIndex : 0; + const end = this.tokens.length > 0 ? this.tokens[this.tokens.length - 1].endIndex : 0; + const line = this.tokens.length > 0 ? this.tokens[0].line : 1; + const column = this.tokens.length > 0 ? this.tokens[0].column : 1; + + return ASTNodeFactory.createScriptBlock(statements, start, end, line, column); + } + + public getComments(): CommentAst[] { + return this.comments; + } + + private parseStatement(): StatementAst | null { + // 函数定义 + if (this.check(TokenType.FUNCTION)) { + return this.parseFunctionDefinition(); + } + + // 控制流语句 + if (this.check(TokenType.IF)) { + return this.parseIfStatement(); + } + + // 赋值或管道 + return this.parsePipeline(); + } + + private parseFunctionDefinition(): FunctionDefinitionAst { + const start = this.current().startIndex; + const line = this.current().line; + const column = this.current().column; + + this.consume(TokenType.FUNCTION, "Expected 'function'"); + + // 函数名可能是CMDLET类型(如Get-Something)或IDENTIFIER + let nameToken: Token; + if (this.check(TokenType.CMDLET)) { + nameToken = this.consume(TokenType.CMDLET, "Expected function name"); + } else { + nameToken = this.consume(TokenType.IDENTIFIER, "Expected function name"); + } + const name = nameToken.value; + + // 解析参数 + const parameters: ParameterAst[] = []; + if (this.match(TokenType.LEFT_PAREN)) { + if (!this.check(TokenType.RIGHT_PAREN)) { + do { + const param = this.parseParameter(); + if (param) { + parameters.push(param); + } + } while (this.match(TokenType.COMMA)); + } + this.consume(TokenType.RIGHT_PAREN, "Expected ')' after parameters"); + } + + // 解析函数体 + const body = this.parseScriptBlock(); + + const end = this.previous().endIndex; + + return ASTNodeFactory.createFunctionDefinition(name, parameters, body, start, end, line, column); + } + + private parseIfStatement(): IfStatementAst { + const start = this.current().startIndex; + const line = this.current().line; + const column = this.current().column; + + this.consume(TokenType.IF, "Expected 'if'"); + + // PowerShell的if语句可能有括号,也可能没有 + const hasParens = this.check(TokenType.LEFT_PAREN); + if (hasParens) { + this.consume(TokenType.LEFT_PAREN, "Expected '(' after 'if'"); + } + + const condition = this.parseExpression(); + + if (hasParens) { + this.consume(TokenType.RIGHT_PAREN, "Expected ')' after if condition"); + } + + const ifBody = this.parseScriptBlock(); + + const elseIfClauses: ElseIfClauseAst[] = []; + let elseBody: ScriptBlockAst | undefined; + + // 处理 elseif 子句 + while (this.match(TokenType.ELSEIF)) { + const elseIfStart = this.previous().startIndex; + const elseIfLine = this.previous().line; + const elseIfColumn = this.previous().column; + + this.consume(TokenType.LEFT_PAREN, "Expected '(' after 'elseif'"); + const elseIfCondition = this.parseExpression(); + this.consume(TokenType.RIGHT_PAREN, "Expected ')' after elseif condition"); + const elseIfBody = this.parseScriptBlock(); + + const elseIfEnd = this.previous().endIndex; + + elseIfClauses.push({ + type: 'ElseIfClause', + condition: elseIfCondition, + body: elseIfBody, + start: elseIfStart, + end: elseIfEnd, + line: elseIfLine, + column: elseIfColumn + }); + } + + // 处理 else 子句 + if (this.match(TokenType.ELSE)) { + elseBody = this.parseScriptBlock(); + } + + const end = this.previous().endIndex; + + return ASTNodeFactory.createIfStatement(condition, ifBody, elseIfClauses, elseBody, start, end, line, column); + } + + private parsePipeline(): PipelineAst { + const start = this.current().startIndex; + const line = this.current().line; + const column = this.current().column; + + const elements: PipelineElementAst[] = []; + + // 解析第一个元素 + const firstElement = this.parsePipelineElement(); + elements.push(firstElement); + + // 解析管道链 + while (this.match(TokenType.PIPE)) { + const element = this.parsePipelineElement(); + elements.push(element); + } + + const end = this.previous().endIndex; + + return ASTNodeFactory.createPipeline(elements, start, end, line, column); + } + + private parsePipelineElement(): PipelineElementAst { + const start = this.current().startIndex; + const line = this.current().line; + const column = this.current().column; + + const expression = this.parseAssignment(); + const end = this.previous().endIndex; + + return { + type: 'PipelineElement', + expression, + start, + end, + line, + column + }; + } + + private parseAssignment(): ExpressionAst { + const expr = this.parseLogicalOr(); + + if (this.match(TokenType.ASSIGNMENT)) { + const operator = this.previous().value; + const right = this.parseAssignment(); + + return ASTNodeFactory.createAssignment( + expr, + operator, + right, + expr.start, + right.end, + expr.line, + expr.column + ); + } + + return expr; + } + + private parseLogicalOr(): ExpressionAst { + let expr = this.parseLogicalAnd(); + + while (this.match(TokenType.LOGICAL)) { + const operator = this.previous().value.toLowerCase(); + if (operator === '-or' || operator === '-xor') { + const right = this.parseLogicalAnd(); + expr = ASTNodeFactory.createBinaryExpression( + expr, + this.previous().value, // 使用原始大小写 + right, + expr.start, + right.end, + expr.line, + expr.column + ); + } else { + // 如果不是预期的操作符,回退 + this.currentIndex--; + break; + } + } + + return expr; + } + + private parseLogicalAnd(): ExpressionAst { + let expr = this.parseComparison(); + + while (this.match(TokenType.LOGICAL)) { + const operator = this.previous().value.toLowerCase(); + if (operator === '-and') { + const right = this.parseComparison(); + expr = ASTNodeFactory.createBinaryExpression( + expr, + this.previous().value, // 使用原始大小写 + right, + expr.start, + right.end, + expr.line, + expr.column + ); + } else { + // 如果不是预期的操作符,回退 + this.currentIndex--; + break; + } + } + + return expr; + } + + private parseComparison(): ExpressionAst { + let expr = this.parseArithmetic(); + + while (this.match(TokenType.COMPARISON)) { + const operator = this.previous().value; + const right = this.parseArithmetic(); + expr = ASTNodeFactory.createBinaryExpression( + expr, + operator, + right, + expr.start, + right.end, + expr.line, + expr.column + ); + } + + return expr; + } + + private parseArithmetic(): ExpressionAst { + let expr = this.parseMultiplicative(); + + while (this.match(TokenType.ARITHMETIC)) { + const token = this.previous(); + if (token.value === '+' || token.value === '-') { + const operator = token.value; + const right = this.parseMultiplicative(); + expr = ASTNodeFactory.createBinaryExpression( + expr, + operator, + right, + expr.start, + right.end, + expr.line, + expr.column + ); + } + } + + return expr; + } + + private parseMultiplicative(): ExpressionAst { + let expr = this.parseUnary(); + + while (this.match(TokenType.ARITHMETIC)) { + const token = this.previous(); + if (token.value === '*' || token.value === '/' || token.value === '%') { + const operator = token.value; + const right = this.parseUnary(); + expr = ASTNodeFactory.createBinaryExpression( + expr, + operator, + right, + expr.start, + right.end, + expr.line, + expr.column + ); + } + } + + return expr; + } + + private parseUnary(): ExpressionAst { + if (this.match(TokenType.LOGICAL)) { + const token = this.previous(); + const operator = token.value.toLowerCase(); + if (operator === '-not') { + const operand = this.parseUnary(); + return { + type: 'UnaryExpression', + operator: token.value, // 使用原始大小写 + operand, + start: token.startIndex, + end: operand.end, + line: token.line, + column: token.column + } as UnaryExpressionAst; + } else { + // 如果不是-not,回退token + this.currentIndex--; + } + } + + // 处理算术一元操作符(+, -) + if (this.match(TokenType.ARITHMETIC)) { + const token = this.previous(); + if (token.value === '+' || token.value === '-') { + const operand = this.parseUnary(); + return { + type: 'UnaryExpression', + operator: token.value, + operand, + start: token.startIndex, + end: operand.end, + line: token.line, + column: token.column + } as UnaryExpressionAst; + } else { + // 如果不是一元操作符,回退 + this.currentIndex--; + } + } + + return this.parsePrimary(); + } + + private parsePrimary(): ExpressionAst { + // 变量 + if (this.match(TokenType.VARIABLE)) { + const token = this.previous(); + return ASTNodeFactory.createVariable( + token.value, + token.startIndex, + token.endIndex, + token.line, + token.column + ); + } + + // 字符串字面量 + if (this.match(TokenType.STRING, TokenType.HERE_STRING)) { + const token = this.previous(); + return ASTNodeFactory.createLiteral( + token.value, + 'String', + token.startIndex, + token.endIndex, + token.line, + token.column + ); + } + + // 数字字面量 + if (this.match(TokenType.NUMBER)) { + const token = this.previous(); + const value = parseFloat(token.value); + return ASTNodeFactory.createLiteral( + value, + 'Number', + token.startIndex, + token.endIndex, + token.line, + token.column + ); + } + + // 命令调用 - 扩展支持更多token类型 + if (this.match(TokenType.CMDLET, TokenType.IDENTIFIER)) { + return this.parseCommand(); + } + + // 处理看起来像cmdlet但被错误标记的标识符 + if (this.check(TokenType.IDENTIFIER) && this.current().value.includes('-')) { + this.advance(); + return this.parseCommand(); + } + + // 哈希表 @{...} + if (this.check(TokenType.LEFT_BRACE) && this.current().value === '@{') { + return this.parseHashtable(); + } + + // 脚本块表达式 {...} - 已在parseHashtableValue中处理 + // 这里不需要处理,因为独立的脚本块很少见 + + // 括号表达式 + if (this.match(TokenType.LEFT_PAREN)) { + const expr = this.parseExpression(); + this.consume(TokenType.RIGHT_PAREN, "Expected ')' after expression"); + return { + type: 'ParenthesizedExpression', + expression: expr, + start: this.previous().startIndex, + end: this.previous().endIndex, + line: this.previous().line, + column: this.previous().column + } as ParenthesizedExpressionAst; + } + + // 对于不认识的token,作为普通标识符处理而不是抛出异常 + const token = this.advance(); + return ASTNodeFactory.createLiteral( + token.value, + 'String', // 将未识别的token作为字符串处理 + token.startIndex, + token.endIndex, + token.line, + token.column + ); + } + + private parseCommand(): CommandAst { + const start = this.previous().startIndex; + const line = this.previous().line; + const column = this.previous().column; + const commandName = this.previous().value; + + const parameters: ParameterAst[] = []; + const args: ExpressionAst[] = []; + + // 解析参数和参数值 + while (!this.isAtEnd() && + !this.check(TokenType.PIPE) && + !this.check(TokenType.NEWLINE) && + !this.check(TokenType.SEMICOLON) && + !this.check(TokenType.RIGHT_PAREN) && + !this.check(TokenType.RIGHT_BRACE)) { + + if (this.match(TokenType.PARAMETER)) { + const paramToken = this.previous(); + const param: ParameterAst = { + type: 'Parameter', + name: paramToken.value, + start: paramToken.startIndex, + end: paramToken.endIndex, + line: paramToken.line, + column: paramToken.column + }; + + // 检查参数是否有值 + if (!this.check(TokenType.PARAMETER) && + !this.check(TokenType.PIPE) && + !this.check(TokenType.NEWLINE) && + !this.check(TokenType.SEMICOLON)) { + param.value = this.parsePrimary(); + } + + parameters.push(param); + } else { + // 位置参数 + const arg = this.parsePrimary(); + args.push(arg); + } + } + + const end = this.previous().endIndex; + + return ASTNodeFactory.createCommand(commandName, parameters, args, start, end, line, column); + } + + private parseParameter(): ParameterAst | null { + if (this.match(TokenType.PARAMETER)) { + const token = this.previous(); + const param: ParameterAst = { + type: 'Parameter', + name: token.value, + start: token.startIndex, + end: token.endIndex, + line: token.line, + column: token.column + }; + + // 检查是否有参数值 + if (this.match(TokenType.ASSIGNMENT)) { + param.value = this.parseExpression(); + } + + return param; + } + return null; + } + + private parseScriptBlock(): ScriptBlockAst { + const start = this.current().startIndex; + const line = this.current().line; + const column = this.current().column; + + this.consume(TokenType.LEFT_BRACE, "Expected '{'"); + + const statements: StatementAst[] = []; + + while (!this.check(TokenType.RIGHT_BRACE) && !this.isAtEnd()) { + this.skipWhitespaceAndNewlines(); + + if (this.check(TokenType.RIGHT_BRACE)) { + break; + } + + const statement = this.parseStatement(); + if (statement) { + statements.push(statement); + } + } + + this.consume(TokenType.RIGHT_BRACE, "Expected '}'"); + + const end = this.previous().endIndex; + + return ASTNodeFactory.createScriptBlock(statements, start, end, line, column); + } + + private parseExpression(): ExpressionAst { + return this.parseAssignment(); + } + + private parseComment(): CommentAst { + const token = this.previous(); + const isMultiline = token.type === TokenType.MULTILINE_COMMENT; + + return ASTNodeFactory.createComment( + token.value, + isMultiline, + token.startIndex, + token.endIndex, + token.line, + token.column + ); + } + + // 辅助方法 + private match(...types: TokenType[]): boolean { + for (const type of types) { + if (this.check(type)) { + this.advance(); + return true; + } + } + return false; + } + + private check(type: TokenType): boolean { + if (this.isAtEnd()) return false; + return this.current().type === type; + } + + private advance(): Token { + if (!this.isAtEnd()) this.currentIndex++; + return this.previous(); + } + + private isAtEnd(): boolean { + return this.currentIndex >= this.tokens.length || this.current().type === TokenType.EOF; + } + + private current(): Token { + if (this.currentIndex >= this.tokens.length) { + return this.tokens[this.tokens.length - 1]; + } + return this.tokens[this.currentIndex]; + } + + private previous(): Token { + return this.tokens[this.currentIndex - 1]; + } + + private consume(type: TokenType, message: string): Token { + if (this.check(type)) return this.advance(); + + const current = this.current(); + throw new Error(`${message}. Got ${current.type}(${current.value}) at line ${current.line}, column ${current.column}`); + } + + + private parseHashtable(): ExpressionAst { + const start = this.current().startIndex; + const line = this.current().line; + const column = this.current().column; + + // 消费 @{ + this.advance(); + + const entries: any[] = []; + + // 解析哈希表内容 + if (!this.check(TokenType.RIGHT_BRACE)) { + do { + // 解析键 - 只接受简单的标识符或字符串 + const key = this.parseHashtableKey(); + + // 消费 = + this.consume(TokenType.ASSIGNMENT, "Expected '=' after hashtable key"); + + // 解析值 + const value = this.parseHashtableValue(); + + entries.push({ + type: 'HashtableEntry', + key, + value, + start: key.start, + end: value.end, + line: key.line, + column: key.column + }); + + } while (this.match(TokenType.SEMICOLON)); + } + + this.consume(TokenType.RIGHT_BRACE, "Expected '}' after hashtable entries"); + const end = this.previous().endIndex; + + return { + type: 'Hashtable', + entries, + start, + end, + line, + column + } as any; + } + + private parseHashtableKey(): ExpressionAst { + // 哈希表键只能是简单的标识符或字符串 + if (this.match(TokenType.STRING, TokenType.HERE_STRING)) { + const token = this.previous(); + return ASTNodeFactory.createLiteral( + token.value, + 'String', + token.startIndex, + token.endIndex, + token.line, + token.column + ); + } + + // 接受各种可能的标识符类型作为哈希表键 + if (this.match(TokenType.IDENTIFIER, TokenType.CMDLET, TokenType.KEYWORD)) { + const token = this.previous(); + return ASTNodeFactory.createLiteral( + token.value, + 'String', + token.startIndex, + token.endIndex, + token.line, + token.column + ); + } + + // 对于任何其他类型的token,尝试作为字面量处理 + const currentToken = this.current(); + this.advance(); + return ASTNodeFactory.createLiteral( + currentToken.value, + 'String', + currentToken.startIndex, + currentToken.endIndex, + currentToken.line, + currentToken.column + ); + } + + private parseHashtableValue(): ExpressionAst { + // 哈希表值可以是任何表达式 + if (this.check(TokenType.LEFT_BRACE)) { + // 这是一个脚本块 {expression} - 完全绕过复杂解析 + const start = this.current().startIndex; + const line = this.current().line; + const column = this.current().column; + + // 直接从原始代码中提取脚本块内容 + const startPos = this.current().startIndex; + this.advance(); // 消费 { + + let braceLevel = 1; + let endPos = this.current().startIndex; + + // 找到匹配的右大括号位置 + while (!this.isAtEnd() && braceLevel > 0) { + const token = this.current(); + if (token.type === TokenType.LEFT_BRACE) { + braceLevel++; + } else if (token.type === TokenType.RIGHT_BRACE) { + braceLevel--; + if (braceLevel === 0) { + endPos = token.startIndex; + break; + } + } + this.advance(); + } + + this.consume(TokenType.RIGHT_BRACE, "Expected '}' after script block"); + const end = this.previous().endIndex; + + // 从原始代码中提取内容(从 { 后到 } 前) + const rawContent = this.getOriginalCodeSlice(startPos + 1, endPos); + + return { + type: 'ScriptBlockExpression', + rawContent: rawContent.trim(), // 去掉首尾空白 + start, + end, + line, + column + } as any; + } + + // 对于其他值,使用简单的解析 + return this.parsePrimary(); + } + + private getOriginalCodeSlice(start: number, end: number): string { + // 直接从原始代码中提取片段 + if (this.originalCode) { + return this.originalCode.substring(start, end); + } + + // 回退到基于token重建(如果没有原始代码) + let result = ''; + for (const token of this.tokens) { + if (token.startIndex >= start && token.endIndex <= end) { + result += token.value; + } + } + return result; + } + + private skipWhitespaceAndNewlines(): void { + while (this.match(TokenType.WHITESPACE, TokenType.NEWLINE)) { + // 继续跳过 + } + } +} diff --git a/frontend/src/common/prettier/plugins/toml/index.ts b/frontend/src/common/prettier/plugins/toml/index.ts new file mode 100644 index 0000000..b9be469 --- /dev/null +++ b/frontend/src/common/prettier/plugins/toml/index.ts @@ -0,0 +1,72 @@ +/** + * Prettier Plugin for TOML file formatting + * + * This plugin provides support for formatting TOML (Tom's Obvious, Minimal Language) files + * using the @toml-tools/parser and custom beautifier. + */ + +import type { Plugin, Parser, Printer, SupportLanguage, SupportOption } from 'prettier'; +import { parse } from '@toml-tools/parser'; +import { locStart, locEnd } from './loc'; +import { print } from './printer'; +import type { TomlDocument, TomlCstNode } from './types'; + +const parserName = 'toml'; + +// https://prettier.io/docs/en/plugins.html#languages +const languages: SupportLanguage[] = [ + { + extensions: ['.toml'], + name: 'Toml', + parsers: [parserName], + filenames: ['Cargo.lock', 'Gopkg.lock'], + tmScope: 'source.toml', + aceMode: 'toml', + codemirrorMode: 'toml', + codemirrorMimeType: 'text/x-toml', + linguistLanguageId: 365, + vscodeLanguageIds: ['toml'], + }, +]; + +// https://prettier.io/docs/en/plugins.html#parsers +const tomlParser: Parser = { + astFormat: 'toml-cst', + parse: (text: string): TomlDocument => { + try { + return parse(text) as TomlDocument; + } catch (error) { + console.error('TOML parsing error:', error); + throw error; + } + }, + locStart, + locEnd, +}; + +// https://prettier.io/docs/en/plugins.html#printers +const tomlPrinter: Printer = { + print, +}; + +// Plugin options +const options: Record = { + +}; + +// Plugin definition +const tomlPlugin: Plugin = { + languages, + parsers: { + [parserName]: tomlParser, + }, + printers: { + 'toml-cst': tomlPrinter, + }, + options, +}; + +export default tomlPlugin; +export { languages }; +export const parsers = tomlPlugin.parsers; +export const printers = tomlPlugin.printers; diff --git a/frontend/src/common/prettier/plugins/toml/loc.ts b/frontend/src/common/prettier/plugins/toml/loc.ts new file mode 100644 index 0000000..8d1eae7 --- /dev/null +++ b/frontend/src/common/prettier/plugins/toml/loc.ts @@ -0,0 +1,82 @@ +/** + * Location utilities for TOML CST nodes + * These functions help Prettier determine the location of nodes for formatting + */ + +import type { TomlCstNode } from './types'; + +/** + * Get the start location of a CST node + * @param cstNode - The TOML CST node + * @returns The start offset of the node + */ +export function locStart(cstNode: TomlCstNode): number { + if (!cstNode) { + return 0; + } + + // If the node has a direct startOffset, use it + if (typeof cstNode.startOffset === 'number') { + return cstNode.startOffset; + } + + // If the node has children, find the earliest start offset + if (cstNode.children) { + let minOffset = Infinity; + for (const key in cstNode.children) { + const childrenArray = cstNode.children[key]; + if (Array.isArray(childrenArray)) { + for (const child of childrenArray) { + const childStart = locStart(child); + if (childStart < minOffset) { + minOffset = childStart; + } + } + } + } + return minOffset === Infinity ? 0 : minOffset; + } + + return 0; +} + +/** + * Get the end location of a CST node + * @param cstNode - The TOML CST node + * @returns The end offset of the node + */ +export function locEnd(cstNode: TomlCstNode): number { + if (!cstNode) { + return 0; + } + + // If the node has a direct endOffset, use it + if (typeof cstNode.endOffset === 'number') { + return cstNode.endOffset; + } + + // If the node has children, find the latest end offset + if (cstNode.children) { + let maxOffset = -1; + for (const key in cstNode.children) { + const childrenArray = cstNode.children[key]; + if (Array.isArray(childrenArray)) { + for (const child of childrenArray) { + const childEnd = locEnd(child); + if (childEnd > maxOffset) { + maxOffset = childEnd; + } + } + } + } + return maxOffset === -1 ? 0 : maxOffset; + } + + // If the node has an image (token), return the length + if (cstNode.image) { + const startOffset = locStart(cstNode); + return startOffset + cstNode.image.length; + } + + return 0; +} diff --git a/frontend/src/common/prettier/plugins/toml/printer-utils.ts b/frontend/src/common/prettier/plugins/toml/printer-utils.ts new file mode 100644 index 0000000..e00d38c --- /dev/null +++ b/frontend/src/common/prettier/plugins/toml/printer-utils.ts @@ -0,0 +1,284 @@ +/** + * Utility functions for TOML printer + */ + +import type { TomlCstNode, TomlComment, TomlContext } from './types'; + +/** + * Trim trailing whitespace from comment text + * @param commentText - The comment text to trim + * @returns Trimmed comment text + */ +export function trimComment(commentText: string): string { + return commentText.replace(/[ \t]+$/, ''); +} + +/** + * Check if a quoted string can be unquoted + * @param quotedText - The quoted text to check + * @returns Whether the text can be unquoted + */ +export function canUnquote(quotedText: string): boolean { + // Remove quotes if present + let text = quotedText; + if (text.startsWith('"') && text.endsWith('"')) { + text = text.slice(1, -1); + } else if (text.startsWith("'") && text.endsWith("'")) { + text = text.slice(1, -1); + } + + // Empty string needs quotes + if (text.length === 0) { + return false; + } + + // Check if the string is a valid unquoted key + // TOML unquoted keys can contain: + // - A-Z, a-z, 0-9, _, - + const unquotedKeyRegex = /^[A-Za-z0-9_-]+$/; + + // Additional checks for values that might be confused with other TOML types + if (unquotedKeyRegex.test(text)) { + // Don't unquote strings that look like booleans + if (text === 'true' || text === 'false') { + return false; + } + + // Don't unquote strings that look like numbers + if (/^[+-]?(\d+\.?\d*|\d*\.\d+)([eE][+-]?\d+)?$/.test(text)) { + return false; + } + + // Don't unquote strings that look like dates/times + if (/^\d{4}-\d{2}-\d{2}/.test(text)) { + return false; + } + + return true; + } + + return false; +} + +/** + * Check if a key needs quotes + * @param keyText - The key text to check + * @returns Whether the key needs quotes + */ +export function keyNeedsQuotes(keyText: string): boolean { + return !canUnquote(`"${keyText}"`); +} + +/** + * Format a key, adding or removing quotes as needed + * @param keyText - The key text to format + * @returns Formatted key + */ +export function formatKey(keyText: string): string { + // If already quoted, check if we can unquote + if ((keyText.startsWith('"') && keyText.endsWith('"')) || + (keyText.startsWith("'") && keyText.endsWith("'"))) { + if (canUnquote(keyText)) { + return keyText.slice(1, -1); + } + return keyText; + } + + // If not quoted, check if we need to add quotes + if (keyNeedsQuotes(keyText)) { + return `"${keyText}"`; + } + + return keyText; +} + +/** + * Check if a string contains escape sequences that need to be preserved + * @param str - The string to check + * @returns Whether the string contains escape sequences + */ +export function containsEscapeSequences(str: string): boolean { + // Check for common escape sequences + return /\\[btnfr"\\\/]|\\u[0-9a-fA-F]{4}|\\U[0-9a-fA-F]{8}/.test(str); +} + +/** + * Check if a string can use literal string syntax (single quotes) + * @param str - The string to check (without quotes) + * @returns Whether literal string syntax can be used + */ +export function canUseLiteralString(str: string): boolean { + // Literal strings cannot contain single quotes or control characters + // and don't need escape sequences + return !str.includes("'") && + !/[\x00-\x08\x0A-\x1F\x7F]/.test(str) && + !containsEscapeSequences(str); +} + +/** + * Check if a string should use multiline syntax + * @param str - The string to check (without quotes) + * @returns Whether multiline syntax should be used + */ +export function shouldUseMultiline(str: string): boolean { + // Use multiline for strings that contain newlines + return str.includes('\n') || str.includes('\r'); +} + +/** + * Format a string value optimally + * @param value - The string value (potentially with quotes) + * @returns Optimally formatted string + */ +export function formatStringValue(value: string): string { + // If it's already a properly formatted string, keep it + if (!value.startsWith('"') && !value.startsWith("'")) { + return value; + } + + // Extract the actual string content + let content: string; + let isLiteral = false; + + if (value.startsWith('"""') && value.endsWith('"""')) { + // Multiline basic string + content = value.slice(3, -3); + } else if (value.startsWith("'''") && value.endsWith("'''")) { + // Multiline literal string + content = value.slice(3, -3); + isLiteral = true; + } else if (value.startsWith('"') && value.endsWith('"')) { + // Basic string + content = value.slice(1, -1); + } else if (value.startsWith("'") && value.endsWith("'")) { + // Literal string + content = value.slice(1, -1); + isLiteral = true; + } else { + return value; // Fallback + } + + // Decide on the best format + if (shouldUseMultiline(content)) { + if (isLiteral || !containsEscapeSequences(content)) { + // Use multiline literal string if no escapes needed + return `'''${content}'''`; + } else { + // Use multiline basic string + return `"""${content}"""`; + } + } else { + if (canUseLiteralString(content) && !containsEscapeSequences(content)) { + // Use literal string for simple cases + return `'${content}'`; + } else { + // Use basic string + return `"${content}"`; + } + } +} + +/** + * Optimize value representation (for strings, numbers, etc.) + * @param value - The value to optimize + * @returns Optimized value representation + */ +export function optimizeValue(value: string): string { + // Handle string values + if (value.startsWith('"') || value.startsWith("'")) { + return formatStringValue(value); + } + + // For non-strings, return as-is + return value; +} + +/** + * Collect all comments from comment newline nodes + * @param commentsNL - Array of comment newline nodes + * @returns Array of comment tokens + */ +export function collectComments(commentsNL: TomlCstNode[] = []): TomlComment[] { + const comments: TomlComment[] = []; + + commentsNL.forEach((commentNLNode) => { + if (commentNLNode.children?.Comment) { + const commentsTok = commentNLNode.children.Comment; + for (const comment of commentsTok) { + if (comment.image) { + comments.push(comment as TomlComment); + } + } + } + }); + + return comments; +} + +/** + * Get a single element from a context that should contain exactly one key-value pair + * @param ctx - The context to extract from + * @returns The single element + * @throws Error if the context doesn't contain exactly one element + */ +export function getSingle(ctx: TomlContext): TomlCstNode { + const ctxKeys = Object.keys(ctx); + if (ctxKeys.length !== 1) { + throw new Error( + `Expecting single key CST ctx but found: <${ctxKeys.length}> keys` + ); + } + + const singleElementKey = ctxKeys[0]; + const singleElementValues = ctx[singleElementKey]; + + if (!Array.isArray(singleElementValues) || singleElementValues.length !== 1) { + throw new Error( + `Expecting single item in CST ctx key but found: <${singleElementValues?.length || 0}> items` + ); + } + + return singleElementValues[0]; +} + +/** + * Get the start offset of an array item (deprecated - use arrItemProp instead) + * @param item - The array item node + * @returns The start offset + */ +export function arrItemOffset(item: TomlCstNode): number { + return arrItemProp(item, 'startOffset') as number; +} + +/** + * Get a specific property from an array item, handling wrapped values + * @param item - The array item node + * @param propName - The property name to retrieve + * @returns The property value + * @throws Error for non-exhaustive matches + */ +export function arrItemProp(item: TomlCstNode, propName: keyof TomlCstNode): any { + let currentItem = item; + + // Unwrap 'val' nodes + if (currentItem.name === 'val' && currentItem.children) { + currentItem = getSingle(currentItem.children); + } + + // Direct property access + if (currentItem[propName] !== undefined) { + return currentItem[propName]; + } + + // Check for LSquare (array start) + if (currentItem.children?.LSquare?.[0]?.[propName] !== undefined) { + return currentItem.children.LSquare[0][propName]; + } + + // Check for LCurly (inline table start) + if (currentItem.children?.LCurly?.[0]?.[propName] !== undefined) { + return currentItem.children.LCurly[0][propName]; + } + + throw new Error(`Non-exhaustive match for property ${propName}`); +} diff --git a/frontend/src/common/prettier/plugins/toml/printer.ts b/frontend/src/common/prettier/plugins/toml/printer.ts new file mode 100644 index 0000000..381b39a --- /dev/null +++ b/frontend/src/common/prettier/plugins/toml/printer.ts @@ -0,0 +1,413 @@ +/** + * TOML Printer for Prettier + * + * This module provides a visitor-based printer for TOML CST nodes, + * converting them to Prettier's document format. + */ + +import { BaseTomlCstVisitor } from '@toml-tools/parser'; +import { tokensDictionary as t } from '@toml-tools/lexer'; +import { doc } from 'prettier'; +import type { AstPath, Doc } from 'prettier'; +import { + trimComment, + collectComments, + arrItemOffset, + arrItemProp, + getSingle, + formatKey, + optimizeValue, +} from './printer-utils'; +import type { + TomlCstNode, + TomlDocument, + TomlExpression, + TomlKeyVal, + TomlComment, + TomlContext +} from './types'; + +const { join, line, hardline, softline, ifBreak, indent, group } = doc.builders; + +/** + * TOML Beautifier Visitor class that extends the base CST visitor + */ +class TomlBeautifierVisitor extends BaseTomlCstVisitor { + // Helper methods + public mapVisit: (elements: TomlCstNode[] | undefined) => (Doc | string)[]; + public visitSingle: (ctx: TomlContext) => Doc | string; + public visit: (ctx: TomlCstNode, inParam?: any) => Doc | string; + + constructor() { + super(); + + // Try to call validateVisitor if it exists + if (typeof (this as any).validateVisitor === 'function') { + (this as any).validateVisitor(); + } + + // Initialize helper methods + this.mapVisit = (elements: TomlCstNode[] | undefined): (Doc | string)[] => { + if (!elements) { + return []; + } + return elements.map((element) => this.visit(element)); + }; + + this.visitSingle = (ctx: TomlContext): Doc | string => { + const singleElement = getSingle(ctx); + return this.visit(singleElement); + }; + + // Store reference to inherited visit method and override it + const originalVisit = Object.getPrototypeOf(this).visit?.bind(this); + this.visit = (ctx: TomlCstNode, inParam?: any): Doc | string => { + if (!ctx) { + return ''; + } + + // Try to use the inherited visit method first + if (originalVisit) { + try { + return originalVisit(ctx, inParam); + } catch (error) { + console.warn('Original visit method failed:', error); + } + } + + // Fallback: manually dispatch based on node name/type + const methodName = ctx.name; + if (methodName && typeof (this as any)[methodName] === 'function') { + const visitMethod = (this as any)[methodName]; + try { + if (ctx.children) { + return visitMethod.call(this, ctx.children); + } else { + return visitMethod.call(this, ctx); + } + } catch (error) { + console.warn(`Visit method ${methodName} failed:`, error); + } + } + + // Final fallback: return image if available + return ctx.image || ''; + }; + } + + /** + * Visit the root TOML document + */ + toml(ctx: TomlDocument): Doc { + // Handle empty toml document + if (!ctx.expression) { + return [line]; + } + + const isTable = (node: TomlExpression): boolean => { + return !!node.table; + }; + + const isOnlyComment = (node: TomlExpression): boolean => { + return !!node.Comment && Object.keys(node).length === 1; + }; + + const expsCsts = ctx.expression; + const cstGroups: TomlExpression[][] = []; + let currCstGroup: TomlExpression[] = []; + + // Split expressions into groups defined by tables + for (let i = expsCsts.length - 1; i >= 0; i--) { + const currCstNode = expsCsts[i]; + currCstGroup.push(currCstNode); + + if (isTable(currCstNode)) { + let j = i - 1; + let stillInComments = true; + + // Add leading comments to current group + while (j >= 0 && stillInComments) { + const priorCstNode = expsCsts[j]; + if (isOnlyComment(priorCstNode)) { + currCstGroup.push(priorCstNode); + j--; + i--; + } else { + stillInComments = false; + } + } + + // Reverse since we scanned backwards + currCstGroup.reverse(); + cstGroups.push(currCstGroup); + currCstGroup = []; + } + } + + if (currCstGroup.length > 0) { + currCstGroup.reverse(); + cstGroups.push(currCstGroup); + } + + // Adjust for reverse scanning + cstGroups.reverse(); + const docGroups = cstGroups.map((currGroup) => this.mapVisit(currGroup)); + + // Add newlines between group elements + const docGroupsInnerNewlines = docGroups.map((currGroup) => + join(line, currGroup) + ); + const docGroupsOuterNewlines = join([line, line], docGroupsInnerNewlines); + + return [docGroupsOuterNewlines, line]; + } + + /** + * Visit an expression (keyval, table, or comment) + */ + expression(ctx: TomlExpression): Doc | string { + if (ctx.keyval) { + let keyValDoc = this.visit(ctx.keyval[0]); + if (ctx.Comment) { + const commentText = trimComment(ctx.Comment[0].image); + keyValDoc = [keyValDoc, ' ' + commentText]; + } + return keyValDoc; + } else if (ctx.table) { + let tableDoc = this.visit(ctx.table[0]); + if (ctx.Comment) { + const commentText = trimComment(ctx.Comment[0].image); + tableDoc = [tableDoc, ' ' + commentText]; + } + return tableDoc; + } else if (ctx.Comment) { + return trimComment(ctx.Comment[0].image); + } + + return ''; + } + + /** + * Visit a key-value pair + */ + keyval(ctx: TomlKeyVal): Doc { + const keyDoc = this.visit(ctx.key[0]); + const valueDoc = this.visit(ctx.val[0]); + return [keyDoc, ' = ', valueDoc]; + } + + /** + * Visit a key + */ + key(ctx: any): Doc { + const keyTexts = ctx.IKey?.map((tok: any) => { + const keyText = tok.image; + // Apply key formatting (add/remove quotes as needed) + return formatKey(keyText); + }) || []; + + return join('.', keyTexts); + } + + /** + * Visit a value + */ + val(ctx: any): Doc | string { + try { + const actualValueNode = getSingle(ctx); + if (actualValueNode.image !== undefined) { + // Terminal token - 优化值的表示 + return optimizeValue(actualValueNode.image); + } else { + return this.visit(actualValueNode); + } + } catch (error) { + // 如果getSingle失败,尝试直接处理children + if (ctx.children) { + // 处理不同类型的值 + for (const [childKey, childNodes] of Object.entries(ctx.children)) { + if (Array.isArray(childNodes) && childNodes.length > 0) { + const firstChild = childNodes[0]; + + // 处理基本类型 + if (firstChild.image !== undefined) { + // 优化值的表示(特别是字符串) + return optimizeValue(firstChild.image); + } + + // 处理复杂类型(如数组、内联表等) + if (firstChild.name) { + return this.visit(firstChild); + } + } + } + } + + return ''; + } + } + + /** + * Visit an array + */ + array(ctx: any): Doc { + const arrayValuesDocs = ctx.arrayValues ? this.visit(ctx.arrayValues) : ''; + const postComments = collectComments(ctx.commentNewline); + const commentsDocs = postComments.map((commentTok) => { + const trimmedCommentText = trimComment(commentTok.image); + return [hardline, trimmedCommentText]; + }); + + return group(['[', indent([arrayValuesDocs, commentsDocs]), softline, ']']); + } + + /** + * Visit array values + */ + arrayValues(ctx: any): Doc { + const values = ctx.val || []; + const commas = ctx.Comma || []; + const comments = collectComments(ctx.commentNewline); + + const itemsCst = [...values, ...commas, ...comments]; + itemsCst.sort((a, b) => { + const aOffset = arrItemOffset(a); + const bOffset = arrItemOffset(b); + return aOffset - bOffset; + }); + + const itemsDoc: Doc[] = []; + + for (let i = 0; i < itemsCst.length; i++) { + const cstItem = itemsCst[i]; + + if (cstItem.name === 'val') { + const valDoc = this.visit(cstItem); + const valEndLine = arrItemProp(cstItem, 'endLine'); + let potentialComma = ''; + + // Handle next item (comma or comment) + if (itemsCst[i + 1]) { + let nextPossibleComment = itemsCst[i + 1]; + + // Skip commas + if (nextPossibleComment.tokenType === t.Comma) { + potentialComma = ','; + i++; + nextPossibleComment = itemsCst[i + 1]; + } + + // Handle same-line comments + if ( + nextPossibleComment && + nextPossibleComment.tokenType === t.Comment && + nextPossibleComment.startLine === valEndLine + ) { + i++; + const trimmedComment = trimComment(nextPossibleComment.image); + const comment = ' ' + trimmedComment; + itemsDoc.push([valDoc, potentialComma, comment, hardline]); + } else { + // No comment on same line + const isTrailingComma = i === itemsCst.length - 1; + const optionalCommaLineBreak = isTrailingComma + ? ifBreak(',', '') // Only print trailing comma if multiline array + : [potentialComma, line]; + itemsDoc.push([valDoc, optionalCommaLineBreak]); + } + } else { + // Last item without followup + itemsDoc.push([valDoc]); + } + } else if (cstItem.tokenType === t.Comment) { + // Separate line comment + const trimmedComment = trimComment(cstItem.image); + itemsDoc.push([trimmedComment, hardline]); + } else { + throw new Error('Non-exhaustive match in arrayValues'); + } + } + + return [softline, itemsDoc]; + } + + /** + * Visit an inline table + */ + inlineTable(ctx: any): Doc { + const inlineTableKeyValsDocs = ctx.inlineTableKeyVals + ? this.visit(ctx.inlineTableKeyVals) + : ''; + return group(['{ ', inlineTableKeyValsDocs, ' }']); + } + + /** + * Visit inline table key-value pairs + */ + inlineTableKeyVals(ctx: any): Doc { + const keyValDocs = this.mapVisit(ctx.keyval); + return join(', ', keyValDocs); + } + + /** + * Visit a table + */ + table(ctx: any): Doc | string { + return this.visitSingle(ctx); + } + + /** + * Visit a standard table + */ + stdTable(ctx: any): Doc { + if (ctx.key && ctx.key[0] && ctx.key[0].children && ctx.key[0].children.IKey) { + const keyTexts = ctx.key[0].children.IKey.map((tok: any) => { + return formatKey(tok.image); + }); + return ['[', join('.', keyTexts), ']']; + } + return '[]'; + } + + /** + * Visit an array table + */ + arrayTable(ctx: any): Doc { + if (ctx.key && ctx.key[0] && ctx.key[0].children && ctx.key[0].children.IKey) { + const keyTexts = ctx.key[0].children.IKey.map((tok: any) => { + return formatKey(tok.image); + }); + return ['[[', join('.', keyTexts), ']]']; + } + return '[[]]'; + } + + /** + * Visit newline (should not be called) + */ + nl(ctx: any): never { + throw new Error('Should not get here!'); + } + + /** + * Visit comment newline (no-op) + */ + commentNewline(ctx: any): void { + // No operation needed + } +} + +// Create singleton visitor instance +const beautifierVisitor = new TomlBeautifierVisitor(); + +/** + * Main print function for Prettier + * @param path - AST path from Prettier + * @param options - Print options + * @param print - Print function (unused in this implementation) + * @returns Formatted document + */ +export function print(path: AstPath, options?: any, print?: any): Doc { + const cst = path.node as TomlDocument; + return beautifierVisitor.visit(cst); +} diff --git a/frontend/src/common/prettier/plugins/toml/types.ts b/frontend/src/common/prettier/plugins/toml/types.ts new file mode 100644 index 0000000..448ac03 --- /dev/null +++ b/frontend/src/common/prettier/plugins/toml/types.ts @@ -0,0 +1,62 @@ +/** + * TypeScript type definitions for TOML Prettier plugin + */ + +// TOML CST Node types based on @toml-tools/parser +export interface TomlCstNode { + name?: string; + image?: string; + children?: Record; + startOffset?: number; + endOffset?: number; + startLine?: number; + endLine?: number; + tokenType?: any; +} + +export interface TomlComment extends TomlCstNode { + image: string; +} + +export interface TomlContext { + [key: string]: TomlCstNode[]; +} + +export interface TomlValue extends TomlCstNode { + children: TomlContext; +} + +export interface TomlKeyVal extends TomlCstNode { + key: TomlCstNode[]; + val: TomlCstNode[]; +} + +export interface TomlArray extends TomlCstNode { + arrayValues?: TomlCstNode; + commentNewline?: TomlCstNode[]; +} + +export interface TomlInlineTable extends TomlCstNode { + inlineTableKeyVals?: TomlCstNode; +} + +export interface TomlTable extends TomlCstNode { + table: TomlCstNode[]; +} + +export interface TomlExpression extends TomlCstNode { + keyval?: TomlKeyVal[]; + table?: TomlTable[]; + Comment?: TomlComment[]; +} + +export interface TomlDocument extends TomlCstNode { + expression?: TomlExpression[]; +} + +// Print options for TOML formatting +export interface TomlPrintOptions { + printWidth?: number; + tabWidth?: number; + useTabs?: boolean; +} diff --git a/frontend/src/stores/themeStore.ts b/frontend/src/stores/themeStore.ts index 32c44dd..d04ec9b 100644 --- a/frontend/src/stores/themeStore.ts +++ b/frontend/src/stores/themeStore.ts @@ -29,6 +29,10 @@ export const useThemeStore = defineStore('theme', () => { const initializeThemeColors = async () => { try { const themes = await ThemeService.GetDefaultThemes(); + if (!themes) { + Object.assign(themeColors.darkTheme, defaultDarkColors); + Object.assign(themeColors.lightTheme, defaultLightColors); + } if (themes[ThemeType.ThemeTypeDark]) { Object.assign(themeColors.darkTheme, themes[ThemeType.ThemeTypeDark].colors); } diff --git a/frontend/src/views/editor/extensions/codeblock/lang-parser/languages.ts b/frontend/src/views/editor/extensions/codeblock/lang-parser/languages.ts index 7ad7f09..1c5a579 100644 --- a/frontend/src/views/editor/extensions/codeblock/lang-parser/languages.ts +++ b/frontend/src/views/editor/extensions/codeblock/lang-parser/languages.ts @@ -43,7 +43,7 @@ import javaPrettierPlugin from "@/common/prettier/plugins/java" import xmlPrettierPlugin from "@prettier/plugin-xml" import * as rustPrettierPlugin from "@/common/prettier/plugins/rust"; import * as shellPrettierPlugin from "@/common/prettier/plugins/shell"; -import tomlPrettierPlugin from "prettier-plugin-toml"; +import tomlPrettierPlugin from "@/common/prettier/plugins/toml"; import clojurePrettierPlugin from "@cospaia/prettier-plugin-clojure"; import groovyPrettierPlugin from "@/common/prettier/plugins/groovy"; import powershellPrettierPlugin from "@/common/prettier/plugins/powershell"; diff --git a/frontend/src/views/settings/pages/TestPage.vue b/frontend/src/views/settings/pages/TestPage.vue index 055af7d..c5b3687 100644 --- a/frontend/src/views/settings/pages/TestPage.vue +++ b/frontend/src/views/settings/pages/TestPage.vue @@ -72,6 +72,66 @@ + + + + + + +
+ + + + + + +
+
+ + +
+
+ {{ formatStatus.type.toUpperCase() }}: {{ formatStatus.message }} +
+
+
+ [{{ detail.time }}] + {{ detail.message }} +
+
+
+ 执行时间: {{ formatStatus.duration }}ms +
+
+ + + + + +
+ @@ -91,6 +151,8 @@ import { ref } from 'vue' import * as TestService from '@/../bindings/voidraft/internal/services/testservice' import SettingSection from '../components/SettingSection.vue' import SettingItem from '../components/SettingItem.vue' +import { format } from 'prettier' +import goPrettierPlugin from '@/common/prettier/plugins/go/go.mjs' // Badge测试状态 const badgeText = ref('') @@ -105,6 +167,33 @@ const notificationStatus = ref<{ type: string; message: string } | null>(null) // 清除状态 const clearStatus = ref<{ type: string; message: string } | null>(null) +// Go代码格式化测试状态 +const goCode = ref(`package main + +import( +"fmt" +"os" +) + +func main(){ +if len(os.Args)<2{ +fmt.Println("Usage: program ") +return +} +name:=os.Args[1] +fmt.Printf("Hello, %s!\\n",name) +}`) + +const formattedCode = ref('') +const isFormatting = ref(false) +const isInitializing = ref(false) +const formatStatus = ref<{ + type: 'success' | 'error' | 'info' | 'warning'; + message: string; + details?: Array<{ time: string; message: string }>; + duration?: number; +} | null>(null) + // 显示状态消息的辅助函数 const showStatus = (statusRef: any, type: 'success' | 'error', message: string) => { statusRef.value = { type, message } @@ -158,6 +247,382 @@ const testUpdateNotification = async () => { } } +// Go代码格式化相关函数 +const addFormatDetail = (message: string) => { + const time = new Date().toLocaleTimeString() + if (!formatStatus.value) { + formatStatus.value = { + type: 'info', + message: '正在执行...', + details: [] + } + } + if (!formatStatus.value.details) { + formatStatus.value.details = [] + } + formatStatus.value.details.push({ time, message }) +} + +// 检查WASM状态 +const checkWasmStatus = async () => { + formatStatus.value = { + type: 'info', + message: '检查WASM状态...', + details: [] + } + + addFormatDetail('开始检查环境...') + + try { + // 检查浏览器环境 + addFormatDetail('检查浏览器环境支持') + if (typeof WebAssembly === 'undefined') { + throw new Error('WebAssembly not supported in this browser') + } + addFormatDetail('✅ WebAssembly 支持正常') + + // 检查Go运行时 + addFormatDetail('检查Go运行时状态') + if (typeof globalThis.Go !== 'undefined') { + addFormatDetail('✅ Go运行时已加载') + } else { + addFormatDetail('❌ Go运行时未加载') + } + + // 检查formatGo函数 + addFormatDetail('检查formatGo函数') + if (typeof globalThis.formatGo === 'function') { + addFormatDetail('✅ formatGo函数可用') + } else { + addFormatDetail('❌ formatGo函数不可用') + } + + // 检查WASM文件可访问性 + addFormatDetail('检查WASM文件可访问性') + try { + const response = await fetch('/go.wasm', { method: 'HEAD' }) + if (response.ok) { + addFormatDetail('✅ go.wasm文件可访问') + } else { + addFormatDetail(`❌ go.wasm文件不可访问: ${response.status}`) + } + } catch (error) { + addFormatDetail(`❌ go.wasm文件访问失败: ${error}`) + } + + // 检查wasm_exec.js + addFormatDetail('检查wasm_exec.js文件') + try { + const response = await fetch('/wasm_exec.js', { method: 'HEAD' }) + if (response.ok) { + addFormatDetail('✅ wasm_exec.js文件可访问') + } else { + addFormatDetail(`❌ wasm_exec.js文件不可访问: ${response.status}`) + } + } catch (error) { + addFormatDetail(`❌ wasm_exec.js文件访问失败: ${error}`) + } + + formatStatus.value.type = 'success' + formatStatus.value.message = 'WASM状态检查完成' + + } catch (error: any) { + addFormatDetail(`❌ 检查失败: ${error.message}`) + formatStatus.value.type = 'error' + formatStatus.value.message = `WASM状态检查失败: ${error.message}` + } +} + +// 手动初始化 Go WASM +const initializeGoWasm = async () => { + if (isInitializing.value) return + + isInitializing.value = true + + formatStatus.value = { + type: 'info', + message: '正在初始化 Go WASM...', + details: [] + } + + try { + addFormatDetail('开始手动初始化 Go WASM') + + // 直接调用插件的初始化函数 + const { initialize } = await import('@/common/prettier/plugins/go/go.mjs') + + addFormatDetail('调用插件初始化函数...') + await initialize() + + addFormatDetail('检查 formatGo 函数是否可用...') + if (typeof globalThis.formatGo === 'function') { + addFormatDetail('✅ formatGo 函数初始化成功') + + // 测试函数 + addFormatDetail('测试 formatGo 函数...') + const testCode = 'package main\nfunc main(){}' + const result = globalThis.formatGo(testCode) + addFormatDetail(`✅ 测试成功,格式化后长度: ${result.length}`) + + formatStatus.value = { + type: 'success', + message: 'Go WASM 初始化成功!', + details: formatStatus.value.details + } + } else { + throw new Error('formatGo 函数仍然不可用') + } + + } catch (error: any) { + addFormatDetail(`❌ 初始化失败: ${error.message}`) + formatStatus.value = { + type: 'error', + message: `Go WASM 初始化失败: ${error.message}`, + details: formatStatus.value.details + } + } finally { + isInitializing.value = false + } +} + +// 测试Go代码格式化 +const testGoFormatter = async () => { + if (isFormatting.value) return + + isFormatting.value = true + formattedCode.value = '' + + const startTime = Date.now() + + formatStatus.value = { + type: 'info', + message: '正在格式化Go代码...', + details: [] + } + + try { + addFormatDetail('开始格式化流程') + addFormatDetail(`输入代码长度: ${goCode.value.length} 字符`) + + // 设置超时检测 + const timeoutId = setTimeout(() => { + addFormatDetail('⚠️ 格式化超时 (10秒),可能存在阻塞') + }, 10000) + + addFormatDetail('调用prettier格式化...') + + const result = await format(goCode.value, { + parser: 'go-format', + plugins: [goPrettierPlugin] + }) + + clearTimeout(timeoutId) + + const duration = Date.now() - startTime + + addFormatDetail('✅ 格式化完成') + addFormatDetail(`输出代码长度: ${result.length} 字符`) + + formattedCode.value = result + + formatStatus.value = { + type: 'success', + message: '代码格式化成功!', + details: formatStatus.value.details, + duration + } + + } catch (error: any) { + const duration = Date.now() - startTime + + addFormatDetail(`❌ 格式化失败: ${error.message}`) + + // 详细错误分析 + if (error.message.includes('WASM')) { + addFormatDetail('可能原因: WASM模块加载或初始化问题') + } else if (error.message.includes('formatGo')) { + addFormatDetail('可能原因: Go函数未正确暴露到全局作用域') + } else if (error.message.includes('timeout')) { + addFormatDetail('可能原因: 代码执行超时或阻塞') + } + + formatStatus.value = { + type: 'error', + message: `格式化失败: ${error.message}`, + details: formatStatus.value.details, + duration + } + } finally { + isFormatting.value = false + } +} + +// 重置Go代码为示例 +const resetGoCode = () => { + goCode.value = `package main + +import( +"fmt" +"os" +) + +func main(){ +if len(os.Args)<2{ +fmt.Println("Usage: program ") +return +} +name:=os.Args[1] +fmt.Printf("Hello, %s!\\n",name) +}` + formattedCode.value = '' + formatStatus.value = null +} + +// 加载复杂示例 +const loadComplexSample = () => { + goCode.value = `package main + +import( +"encoding/json" +"fmt" +"io/ioutil" +"log" +"net/http" +"os" +"strconv" +"strings" +"time" +) + +type User struct{ +ID int \`json:"id"\` +Name string \`json:"name"\` +Email string \`json:"email"\` +CreatedAt time.Time \`json:"created_at"\` +} + +type UserService struct{ +users []User +nextID int +} + +func NewUserService()*UserService{ +return &UserService{ +users:make([]User,0), +nextID:1, +} +} + +func(s *UserService)CreateUser(name,email string)*User{ +user:=User{ +ID:s.nextID, +Name:name, +Email:email, +CreatedAt:time.Now(), +} +s.users=append(s.users,user) +s.nextID++ +return &user +} + +func(s *UserService)GetUser(id int)*User{ +for i:=range s.users{ +if s.users[i].ID==id{ +return &s.users[i] +} +} +return nil +} + +func(s *UserService)ListUsers()[]User{ +return s.users +} + +func main(){ +service:=NewUserService() + +http.HandleFunc("/users",func(w http.ResponseWriter,r *http.Request){ +switch r.Method{ +case http.MethodGet: +users:=service.ListUsers() +w.Header().Set("Content-Type","application/json") +json.NewEncoder(w).Encode(users) +case http.MethodPost: +body,err:=ioutil.ReadAll(r.Body) +if err!=nil{ +http.Error(w,"Bad request",http.StatusBadRequest) +return +} +var req struct{ +Name string \`json:"name"\` +Email string \`json:"email"\` +} +if err:=json.Unmarshal(body,&req);err!=nil{ +http.Error(w,"Invalid JSON",http.StatusBadRequest) +return +} +user:=service.CreateUser(req.Name,req.Email) +w.Header().Set("Content-Type","application/json") +w.WriteHeader(http.StatusCreated) +json.NewEncoder(w).Encode(user) +default: +http.Error(w,"Method not allowed",http.StatusMethodNotAllowed) +} +}) + +http.HandleFunc("/users/",func(w http.ResponseWriter,r *http.Request){ +if r.Method!=http.MethodGet{ +http.Error(w,"Method not allowed",http.StatusMethodNotAllowed) +return +} +idStr:=strings.TrimPrefix(r.URL.Path,"/users/") +id,err:=strconv.Atoi(idStr) +if err!=nil{ +http.Error(w,"Invalid user ID",http.StatusBadRequest) +return +} +user:=service.GetUser(id) +if user==nil{ +http.Error(w,"User not found",http.StatusNotFound) +return +} +w.Header().Set("Content-Type","application/json") +json.NewEncoder(w).Encode(user) +}) + +port:=os.Getenv("PORT") +if port==""{ +port="8080" +} + +fmt.Printf("Server starting on port %s\\n",port) +log.Fatal(http.ListenAndServe(":"+port,nil)) +}` + formattedCode.value = '' + formatStatus.value = null +} + +// 加载有语法错误的示例 +const loadBrokenSample = () => { + goCode.value = `package main + +import( +"fmt" +"os +) + +func main({ +if len(os.Args<2{ +fmt.Println("Usage: program ") +return +} +name:=os.Args[1 +fmt.Printf("Hello, %s!\\n",name) +` + formattedCode.value = '' + formatStatus.value = null +} + // 清除所有测试状态 const clearAll = async () => { try { @@ -167,6 +632,10 @@ const clearAll = async () => { notificationTitle.value = '' notificationSubtitle.value = '' notificationBody.value = '' + // 清空Go测试状态 + formattedCode.value = '' + formatStatus.value = null + resetGoCode() showStatus(clearStatus, 'success', 'All test states cleared successfully') } catch (error: any) { showStatus(clearStatus, 'error', `Failed to clear test states: ${error.message || error}`) @@ -207,6 +676,25 @@ const clearAll = async () => { font-family: inherit; line-height: 1.4; } + + &.code-textarea { + font-family: 'JetBrains Mono', 'Fira Code', 'SF Mono', Consolas, 'Liberation Mono', Menlo, Courier, monospace; + font-size: 11px; + line-height: 1.5; + width: 100%; + max-width: 600px; + min-height: 120px; + white-space: pre; + overflow-wrap: normal; + word-break: normal; + tab-size: 2; + + &.result-textarea { + background-color: var(--settings-card-bg); + border-color: #22c55e; + color: var(--settings-text); + } + } } .button-group { @@ -271,4 +759,47 @@ const clearAll = async () => { border-color: rgba(239, 68, 68, 0.2); } } + +.detailed-status { + .status-header { + margin-bottom: 8px; + font-weight: 600; + } + + .status-details { + background-color: rgba(0, 0, 0, 0.05); + border-radius: 4px; + padding: 8px; + margin: 8px 0; + max-height: 200px; + overflow-y: auto; + font-family: 'JetBrains Mono', 'Fira Code', 'SF Mono', Consolas, monospace; + font-size: 10px; + line-height: 1.4; + + .status-detail { + margin-bottom: 2px; + display: flex; + gap: 8px; + + .detail-time { + color: var(--settings-text-secondary); + flex-shrink: 0; + font-weight: 500; + } + + .detail-message { + color: var(--settings-text); + word-break: break-word; + } + } + } + + .status-duration { + margin-top: 8px; + font-size: 10px; + color: var(--settings-text-secondary); + font-weight: 500; + } +}