diff --git a/CHANGELOG.md b/CHANGELOG.md index add871c..2d84ef7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to the **VS Code Aster** extension will be documented in thi The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.6.0] - 2026-04-15 + +Run workflow overhaul: terminal reuse, automatic diagnostics in the Problems panel, and refreshed toolbar icons. + +### Added +- Run diagnostics: `` warnings and ``/`` errors from code_aster, Python tracebacks, `SyntaxError`s, fatal errors (e.g. segfaults), and MED/Fortran errors now surface automatically in the VS Code Problems panel — no `F mess` entry required in the `.export` +- Diagnostics attached to the originating `.comm`/`.com1` line when possible (via CMDTAG markers and Python tracebacks), and cleared between runs +- The existing `code-aster runner` terminal is now reused across runs instead of spawning a new one each time +- Colored toolbar icons: blue rocket for the run button (shared with the `.export` file icon) and orange eye for the mesh viewer button (matches the `.med` palette) + ## [1.5.4] - 2026-04-15 File icon improvements and language support for `.export` and MED files. diff --git a/CITATION.cff b/CITATION.cff index a3dffb4..f075ea4 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,4 +1,4 @@ -cff-version: 1.5.4 +cff-version: 1.6.0 title: VS Code Aster message: >- If you use this software, please cite it using the diff --git a/README.md b/README.md index e072e84..7a24027 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

Simvia Logo

- Version + Version License

