diff --git a/compiler/src/ScriptCompiler.ts b/compiler/src/ScriptCompiler.ts index 58389df..4324e0e 100644 --- a/compiler/src/ScriptCompiler.ts +++ b/compiler/src/ScriptCompiler.ts @@ -4,6 +4,8 @@ import * as path from 'path'; export interface CompilationError { filePath: string; line: number; + charStart?: number; + charEnd?: number; message: string; } @@ -15,48 +17,92 @@ export interface ScriptCompilerOptions { onError?: (error: CompilationError) => void } +export interface ICompilationLogger { + info(message: string): void; + error(message: string): void; + writeLine(message: string): void; +} + +class Metrics { + public totalLinesRead : number = 0; + public totalLinesWritten: number = 0; + public readTimeMs : number = 0; + public writeTimeMs : number = 0; + public totalTimeMs : number = 0; + public filesRead : number = 0; + public filesWritten : number = 0; + + log(logger: ICompilationLogger){ + logger.writeLine("Compilation Metrics: ") + logger.writeLine(`=====================`) + logger.writeLine(`Lines Read: ${this.totalLinesRead}`) + logger.writeLine(`Lines Written: ${this.totalLinesWritten}`) + logger.writeLine(`Files Processed: ${this.filesRead} | ${this.filesWritten}`) + logger.writeLine(`=====================`) + logger.writeLine(`Read Time (ms): ${this.readTimeMs} ms`) + logger.writeLine(`Write Time (ms): ${this.writeTimeMs} ms`) + logger.writeLine(`Total Time (ms): ${this.totalTimeMs} ms`) + } +} + const LUA_SCRIPT_GLOBAL_KEYWORD = 'ScriptGlobals'; export class ScriptCompiler { - constructor(private options: ScriptCompilerOptions) { + constructor(private options: ScriptCompilerOptions, private logger: ICompilationLogger) { if (options.outputFileName === undefined) { this.options.outputFileName = 'compiled.lua'; } } public async compile(): Promise { + const start = Date.now(); + const metricsMeter = new Metrics(); const entries = fs.readdirSync(this.options.sourcePath, { recursive: true, withFileTypes: true }); const parsedFiles: Map = new Map(); + const readStart = Date.now(); for (const entry of entries) { if (entry.isFile() && entry.name.endsWith('.lua')) { const fullPath = path.join(entry.parentPath ?? entry.path, entry.name); const relativePath = path.relative(this.options.sourcePath, fullPath); const content = fs.readFileSync(fullPath, 'utf-8'); - const parsedFile = this.parseFile(relativePath, content, fullPath); + const parsedFile = this.parseFile(relativePath, content, fullPath, metricsMeter); parsedFiles.set(parsedFile.fileKey, parsedFile); + metricsMeter.filesRead++; } } + const readEnd = Date.now(); + metricsMeter.readTimeMs = readEnd - readStart; const writer = new Writer( path.join(this.options.outputPath, this.options.outputFileName!), parsedFiles, + this.logger, this.options.onError ); + + const writeStart = Date.now(); + writer.write(metricsMeter); + const writeEnd = Date.now(); + metricsMeter.writeTimeMs = (writeEnd - writeStart); - writer.write(); - console.log(`Compilation complete. Output written to ${path.join(this.options.outputPath, this.options.outputFileName!)}`); + writer.logDependencyTree(); + this.logger.info(`Compilation complete. Output written to ${path.join(this.options.outputPath, this.options.outputFileName!)}`); + const end = Date.now(); + + metricsMeter.totalTimeMs = (end-start); + metricsMeter.log(this.logger); } - private reportError(filePath: string, line: number, message: string): void { + private reportError(filePath: string, line: number, charStart: number | undefined, charEnd: number | undefined, message: string): void { if (this.options.onError) { - this.options.onError({ filePath, line, message }); + this.options.onError({ filePath, line, charStart, charEnd, message }); } } - private parseFile(filePath: string, content: string, fullPath: string): ParsedFile { + private parseFile(filePath: string, content: string, fullPath: string, metricsMeter: Metrics): ParsedFile { const dependencies: Dependency[] = []; const newLines: string[] = [`do --${filePath}`]; @@ -83,6 +129,7 @@ export class ScriptCompiler { }; for (let i = 0; i < lines.length; i++) { + metricsMeter.totalLinesRead++; let line = lines[i]; const trimmedLine = line.trim(); @@ -103,7 +150,7 @@ export class ScriptCompiler { } newLines.push(line); } else { - this.reportError(fullPath, i, `Code found after module-level return: ${trimmedLine}`); + this.reportError(fullPath, i, undefined, undefined, `Code found after module-level return: ${trimmedLine}`); newLines.push(line); // Continue processing despite error } continue; @@ -112,8 +159,10 @@ export class ScriptCompiler { // Handle require statements const requireMatch = line.match(/require\(['"](.+?)['"]\)/); if (requireMatch) { - const requiredModule = fileReferenceToLuaVariable(requireMatch[1]); - dependencies.push(new Dependency(requiredModule, fullPath, i + 1)); + const textMatch = requireMatch[1]; + const requiredModule = fileReferenceToLuaVariable(textMatch); + + dependencies.push(new Dependency(textMatch, i, requireMatch.index ?? 0, (requireMatch.index ?? 0) + requireMatch[0].length)); line = line.replace(requireMatch[0], requiredModule); } @@ -164,7 +213,7 @@ export class ScriptCompiler { const afterReturn = trimmedLine.substring(afterReturnPos).trim(); if (afterReturn === '') { - this.reportError(fullPath, i, 'Empty return statement at module level'); + this.reportError(fullPath, i, afterReturnPos, afterReturnPos, 'Empty return statement at module level'); continue; } @@ -207,7 +256,7 @@ export class ScriptCompiler { } if (hasMultipleValues) { - this.reportError(fullPath, i, `Multiple return values not supported: ${trimmedLine}`); + this.reportError(fullPath, i, afterReturnPos, afterReturnPos + afterReturn.length, `Multiple return values not supported: ${trimmedLine}`); } else { // Replace return with assignment const moduleVariable = fileReferenceToLuaVariable(filePath); @@ -242,12 +291,17 @@ function stripLuaMultilineComments(content: string): string { } class Dependency { - + public readonly fileKey: string; + constructor( - public readonly name: string, - public readonly filePath: string, - public readonly requiredAtLine: number - ){} + public readonly requiredModule: string, + public readonly requiredAtLine: number, + public readonly charStart: number, + public readonly charEnd: number + ) + { + this.fileKey = fileReferenceToLuaVariable(requiredModule); + } } class ParsedFile { @@ -298,38 +352,52 @@ class Writer { constructor( public location: string, public files: Map, + private readonly logger: ICompilationLogger, public onError?: (error: CompilationError) => void ) {} private getStartLines(): string[] { - return [`local ${LUA_SCRIPT_GLOBAL_KEYWORD} = {}`]; + return [ + `-- Transpiled at (UTC): ${new Date().toISOString()}`, + `local ${LUA_SCRIPT_GLOBAL_KEYWORD} = {}` + ]; } - write(): void { + logDependencyTree(): void { + const visited: Set = new Set(); + const depth : number = 1; + this.logger.writeLine('Dependency Tree :'); + const logFileRecursive = (parsedFile: ParsedFile, currentDepth: number) => { + if (visited.has(parsedFile.fileKey)) { + return; + } + visited.add(parsedFile.fileKey); + let padding = ' '.repeat(currentDepth); + if (currentDepth > 0) { + padding = ' '.repeat(currentDepth) + '└─>'; + } + const printable = parsedFile.fileKey.replace(LUA_SCRIPT_GLOBAL_KEYWORD + '.', ''); + this.logger.writeLine(padding + printable); + for (const dep of parsedFile.dependencies) { + const depFile = this.files.get(dep.fileKey); + if (depFile) { + logFileRecursive(depFile, currentDepth + 1); + } + } + } + + for (const parsedFile of this.files.values()) { + logFileRecursive(parsedFile, depth); + } + }; + + write(metrics: Metrics): void { const writtenFiles: Set = new Set(); const outputLines: string[] = []; - outputLines.push(...this.getStartLines()); - - const writeFileRecursive = (parsedFile: ParsedFile) => { - - // Write dependencies first - for (const dep of parsedFile.dependencies) { - const depFile = this.files.get(dep.name); - if (depFile) { - writeFileRecursive(depFile); - } else { - this.onError?.({ - filePath: parsedFile.fullPath, - line: 0, - message: `Missing dependency: ${dep}` - }); - } - } - - outputLines.push(...parsedFile.lines); - writtenFiles.add(parsedFile.fileKey); - }; + const startLines = this.getStartLines(); + metrics.totalLinesWritten+=startLines.length; + outputLines.push(...startLines); //Check for circular dependencies before writing const visited: Set = new Set(); @@ -357,7 +425,7 @@ class Writer { const file = this.files.get(key); if (file) { for (const dep of file.dependencies) { - if (checkCircular(dep.name)) { + if (checkCircular(dep.fileKey)) { return true; } } @@ -371,6 +439,32 @@ class Writer { } } + const writeFileRecursive = (parsedFile: ParsedFile) => { + if (writtenFiles.has(parsedFile.fileKey)) { + return; + } + + // Write dependencies first + for (const dep of parsedFile.dependencies) { + const depFile = this.files.get(dep.fileKey); + if (depFile) { + writeFileRecursive(depFile); + } else { + this.onError?.({ + filePath: parsedFile.fullPath, + line: dep.requiredAtLine, + charStart: dep.charStart, + charEnd: dep.charEnd, + message: `Missing dependency: ${dep.fileKey}` + }); + } + } + metrics.totalLinesWritten += parsedFile.lines.length; + outputLines.push(...parsedFile.lines); + writtenFiles.add(parsedFile.fileKey); + metrics.filesWritten++; + }; + // Write all files in dependency order for (const parsedFile of this.files.values()) { writeFileRecursive(parsedFile); diff --git a/dutchies-dcs-scripting-tools/package.json b/dutchies-dcs-scripting-tools/package.json index c3baa5d..8b56cb2 100644 --- a/dutchies-dcs-scripting-tools/package.json +++ b/dutchies-dcs-scripting-tools/package.json @@ -22,6 +22,17 @@ "activationEvents": [], "main": "./dist/extension.js", "contributes": { + "configuration": { + "title": "Dutchies DCS Scripting Tools Settings", + "properties": { + "dutchiesDcsScriptingTools.compileAt" : { + "type": "string", + "enum": ["onSave", "onCompileCommand"], + "default": "onCompileCommand", + "description": "When to compile the Lua scripts" + } + } + }, "grammars": [ { "path": "./syntaxes/lua-slocal.json", @@ -56,9 +67,7 @@ "lua-addons/**/*", "syntaxes/**/*" ], - "dependencies": { - "dcs-script-compiler" : "*" - }, + "dependencies": {}, "scripts": { "vscode:prepublish": "npm run build -w compiler && npm run package", "compile": "npm run check-types && npm run lint && node esbuild.js", diff --git a/dutchies-dcs-scripting-tools/src/extension.ts b/dutchies-dcs-scripting-tools/src/extension.ts index f261035..15a43c6 100644 --- a/dutchies-dcs-scripting-tools/src/extension.ts +++ b/dutchies-dcs-scripting-tools/src/extension.ts @@ -1,7 +1,8 @@ // The module 'vscode' contains the VS Code extensibility API // Import the module and reference it with the alias vscode in your code below import * as vscode from 'vscode'; -import { ScriptCompiler, ScriptCompilerOptions, CompilationError } from 'dcs-script-compiler'; +import { ScriptCompiler, ScriptCompilerOptions, CompilationError, ICompilationLogger } from 'dcs-script-compiler'; +import { Logger } from './logger'; const luaWorkSpaceSettingKey = "Lua.workspace"; const librarySettingsKey = "library"; @@ -9,6 +10,7 @@ const diagnosticCollection = vscode.languages.createDiagnosticCollection('lua-tr let pluginPath : string | undefined; +const logger = new Logger(); // This method is called when your extension is activated // Your extension is activated the very first time the command is executed @@ -36,12 +38,12 @@ export function activate(context: vscode.ExtensionContext) { const start = Date.now(); await compileLuaScripts(); const end = Date.now(); - console.log(`Lua scripts compiled in ${(end - start) / 1000} seconds.`); vscode.window.showInformationMessage(`Lua scripts compiled successfully in ${(end - start) / 1000} seconds.`); }catch(err){ vscode.window.showErrorMessage('Error compiling Lua scripts: ' + (err as Error).message); } }); + } // This method is called when your extension is deactivated @@ -50,6 +52,22 @@ export function deactivate() removePluginPathFromSettings(); } +class CompilationLogger implements ICompilationLogger { + + constructor( + private readonly logger: Logger) { + } + info(message: string): void { + this.logger.info(message); + } + error(message: string): void { + this.logger.error(message); + } + writeLine(message: string): void { + this.logger.log(message); + } +} + async function compileLuaScripts() { const config = vscode.workspace.getConfiguration('dcsScriptingTools'); @@ -74,7 +92,8 @@ async function compileLuaScripts() { } }; - const compiler = new ScriptCompiler(options); + const compilationLogger = new CompilationLogger(logger); + const compiler = new ScriptCompiler(options, compilationLogger); try { await compiler.compile(); } catch (err) { @@ -86,7 +105,7 @@ async function compileLuaScripts() { for (const [filePath, errors] of errorsByFile) { const uri = vscode.Uri.file(filePath); const diagnostics = errors.map(error => { - const range = new vscode.Range(error.line, 0, error.line, 0); + const range = new vscode.Range(error.line, error.charStart ?? 0, error.line, error.charEnd ?? 0); const diagnostic = new vscode.Diagnostic(range, error.message, vscode.DiagnosticSeverity.Error); diagnostic.source = 'DCS Lua Transpiler'; return diagnostic; diff --git a/package-lock.json b/package-lock.json index 1203ade..d39b303 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,15 @@ "workspaces": [ "compiler", "dutchies-dcs-scripting-tools" - ] + ], + "devDependencies": { + "@types/node": "^25.5.2" + } }, "compiler": { "name": "dcs-script-compiler", "version": "1.0.0", + "extraneous": true, "devDependencies": { "@types/node": "^22.0.0", "typescript": "^5.9.3" @@ -21,9 +25,6 @@ "dutchies-dcs-scripting-tools": { "version": "0.0.1", "license": "MIT", - "dependencies": { - "dcs-script-compiler": "*" - }, "devDependencies": { "@types/mocha": "^10.0.10", "@types/node": "22.x", @@ -40,6 +41,16 @@ "vscode": "^1.108.1" } }, + "dutchies-dcs-scripting-tools/node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, "node_modules/@bcoe/v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", @@ -832,15 +843,22 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.19.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz", - "integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==", + "version": "25.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz", + "integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.18.0" } }, + "node_modules/@types/node/node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/vscode": { "version": "1.108.1", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.108.1.tgz", @@ -1713,10 +1731,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/dcs-script-compiler": { - "resolved": "compiler", - "link": true - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", diff --git a/package.json b/package.json index f50bb0b..6256f4e 100644 --- a/package.json +++ b/package.json @@ -4,5 +4,8 @@ "workspaces": [ "compiler", "dutchies-dcs-scripting-tools" - ] -} \ No newline at end of file + ], + "devDependencies": { + "@types/node": "^25.5.2" + } +}