import * as vscode from 'vscode'; const MODULE_HEADER = /\b(DEFINITION\s+)?MODULE\s+([A-Za-z_][A-Za-z0-9_]*)/; const END_MODULE = /\bEND\s+([A-Za-z_][A-Za-z0-9_]*)\s*(\.)?/g; interface ScanResult { diagnostics: vscode.Diagnostic[]; code: string; } function positionAt(document: vscode.TextDocument, offset: number): vscode.Position { return document.positionAt(Math.max(0, Math.min(offset, document.getText().length))); } function rangeAt(document: vscode.TextDocument, start: number, end: number): vscode.Range { return new vscode.Range(positionAt(document, start), positionAt(document, end)); } function diagnostic( document: vscode.TextDocument, start: number, end: number, message: string, severity = vscode.DiagnosticSeverity.Error ): vscode.Diagnostic { const item = new vscode.Diagnostic(rangeAt(document, start, end), message, severity); item.source = 'Oberon'; return item; } /** * Lightweight diagnostics used alongside the Coco/R dialect validators. * * Only constructs recognisable without pretending to be a complete * Oberon parser: unterminated comments/strings, the module header, and * the final `END name.` mismatch. Note Oberon is case-sensitive, so the * header match is exact. Real syntax diagnostics come from the * configured dialect validator. */ function scan(document: vscode.TextDocument): ScanResult { const text = document.getText(); const diagnostics: vscode.Diagnostic[] = []; let code = ''; let i = 0; while (i < text.length) { const ch = text[i]; const next = text[i + 1]; if (ch === '(' && next === '*') { const start = i; i += 2; let depth = 1; while (i < text.length && depth > 0) { if (text[i] === '(' && text[i + 1] === '*') { depth++; i += 2; continue; } if (text[i] === '*' && text[i + 1] === ')') { depth--; i += 2; continue; } i++; } if (depth > 0) { diagnostics.push(diagnostic(document, start, Math.min(start + 2, text.length), 'Unterminated Oberon comment.')); } code += ' '; continue; } if (ch === '"' || ch === "'") { const start = i; i++; let closed = false; while (i < text.length && text[i] !== '\n') { if (text[i] === ch) { i++; closed = true; break; } i++; } if (!closed) { diagnostics.push(diagnostic(document, start, Math.min(start + 1, text.length), 'Unterminated string literal.')); } code += ' '; continue; } code += ch; i++; } const header = MODULE_HEADER.exec(code); if (!header) { diagnostics.push(diagnostic(document, 0, Math.min(code.length, 1), 'Expected a MODULE header.')); return { diagnostics, code }; } const moduleName = header[2]; const headerEnd = header.index + header[0].length; const semicolon = code.indexOf(';', headerEnd); if (semicolon < 0) { diagnostics.push(diagnostic(document, headerEnd, Math.min(headerEnd + moduleName.length, code.length), 'Expected ";" after the module header.')); } END_MODULE.lastIndex = 0; let match: RegExpExecArray | null = null; let lastEnd: RegExpExecArray | null = null; while ((match = END_MODULE.exec(code)) !== null) { lastEnd = match; } if (!lastEnd) { diagnostics.push(diagnostic(document, Math.max(0, code.trimEnd().length - 1), code.length, `Expected "END ${moduleName}." at the end of the module.`)); } else { const endName = lastEnd[1]; const nameStart = lastEnd.index + lastEnd[0].indexOf(endName); // Case-sensitive: END Name must match MODULE Name exactly. if (endName !== moduleName) { diagnostics.push(diagnostic(document, nameStart, nameStart + endName.length, `Module name "${moduleName}" expected after END, found "${endName}".`)); } if (!lastEnd[2]) { const dot = lastEnd.index + lastEnd[0].length; diagnostics.push(diagnostic(document, dot, Math.min(dot + 1, code.length), `Expected "." after END ${endName}.`)); } } return { diagnostics, code }; } export function createDiagnostics(context: vscode.ExtensionContext): void { const collection = vscode.languages.createDiagnosticCollection('oberon'); context.subscriptions.push(collection); const update = (document: vscode.TextDocument): void => { if (document.languageId !== 'oberon') { return; } collection.set(document.uri, scan(document).diagnostics); }; for (const document of vscode.workspace.textDocuments) { update(document); } context.subscriptions.push( vscode.workspace.onDidOpenTextDocument(update), vscode.workspace.onDidChangeTextDocument(event => update(event.document)), vscode.workspace.onDidCloseTextDocument(document => collection.delete(document.uri)) ); }