|
|
@@ -0,0 +1,143 @@
|
|
|
+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))
|
|
|
+ );
|
|
|
+}
|