diff --git a/ROADMAP.md b/ROADMAP.md index 949c6b7..49c3f2b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -4,7 +4,7 @@ The extension aims to reduce friction between modeling, validation, execution, and analysis by bringing **code_aster** native workflows into the editor. -## Current Capabilities (v1.5.4) +## Current Capabilities (v1.6.0) - `.export` file generator - 3D mesh viewer diff --git a/media/images/icone-export-dark.svg b/media/images/icone-export-dark.svg deleted file mode 100644 index a66c0e3..0000000 --- a/media/images/icone-export-dark.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/media/images/icone-export-light.svg b/media/images/icone-export-light.svg deleted file mode 100644 index a66c0e3..0000000 --- a/media/images/icone-export-light.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/media/images/icone-eye.svg b/media/images/icone-eye.svg new file mode 100644 index 0000000..3ab27ae --- /dev/null +++ b/media/images/icone-eye.svg @@ -0,0 +1 @@ + diff --git a/media/images/icone-med-light.svg b/media/images/icone-med-light.svg deleted file mode 100644 index e113e74..0000000 --- a/media/images/icone-med-light.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/media/images/icone-med-dark.svg b/media/images/icone-med.svg similarity index 100% rename from media/images/icone-med-dark.svg rename to media/images/icone-med.svg diff --git a/media/images/icone-rocket.svg b/media/images/icone-rocket.svg new file mode 100644 index 0000000..5664dff --- /dev/null +++ b/media/images/icone-rocket.svg @@ -0,0 +1 @@ + diff --git a/package-lock.json b/package-lock.json index f255755..bc4d48e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "vs-code-aster", - "version": "1.5.4", + "version": "1.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "vs-code-aster", - "version": "1.5.4", + "version": "1.6.0", "license": "GPL-3.0", "dependencies": { "@tailwindcss/cli": "^4.1.17", diff --git a/package.json b/package.json index 457bc47..57a32c5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vs-code-aster", "displayName": "VS Code Aster", - "version": "1.5.4", + "version": "1.6.0", "description": "VS Code extension for code_aster", "publisher": "simvia", "license": "GPL-3.0", @@ -44,12 +44,18 @@ { "command": "vs-code-aster.run-aster", "title": "Run with code_aster", - "icon": "$(run)" + "icon": { + "light": "./media/images/icone-rocket.svg", + "dark": "./media/images/icone-rocket.svg" + } }, { "command": "vs-code-aster.meshViewer", "title": "Open visualizer", - "icon": "$(eye)" + "icon": { + "light": "./media/images/icone-eye.svg", + "dark": "./media/images/icone-eye.svg" + } }, { "command": "vs-code-aster.restartLSPServer", @@ -92,8 +98,8 @@ ".export" ], "icon": { - "light": "./media/images/icone-export-light.svg", - "dark": "./media/images/icone-export-dark.svg" + "light": "./media/images/icone-rocket.svg", + "dark": "./media/images/icone-rocket.svg" } }, { @@ -107,8 +113,8 @@ ".rmed" ], "icon": { - "light": "./media/images/icone-med-light.svg", - "dark": "./media/images/icone-med-dark.svg" + "light": "./media/images/icone-med.svg", + "dark": "./media/images/icone-med.svg" } } ], diff --git a/src/OutputParser.ts b/src/OutputParser.ts new file mode 100644 index 0000000..8870c67 --- /dev/null +++ b/src/OutputParser.ts @@ -0,0 +1,291 @@ +import * as vscode from 'vscode'; + +interface CMDTAGMatch { + lineNumber: number; // 0-based + column?: number; +} + +/** + * Parses captured code_aster run output (stdout+stderr) and produces diagnostics. + * + * Handles: + * - Code_aster box-drawn `` (warning) / `` / `` (error) messages with CMDTAG mapping + * - Python tracebacks (NameError, AttributeError, RuntimeError, ...) + * - Python SyntaxError blocks (caret-style, no traceback header) + * - Fatal Python errors (e.g. Segmentation fault) + * - MED / Fortran-layer errors (`filename.c [N] : Erreur ...`) + */ +export function parseRunOutput( + content: string, + exportUri: vscode.Uri, + commFiles: Map +): Map { + const diagnostics = new Map(); + + const addDiagnostic = (uri: vscode.Uri, diag: vscode.Diagnostic) => { + const key = uri.toString(); + if (!diagnostics.has(key)) { + diagnostics.set(key, []); + } + diagnostics.get(key)!.push(diag); + }; + + const resolveSourceFile = (filePath: string): { uri: vscode.Uri; matched: boolean } => { + const match = filePath.match(/([^/\\]+?)(?:\.changed\.py)?$/); + if (match && commFiles.has(match[1])) { + return { uri: commFiles.get(match[1])!, matched: true }; + } + return { uri: exportUri, matched: false }; + }; + + const makeRange = (line: number, col = 0): vscode.Range => + new vscode.Range(new vscode.Position(line, col), new vscode.Position(line, col)); + + const lines = content.split('\n'); + + const cmdtagRegex = /^\.\. _(?:run|stg)\d+_(?:cmd|txt)(\d+)(?::(\d+))?/; + // Match ``, ``, `` followed by whitespace or `<` (next tag) — not `_` + // which appears in cave's status flags like `_ALARM`, `_ABNORMAL_ABORT`. + const tagRegex = /<([AEF])>(?!_)/; + const exceptionRegex = /^([A-Z][A-Za-z_]*(?:Error|Exception|Warning|Interrupt|Exit)):\s*(.*)/; + const fileRefRegex = /File "([^"]+)", line (\d+)/; + const medErrorRegex = /^(\w+\.c)\s*\[\d+\]\s*:\s*(.*)/i; + const currentFileRegex = /^__file__\s*=\s*r?["']([^"']+)["']/; + + let lastCMDTAG: CMDTAGMatch | undefined; + let currentCommUri: vscode.Uri | undefined; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (!line.trim()) { + continue; + } + + // Track the current active command file via `__file__ = r"..."` markers + // emitted by cave in each processed command file's preamble. + const currentFileMatch = line.match(currentFileRegex); + if (currentFileMatch) { + const basename = currentFileMatch[1].split(/[/\\]/).pop() || ''; + if (commFiles.has(basename)) { + currentCommUri = commFiles.get(basename); + } + continue; + } + + // CMDTAG marker: remember for the next code_aster box message + const cmdtagMatch = line.match(cmdtagRegex); + if (cmdtagMatch) { + const lineNum = parseInt(cmdtagMatch[1], 10) - 1; + const column = cmdtagMatch[2] ? parseInt(cmdtagMatch[2], 10) - 1 : 0; + lastCMDTAG = { lineNumber: lineNum, column }; + continue; + } + + // 1. Code_aster box message: `` / `` / `` + const tagMatch = line.match(tagRegex); + if (tagMatch) { + const severity = tagMatch[1]; + const diagSeverity = + severity === 'A' ? vscode.DiagnosticSeverity.Warning : vscode.DiagnosticSeverity.Error; + + const messageLines: string[] = [line]; + for (let j = i + 1; j < lines.length; j++) { + const nextLine = lines[j]; + if (!nextLine.trim()) { + break; + } + if (nextLine.match(tagRegex) || nextLine.match(cmdtagRegex)) { + break; + } + messageLines.push(nextLine); + i = j; + } + + const message = messageLines + .filter((l) => l.trim() && !/^[\s║╔╚═╗╝]+$/.test(l)) + .map((l) => l.replace(/[║╔╚╗╝]/g, '').trim()) + .filter((l) => l) + .join('\n') + .trim(); + + let targetUri: vscode.Uri; + let range: vscode.Range; + if (lastCMDTAG && currentCommUri) { + targetUri = currentCommUri; + range = makeRange(lastCMDTAG.lineNumber, lastCMDTAG.column || 0); + } else { + targetUri = exportUri; + range = makeRange(0); + } + + const diag = new vscode.Diagnostic(range, message, diagSeverity); + diag.source = 'code-aster'; + addDiagnostic(targetUri, diag); + lastCMDTAG = undefined; + continue; + } + + // 2. Python traceback: starts with `Traceback (most recent call last):` + if (line.trim() === 'Traceback (most recent call last):') { + const block: string[] = []; + let lastUserFileRef: { path: string; lineNumber: number } | undefined; + let exceptionLine = ''; + let j = i + 1; + for (; j < lines.length; j++) { + const cur = lines[j]; + if (!cur.trim()) { + continue; + } + const fileMatch = cur.match(fileRefRegex); + if (fileMatch) { + const filePath = fileMatch[1]; + const resolved = resolveSourceFile(filePath); + if (resolved.matched) { + lastUserFileRef = { path: filePath, lineNumber: parseInt(fileMatch[2], 10) - 1 }; + } else if (!lastUserFileRef) { + lastUserFileRef = { path: filePath, lineNumber: parseInt(fileMatch[2], 10) - 1 }; + } + block.push(cur); + continue; + } + if (cur.startsWith(' ')) { + block.push(cur); + continue; + } + const excMatch = cur.match(exceptionRegex); + if (excMatch) { + exceptionLine = cur.trim(); + break; + } + break; + } + i = j; + + if (exceptionLine) { + const targetUri = lastUserFileRef ? resolveSourceFile(lastUserFileRef.path).uri : exportUri; + const lineNum = lastUserFileRef ? lastUserFileRef.lineNumber : 0; + const message = [exceptionLine, ...block.slice(-4)].join('\n'); + const diag = new vscode.Diagnostic( + makeRange(lineNum), + message, + vscode.DiagnosticSeverity.Error + ); + diag.source = 'code-aster'; + addDiagnostic(targetUri, diag); + } + continue; + } + + // 3. Python SyntaxError (no Traceback header). Pattern: + // File "...", line N + // + // ^ + // SyntaxError: ... + const fileOnlyMatch = line.match(/^\s*File "([^"]+)", line (\d+)\s*$/); + if (fileOnlyMatch) { + const lookahead = lines.slice(i + 1, i + 6); + const hasCaret = lookahead.some((l) => /^\s*\^+\s*$/.test(l)); + const syntaxIdx = lookahead.findIndex((l) => /^SyntaxError:/.test(l.trim())); + if (hasCaret && syntaxIdx >= 0) { + const filePath = fileOnlyMatch[1]; + const lineNum = parseInt(fileOnlyMatch[2], 10) - 1; + const resolved = resolveSourceFile(filePath); + const message = [lookahead[syntaxIdx].trim(), ...lookahead.slice(0, syntaxIdx)] + .map((l) => l.replace(/\s+$/, '')) + .filter((l) => l) + .join('\n'); + const diag = new vscode.Diagnostic( + makeRange(lineNum), + message, + vscode.DiagnosticSeverity.Error + ); + diag.source = 'code-aster'; + addDiagnostic(resolved.uri, diag); + i += syntaxIdx + 1; + continue; + } + } + + // 4. Fatal Python error. The block is header + `Current thread ...:` + + // indented `File "..."` lines, ending at `Extension modules:` or a + // terminal status line. Scan the next ~40 lines for the deepest user + // source file reference. + if (line.startsWith('Fatal Python error:')) { + const header = line.trim(); + let userFileRef: { path: string; lineNumber: number } | undefined; + let j = i + 1; + const end = Math.min(lines.length, i + 40); + for (; j < end; j++) { + const cur = lines[j]; + if (/^Extension modules:/.test(cur) || /^Segmentation fault/.test(cur.trim())) { + break; + } + const fileMatch = cur.match(fileRefRegex); + if (fileMatch) { + const resolved = resolveSourceFile(fileMatch[1]); + if (resolved.matched) { + userFileRef = { path: fileMatch[1], lineNumber: parseInt(fileMatch[2], 10) - 1 }; + } else if (!userFileRef) { + userFileRef = { path: fileMatch[1], lineNumber: parseInt(fileMatch[2], 10) - 1 }; + } + } + } + i = j; + + const targetUri = userFileRef ? resolveSourceFile(userFileRef.path).uri : exportUri; + const lineNum = userFileRef ? userFileRef.lineNumber : 0; + const diag = new vscode.Diagnostic( + makeRange(lineNum), + header, + vscode.DiagnosticSeverity.Error + ); + diag.source = 'code-aster'; + addDiagnostic(targetUri, diag); + continue; + } + + // 5. MED / Fortran-layer error: `filename.c [N] : Erreur ...` + const medMatch = line.match(medErrorRegex); + if (medMatch && /erreur|error/i.test(line)) { + const messageLines = [line.trim()]; + let j = i + 1; + for (; j < lines.length && j < i + 5; j++) { + const cur = lines[j]; + if (cur.match(medErrorRegex)) { + messageLines.push(cur.trim()); + continue; + } + break; + } + i = j - 1; + + const diag = new vscode.Diagnostic( + makeRange(0), + messageLines.join('\n'), + vscode.DiagnosticSeverity.Error + ); + diag.source = 'code-aster'; + addDiagnostic(exportUri, diag); + continue; + } + } + + // Deduplicate: Python tracebacks are sometimes printed twice by the runtime + // (once to stderr, once tee'd to fort.6). Collapse identical diagnostics + // keyed by (line, first message line). + for (const [key, list] of diagnostics) { + const seen = new Set(); + const unique: vscode.Diagnostic[] = []; + for (const d of list) { + const firstLine = d.message.split('\n')[0]; + const sig = `${d.range.start.line}:${d.severity}:${firstLine}`; + if (!seen.has(sig)) { + seen.add(sig); + unique.push(d); + } + } + diagnostics.set(key, unique); + } + + return diagnostics; +} diff --git a/src/RunAster.ts b/src/RunAster.ts index 01cae87..4743b84 100644 --- a/src/RunAster.ts +++ b/src/RunAster.ts @@ -1,11 +1,25 @@ import * as vscode from 'vscode'; import * as path from 'path'; +import * as fs from 'fs'; import { exec } from 'child_process'; import { promisify } from 'util'; +import { parseRunOutput } from './OutputParser'; const execAsync = promisify(exec); +const LOG_FILENAME = '.vscode-aster-run.log'; + export class RunAster { + private static diagnosticCollection: vscode.DiagnosticCollection | undefined; + private static logWatcher: vscode.FileSystemWatcher | undefined; + + /** + * Initialize the diagnostic collection (called from extension.ts) + */ + public static init(collection: vscode.DiagnosticCollection) { + RunAster.diagnosticCollection = collection; + } + /** * Runs code_aster on the selected .export file in the workspace. */ @@ -34,14 +48,84 @@ export class RunAster { // return; // } - const simulationTerminal = vscode.window.createTerminal({ - name: 'code-aster runner', - cwd: fileDir, - }); + // Parse .export to find all `F comm` entries so we can map traceback + // paths (which reference `.changed.py` in a temp dir) back to + // the user's original files. + const commFiles = new Map(); + try { + const exportContent = fs.readFileSync(filePath, 'utf-8'); + for (const line of exportContent.split('\n')) { + const parts = line.trim().split(/\s+/); + if (parts[0] === 'F' && parts[1] === 'comm' && parts[2]) { + let commPath = parts[2]; + if (!path.isAbsolute(commPath)) { + commPath = path.join(fileDir, commPath); + } + commFiles.set(path.basename(commPath), vscode.Uri.file(commPath)); + } + } + } catch (error) { + console.error('Failed to parse .export file:', error); + } + + // Clear previous diagnostics and dispose old watcher + if (RunAster.diagnosticCollection) { + RunAster.diagnosticCollection.clear(); + } + if (RunAster.logWatcher) { + RunAster.logWatcher.dispose(); + RunAster.logWatcher = undefined; + } - const cmd = `${alias} ${fileName}`; - simulationTerminal.show(); - simulationTerminal.sendText(cmd); + // Capture all stdout/stderr via `tee` to a known log file so we can + // parse the full run output regardless of whether `F mess` is set in + // the `.export`. The user still sees live output in the terminal. + const logPath = path.join(fileDir, LOG_FILENAME); + try { + if (fs.existsSync(logPath)) { + fs.unlinkSync(logPath); + } + } catch (error) { + console.error('Failed to remove old log file:', error); + } + + const cmd = `${alias} ${fileName} 2>&1 | tee "${logPath}"`; + + let simulationTerminal = vscode.window.terminals.find((t) => t.name === 'code-aster runner'); + if (simulationTerminal) { + simulationTerminal.show(); + simulationTerminal.sendText(`cd "${fileDir}" && ${cmd}`); + } else { + simulationTerminal = vscode.window.createTerminal({ + name: 'code-aster runner', + cwd: fileDir, + }); + simulationTerminal.show(); + simulationTerminal.sendText(cmd); + } + + // Set up file watcher for the log file + if (RunAster.diagnosticCollection) { + const exportUri = editor.document.uri; + const updateDiagnostics = () => { + try { + const logContent = fs.readFileSync(logPath, 'utf-8'); + const diagnosticsMap = parseRunOutput(logContent, exportUri, commFiles); + + RunAster.diagnosticCollection!.clear(); + for (const [uriKey, diags] of diagnosticsMap) { + const uri = vscode.Uri.parse(uriKey); + RunAster.diagnosticCollection!.set(uri, diags); + } + } catch (error) { + console.error('Failed to parse run output:', error); + } + }; + + RunAster.logWatcher = vscode.workspace.createFileSystemWatcher(logPath); + RunAster.logWatcher.onDidCreate(updateDiagnostics); + RunAster.logWatcher.onDidChange(updateDiagnostics); + } } /** diff --git a/src/extension.ts b/src/extension.ts index 0d63061..bc61129 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -21,6 +21,11 @@ export async function activate(context: vscode.ExtensionContext) { // Set up telemetry context setTelemetryContext(context); + // Set up diagnostics collection for code-aster output parsing + const diagnosticCollection = vscode.languages.createDiagnosticCollection('code-aster'); + RunAster.init(diagnosticCollection); + context.subscriptions.push(diagnosticCollection); + const runaster = vscode.commands.registerCommand('vs-code-aster.run-aster', () => { RunAster.runCodeAster(); });