Bladeren bron

Split language plugins: Modula-2, Pascal, Oberon

Eric Streit 2 dagen geleden
bovenliggende
commit
9b44b7b708

+ 0 - 28
extensions/modula2-language/COCOR-LSP.md

@@ -83,31 +83,3 @@ consumers must parse stdout.
   Two probes were correctly *not* errors: gm2 itself only warns on a
   missing `;` before `END` and on an unclosed `(` (separator/recovery
   leniency), and GM2 agrees with it.
-
-## Pascal dialects (`.pas` / `.pp` / `.p`)
-
-The same server validates Pascal files with the pilot validators from
-`grammars/`. Each pilot binary takes the source file as its single
-argument and writes a `.LST` listing next to it; the server parses the
-listing's `***** ^ message` lines (positions are caret-relative to the
-code after the listing prefix, zero-based columns).
-
-```json
-"modula2.pascal.dialect": "freepascal",
-"modula2.pascal.validators": {
-  "p2": "<root>/grammars/P2/build/P2",
-  "p4": "<root>/grammars/P4/build/P4",
-  "p6": "<root>/grammars/P6/build/P6",
-  "pascal": "<root>/grammars/Pascal/build/PStd",
-  "turbopascal3": "<root>/grammars/TurboPascal3/build/TP3",
-  "freepascal": "<root>/grammars/FreePascal/build/FPC",
-  "blaise": "<root>/grammars/Blaise/build/BlaiseV",
-  "pascals": "<root>/grammars/PascalS/build/PascalS"
-}
-```
-
-Build each binary first with its pilot `build.sh` (needs `CR` and
-`gm2 -fiso`). The local Modula-2 analysis never runs on Pascal text.
-With no command configured for the active dialect the server publishes
-no diagnostics. Never point these at the `pcom`/`psc` oracle compilers
-— they can hang on invalid input.

+ 1 - 35
extensions/modula2-language/package.json

@@ -12,8 +12,7 @@
   ],
   "activationEvents": [
     "onLanguage:modula2",
-    "onLanguage:modula2-definition",
-    "onLanguage:pascal"
+    "onLanguage:modula2-definition"
   ],
   "main": "./out/extension.js",
   "contributes": {
@@ -39,19 +38,6 @@
           ".def"
         ],
         "configuration": "./language-configuration.json"
-      },
-      {
-        "id": "pascal",
-        "aliases": [
-          "Pascal",
-          "pascal"
-        ],
-        "extensions": [
-          ".pas",
-          ".pp",
-          ".p"
-        ],
-        "configuration": "./language-configuration.json"
       }
     ],
     "grammars": [
@@ -162,26 +148,6 @@
             "{file}"
           ],
           "description": "Arguments passed to the Coco/R validator. Use {file} for the temporary source file."
-        },
-        "modula2.pascal.dialect": {
-          "type": "string",
-          "enum": [
-            "p2",
-            "p4",
-            "p6",
-            "pascal",
-            "turbopascal3",
-            "freepascal",
-            "blaise",
-            "pascals"
-          ],
-          "default": "freepascal",
-          "description": "Pascal dialect used to validate .pas files. Builds: grammars/P2/build/P2, P4/build/P4, P6/build/P6, Pascal/build/PStd, TurboPascal3/build/TP3, FreePascal/build/FPC, Blaise/build/BlaiseV, PascalS/build/PascalS."
-        },
-        "modula2.pascal.validators": {
-          "type": "object",
-          "default": {},
-          "description": "Validator command per Pascal dialect, e.g. {\"freepascal\": \"/path/to/FPC\"}. Each command receives the temporary source file as its single argument. Never point these at pcom/psc oracle compilers (they can hang)."
         }
       }
     }

+ 1 - 4
extensions/modula2-language/src/extension.ts

@@ -26,15 +26,12 @@ export function activate(context: vscode.ExtensionContext): void {
   const clientOptions: LanguageClientOptions = {
     documentSelector: [
       { scheme: 'file', language: 'modula2' },
-      { scheme: 'file', language: 'modula2-definition' },
-      { scheme: 'file', language: 'pascal' }
+      { scheme: 'file', language: 'modula2-definition' }
     ],
     synchronize: { configurationSection: 'modula2' },
     initializationOptions: {
       validatorCommand: cfg.get<string>('cocoR.validatorCommand', ''),
       validatorArguments: cfg.get<string[]>('cocoR.validatorArguments', ['{file}']),
-      pascalDialect: cfg.get<string>('pascal.dialect', 'freepascal'),
-      pascalValidators: cfg.get<object>('pascal.validators', {}),
       format: {
         indentSize: cfg.get<number>('format.indentSize', 2),
         useTabs: cfg.get<boolean>('format.useTabs', false),

+ 0 - 54
extensions/modula2-language/src/server.ts

@@ -11,7 +11,6 @@ import { promises as fs, readFileSync } from 'fs';
 import { spawn } from 'child_process';
 import { pathToFileURL } from 'url';
 import { lex, Token } from './m2/lexer';
-import { parseListing } from './pascalListing';
 import { M2Symbol, M2Unit, parseUnitText } from './m2/parser';
 import { analyseUnit } from './m2/analyse';
 import { computeSemanticTokens, semanticTokensLegend } from './m2/semantic';
@@ -25,20 +24,15 @@ const connection = createConnection(ProposedFeatures.all);
 const documents = new TextDocuments(TextDocument);
 let validatorCommand = '';
 let validatorArguments: string[] = ['{file}'];
-let pascalDialect = 'freepascal';
-let pascalValidators: Record<string, string> = {};
 let formatOptions: FormatOptions = { ...DEFAULT_FORMAT_OPTIONS };
 
 connection.onInitialize((params: InitializeParams): InitializeResult => {
   const options = (params.initializationOptions || {}) as {
     validatorCommand?: string; validatorArguments?: string[];
-    pascalDialect?: string; pascalValidators?: Record<string, string>;
     format?: Partial<FormatOptions>;
   };
   validatorCommand = options.validatorCommand || '';
   validatorArguments = options.validatorArguments || ['{file}'];
-  pascalDialect = options.pascalDialect || 'freepascal';
-  pascalValidators = options.pascalValidators || {};
   formatOptions = normalizeFormatOptions(options.format ?? {});
   return {
     capabilities: {
@@ -63,14 +57,11 @@ connection.onDidChangeConfiguration((params: DidChangeConfigurationParams) => {
   const settings = (params.settings || {}) as {
     modula2?: {
       cocoR?: { validatorCommand?: string; validatorArguments?: string[] };
-      pascal?: { dialect?: string; validators?: Record<string, string> };
       format?: Partial<FormatOptions>;
     };
   };
   validatorCommand = settings.modula2?.cocoR?.validatorCommand || validatorCommand;
   validatorArguments = settings.modula2?.cocoR?.validatorArguments || validatorArguments;
-  pascalDialect = settings.modula2?.pascal?.dialect || pascalDialect;
-  pascalValidators = settings.modula2?.pascal?.validators || pascalValidators;
   if (settings.modula2?.format) {
     formatOptions = normalizeFormatOptions(settings.modula2.format);
   }
@@ -81,10 +72,6 @@ documents.onDidChangeContent(e => validate(e.document));
 documents.onDidClose(e => connection.sendDiagnostics({ uri: e.document.uri, diagnostics: [] }));
 
 async function validate(document: TextDocument): Promise<void> {
-  if (document.languageId === 'pascal') {
-    await validatePascal(document);
-    return;
-  }
   const local = analyseDocument(document);
   if (!validatorCommand) {
     connection.sendDiagnostics({ uri: document.uri, diagnostics: local });
@@ -113,47 +100,6 @@ async function validate(document: TextDocument): Promise<void> {
   }
 }
 
-/** Validate a Pascal document with the configured dialect validator.
- *
- *  The local Modula-2 analysis never runs on Pascal text. Each pilot
- *  validator takes the source file as its single argument and writes
- *  `<base>.LST` next to it; diagnostics come from that listing, whose
- *  `***** ^ message` lines point at the preceding source line.
- *  No validator configured (or dialect unknown) means no diagnostics.
- */
-async function validatePascal(document: TextDocument): Promise<void> {
-  const command = pascalValidators[pascalDialect];
-  if (!command) {
-    connection.sendDiagnostics({ uri: document.uri, diagnostics: [] });
-    return;
-  }
-  const filePath = uriToFilePath(document.uri);
-  const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'pascal-lsp-'));
-  const base = path.basename(filePath, path.extname(filePath));
-  const tempFile = path.join(dir, base + '.pas');
-  try {
-    await fs.writeFile(tempFile, document.getText(), 'utf8');
-    await execute(command, [tempFile], path.dirname(filePath));
-    let listing = '';
-    try {
-      listing = await fs.readFile(path.join(dir, base + '.LST'), 'utf8');
-    } catch {
-      listing = '';
-    }
-    connection.sendDiagnostics({ uri: document.uri, diagnostics: parseListing(listing) });
-  } catch (error) {
-    connection.sendDiagnostics({ uri: document.uri, diagnostics: [{
-      severity: DiagnosticSeverity.Warning,
-      range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } },
-      message: `Pascal validator: ${error instanceof Error ? error.message : String(error)}`,
-      source: 'Pascal'
-    }] });
-  } finally {
-    await fs.rm(dir, { recursive: true, force: true });
-  }
-}
-
-
 function execute(command: string, args: string[], cwd: string): Promise<string> {
   return new Promise((resolve, reject) => {
     const child = spawn(command, args, { cwd, shell: false });

+ 28 - 0
extensions/oberon-language/COCOR-LSP.md

@@ -0,0 +1,28 @@
+# Coco/R diagnostics through LSP
+
+Version 0.1.7 adds the first real Language Server connection. The Theia/VS
+Code extension starts `out/server.js` through `vscode-languageclient`; the
+server invokes a configured executable containing the **generated Coco/R
+parser/validator**.
+
+## Oberon dialects (`.Mod` / `.mod` / `.def`)
+
+The server validates Oberon files with the pilot validators from
+`grammars/`. Each pilot binary takes the source file as its single
+argument and writes a `.LST` listing next to it; the server parses the
+listing's `***** ^ message` lines (positions are caret-relative to the
+code after the listing prefix, zero-based columns).
+
+```json
+"modula2.oberon.dialect": "oberon2",
+"modula2.oberon.validators": {
+  "oberon07": "<root>/grammars/Oberon07/build/O07",
+  "oberon2": "<root>/grammars/Oberon2/build/O2",
+  "ulm": "<root>/grammars/Ulm/build/Ulm"
+}
+```
+
+Build each binary first with its pilot `build.sh` (needs `CR` and
+`gm2 -fiso`). The local Modula-2 analysis never runs on Oberon text.
+With no command configured for the active dialect the server publishes
+no diagnostics.

+ 10 - 0
extensions/oberon-language/language-configuration.json

@@ -0,0 +1,10 @@
+{
+  "comments": {"blockComment": ["(*", "*)"]},
+  "brackets": [["(", ")"], ["[", "]"]],
+  "autoClosingPairs": [{"open": "(", "close": ")"}, {"open": "[", "close": "]"}],
+  "surroundingPairs": [["(", ")"], ["[", "]"]],
+  "indentationRules": {
+    "increaseIndentPattern": "^\\s*(MODULE|DEFINITION\\s+MODULE|PROCEDURE|IF|ELSIF|ELSE|CASE|FOR|WHILE|REPEAT|LOOP|RECORD)\\b.*$",
+    "decreaseIndentPattern": "^\\s*(END|ELSE|ELSIF|UNTIL)\\b.*$"
+  }
+}

+ 72 - 0
extensions/oberon-language/package.json

@@ -0,0 +1,72 @@
+{
+  "name": "oberon-language",
+  "displayName": "Oberon Language Support",
+  "version": "0.1.5",
+  "publisher": "modula2",
+  "license": "MIT",
+  "engines": {
+    "vscode": "^1.90.0"
+  },
+  "categories": [
+    "Programming Languages"
+  ],
+  "activationEvents": [
+    "onLanguage:oberon"
+  ],
+  "main": "./out/extension.js",
+  "contributes": {
+    "languages": [
+      {
+        "id": "oberon",
+        "aliases": [
+          "Oberon",
+          "oberon"
+        ],
+        "extensions": [
+          ".Mod",
+          ".mod",
+          ".def"
+        ],
+        "configuration": "./language-configuration.json"
+      }
+    ],
+    "configuration": {
+      "title": "Oberon",
+      "properties": {
+        "modula2.oberon.dialect": {
+          "type": "string",
+          "enum": [
+            "oberon07",
+            "oberon2",
+            "ulm"
+          ],
+          "default": "oberon2",
+          "description": "Oberon dialect used to validate Oberon files. Builds: grammars/Oberon07/build/O07, Oberon2/build/O2, Ulm/build/Ulm."
+        },
+        "modula2.oberon.validators": {
+          "type": "object",
+          "default": {},
+          "description": "Validator command per Oberon dialect, e.g. {\"oberon2\": \"/path/to/O2\"}. Each command receives the temporary source file as its single argument."
+        }
+      }
+    }
+  },
+  "scripts": {
+    "clean": "rimraf out",
+    "build": "tsc -p tsconfig.json",
+    "prepare": "yarn build",
+    "watch": "tsc -p tsconfig.json --watch"
+  },
+  "devDependencies": {
+    "@types/node": "^24.0.0",
+    "@types/vscode": "^1.90.0",
+    "rimraf": "^6.0.1",
+    "typescript": "^5.9.2"
+  },
+  "dependencies": {
+    "vscode-languageclient": "^9.0.1",
+    "vscode-languageserver": "^9.0.1",
+    "vscode-languageserver-textdocument": "^1.0.11"
+  },
+  "description": "Oberon language support with Coco/R dialect validators (Oberon-07, Oberon-2, Ulm-Oberon)."
+}

+ 26 - 0
extensions/oberon-language/src/extension.ts

@@ -0,0 +1,26 @@
+import * as vscode from 'vscode';
+import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind } from 'vscode-languageclient/node';
+
+export function activate(context: vscode.ExtensionContext): void {
+  const serverModule = context.asAbsolutePath('out/server.js');
+  const serverOptions: ServerOptions = {
+    run: { module: serverModule, transport: TransportKind.ipc },
+    debug: { module: serverModule, transport: TransportKind.ipc }
+  };
+  const cfg = vscode.workspace.getConfiguration('modula2');
+  const clientOptions: LanguageClientOptions = {
+    documentSelector: [
+      { scheme: 'file', language: 'oberon' }
+    ],
+    synchronize: { configurationSection: 'modula2' },
+    initializationOptions: {
+      oberonDialect: cfg.get<string>('oberon.dialect', 'oberon2'),
+      oberonValidators: cfg.get<object>('oberon.validators', {})
+    }
+  };
+  const client = new LanguageClient('oberonCocoR', 'Oberon Coco/R Language Server', serverOptions, clientOptions);
+  context.subscriptions.push(client);
+  void client.start();
+}
+
+export function deactivate(): void {}

+ 28 - 0
extensions/oberon-language/src/oberonListing.ts

@@ -0,0 +1,28 @@
+import { Diagnostic, DiagnosticSeverity } from 'vscode-languageserver/node';
+
+/** Parse a Coco/R `.LST` listing into LSP diagnostics.
+ *
+ *  Error lines look like `*****       ^ invalid Primary` and refer to
+ *  the source line above them (`    4  I := ;`); the caret column is
+ *  relative to the code after the listing's line-number prefix.
+ */
+export function parseListing(listing: string): Diagnostic[] {
+  const result: Diagnostic[] = [];
+  const lines = listing.split('\n');
+  for (let i = 1; i < lines.length; i++) {
+    const m = /^\*+\s*\^\s*(.*)$/.exec(lines[i]);
+    if (!m) continue;
+    const prev = lines[i - 1];
+    const pm = /^\s*(\d+)\s/.exec(prev);
+    if (!pm) continue;
+    const prefix = /^\s*\d+\s*/.exec(prev);
+    const prefixLen = prefix ? prefix[0].length : 0;
+    const line = Math.max(0, Number(pm[1]) - 1);
+    const character = Math.max(0, lines[i].indexOf('^') - prefixLen);
+    const message = (m[1] || '').trim() || 'syntax error';
+    result.push({ severity: DiagnosticSeverity.Error, range: {
+      start: { line, character }, end: { line, character: character + 1 }
+    }, message, source: 'Oberon' });
+  }
+  return result;
+}

+ 101 - 0
extensions/oberon-language/src/server.ts

@@ -0,0 +1,101 @@
+import {
+  createConnection, ProposedFeatures, InitializeParams, InitializeResult,
+  TextDocuments, TextDocumentSyncKind, Diagnostic, DiagnosticSeverity,
+  DidChangeConfigurationParams
+} from 'vscode-languageserver/node';
+import { TextDocument } from 'vscode-languageserver-textdocument';
+import * as path from 'path';
+import * as os from 'os';
+import { promises as fs } from 'fs';
+import { spawn } from 'child_process';
+import { parseListing } from './oberonListing';
+
+const connection = createConnection(ProposedFeatures.all);
+const documents = new TextDocuments(TextDocument);
+let oberonDialect = 'oberon2';
+let oberonValidators: Record<string, string> = {};
+
+connection.onInitialize((params: InitializeParams): InitializeResult => {
+  const options = (params.initializationOptions || {}) as {
+    oberonDialect?: string; oberonValidators?: Record<string, string>;
+  };
+  oberonDialect = options.oberonDialect || 'oberon2';
+  oberonValidators = options.oberonValidators || {};
+  return {
+    capabilities: {
+      textDocumentSync: TextDocumentSyncKind.Full
+    }
+  };
+});
+
+connection.onDidChangeConfiguration((params: DidChangeConfigurationParams) => {
+  const settings = (params.settings || {}) as {
+    modula2?: {
+      oberon?: { dialect?: string; validators?: Record<string, string> };
+    };
+  };
+  oberonDialect = settings.modula2?.oberon?.dialect || oberonDialect;
+  oberonValidators = settings.modula2?.oberon?.validators || oberonValidators;
+});
+
+documents.onDidOpen(e => validate(e.document));
+documents.onDidChangeContent(e => validate(e.document));
+documents.onDidClose(e => connection.sendDiagnostics({ uri: e.document.uri, diagnostics: [] }));
+
+/** Validate a Pascal document with the configured dialect validator.
+ *
+ *  Each pilot validator takes the source file as its single argument and
+ *  writes `<base>.LST` next to it; diagnostics come from that listing, whose
+ *  `***** ^ message` lines point at the preceding source line.
+ *  No validator configured (or dialect unknown) means no diagnostics.
+ */
+async function validate(document: TextDocument): Promise<void> {
+  const command = oberonValidators[oberonDialect];
+  if (!command) {
+    connection.sendDiagnostics({ uri: document.uri, diagnostics: [] });
+    return;
+  }
+  const filePath = uriToFilePath(document.uri);
+  const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oberon-lsp-'));
+  const base = path.basename(filePath, path.extname(filePath));
+  const tempFile = path.join(dir, base + path.extname(filePath));
+  try {
+    await fs.writeFile(tempFile, document.getText(), 'utf8');
+    await execute(command, [tempFile], path.dirname(filePath));
+    let listing = '';
+    try {
+      listing = await fs.readFile(path.join(dir, base + '.LST'), 'utf8');
+    } catch {
+      listing = '';
+    }
+    connection.sendDiagnostics({ uri: document.uri, diagnostics: parseListing(listing) });
+  } catch (error) {
+    connection.sendDiagnostics({ uri: document.uri, diagnostics: [{
+      severity: DiagnosticSeverity.Warning,
+      range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } },
+      message: `Oberon validator: ${error instanceof Error ? error.message : String(error)}`,
+      source: 'Oberon'
+    }] });
+  } finally {
+    await fs.rm(dir, { recursive: true, force: true });
+  }
+}
+
+function execute(command: string, args: string[], cwd: string): Promise<string> {
+  return new Promise((resolve, reject) => {
+    const child = spawn(command, args, { cwd, shell: false });
+    let output = '';
+    child.stdout.on('data', d => output += d.toString());
+    child.stderr.on('data', d => output += d.toString());
+    child.on('error', reject);
+    child.on('close', () => resolve(output));
+  });
+}
+
+function uriToFilePath(uri: string): string {
+  if (uri.startsWith('file://')) return decodeURIComponent(new URL(uri).pathname);
+  return uri;
+}
+
+documents.listen(connection);
+connection.listen();

+ 8 - 0
extensions/oberon-language/tsconfig.json

@@ -0,0 +1,8 @@
+{
+  "compilerOptions": {
+    "target": "ES2022", "module": "commonjs", "lib": ["ES2022"],
+    "outDir": "out", "rootDir": "src", "strict": true,
+    "sourceMap": true, "esModuleInterop": true, "skipLibCheck": true
+  },
+  "include": ["src/**/*.ts"]
+}

+ 34 - 0
extensions/pascal-language/COCOR-LSP.md

@@ -0,0 +1,34 @@
+# Coco/R diagnostics through LSP
+
+Version 0.1.7 adds the first real Language Server connection. The Theia/VS
+Code extension starts `out/server.js` through `vscode-languageclient`; the
+server invokes a configured executable containing the **generated Coco/R
+parser/validator**.
+
+## Pascal dialects (`.pas` / `.pp` / `.p`)
+
+The same server validates Pascal files with the pilot validators from
+`grammars/`. Each pilot binary takes the source file as its single
+argument and writes a `.LST` listing next to it; the server parses the
+listing's `***** ^ message` lines (positions are caret-relative to the
+code after the listing prefix, zero-based columns).
+
+```json
+"modula2.pascal.dialect": "freepascal",
+"modula2.pascal.validators": {
+  "p2": "<root>/grammars/P2/build/P2",
+  "p4": "<root>/grammars/P4/build/P4",
+  "p6": "<root>/grammars/P6/build/P6",
+  "pascal": "<root>/grammars/Pascal/build/PStd",
+  "turbopascal3": "<root>/grammars/TurboPascal3/build/TP3",
+  "freepascal": "<root>/grammars/FreePascal/build/FPC",
+  "blaise": "<root>/grammars/Blaise/build/BlaiseV",
+  "pascals": "<root>/grammars/PascalS/build/PascalS"
+}
+```
+
+Build each binary first with its pilot `build.sh` (needs `CR` and
+`gm2 -fiso`). The local Modula-2 analysis never runs on Pascal text.
+With no command configured for the active dialect the server publishes
+no diagnostics. Never point these at the `pcom`/`psc` oracle compilers
+— they can hang on invalid input.

+ 10 - 0
extensions/pascal-language/language-configuration.json

@@ -0,0 +1,10 @@
+{
+  "comments": {"blockComment": ["(*", "*)"]},
+  "brackets": [["(", ")"], ["[", "]"]],
+  "autoClosingPairs": [{"open": "(", "close": ")"}, {"open": "[", "close": "]"}],
+  "surroundingPairs": [["(", ")"], ["[", "]"]],
+  "indentationRules": {
+    "increaseIndentPattern": "^\\s*(MODULE|DEFINITION\\s+MODULE|PROCEDURE|IF|ELSIF|ELSE|CASE|FOR|WHILE|REPEAT|LOOP|RECORD)\\b.*$",
+    "decreaseIndentPattern": "^\\s*(END|ELSE|ELSIF|UNTIL)\\b.*$"
+  }
+}

+ 77 - 0
extensions/pascal-language/package.json

@@ -0,0 +1,77 @@
+{
+  "name": "pascal-language",
+  "displayName": "Pascal Language Support",
+  "version": "0.1.5",
+  "publisher": "modula2",
+  "license": "MIT",
+  "engines": {
+    "vscode": "^1.90.0"
+  },
+  "categories": [
+    "Programming Languages"
+  ],
+  "activationEvents": [
+    "onLanguage:pascal"
+  ],
+  "main": "./out/extension.js",
+  "contributes": {
+    "languages": [
+      {
+        "id": "pascal",
+        "aliases": [
+          "Pascal",
+          "pascal"
+        ],
+        "extensions": [
+          ".pas",
+          ".pp",
+          ".p"
+        ],
+        "configuration": "./language-configuration.json"
+      }
+    ],
+    "configuration": {
+      "title": "Pascal",
+      "properties": {
+        "modula2.pascal.dialect": {
+          "type": "string",
+          "enum": [
+            "p2",
+            "p4",
+            "p6",
+            "pascal",
+            "turbopascal3",
+            "freepascal",
+            "blaise",
+            "pascals"
+          ],
+          "default": "freepascal",
+          "description": "Pascal dialect used to validate .pas files. Builds: grammars/P2/build/P2, P4/build/P4, P6/build/P6, Pascal/build/PStd, TurboPascal3/build/TP3, FreePascal/build/FPC, Blaise/build/BlaiseV, PascalS/build/PascalS."
+        },
+        "modula2.pascal.validators": {
+          "type": "object",
+          "default": {},
+          "description": "Validator command per Pascal dialect, e.g. {\"freepascal\": \"/path/to/FPC\"}. Each command receives the temporary source file as its single argument. Never point these at pcom/psc oracle compilers (they can hang)."
+        }
+      }
+    }
+  },
+  "scripts": {
+    "clean": "rimraf out",
+    "build": "tsc -p tsconfig.json",
+    "prepare": "yarn build",
+    "watch": "tsc -p tsconfig.json --watch"
+  },
+  "devDependencies": {
+    "@types/node": "^24.0.0",
+    "@types/vscode": "^1.90.0",
+    "rimraf": "^6.0.1",
+    "typescript": "^5.9.2"
+  },
+  "dependencies": {
+    "vscode-languageclient": "^9.0.1",
+    "vscode-languageserver": "^9.0.1",
+    "vscode-languageserver-textdocument": "^1.0.11"
+  },
+  "description": "Pascal language support with Coco/R dialect validators (P2, P4, P6, Pascal, Turbo Pascal 3, Free Pascal, Blaise, Pascal-S)."
+}

+ 26 - 0
extensions/pascal-language/src/extension.ts

@@ -0,0 +1,26 @@
+import * as vscode from 'vscode';
+import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind } from 'vscode-languageclient/node';
+
+export function activate(context: vscode.ExtensionContext): void {
+  const serverModule = context.asAbsolutePath('out/server.js');
+  const serverOptions: ServerOptions = {
+    run: { module: serverModule, transport: TransportKind.ipc },
+    debug: { module: serverModule, transport: TransportKind.ipc }
+  };
+  const cfg = vscode.workspace.getConfiguration('modula2');
+  const clientOptions: LanguageClientOptions = {
+    documentSelector: [
+      { scheme: 'file', language: 'pascal' }
+    ],
+    synchronize: { configurationSection: 'modula2' },
+    initializationOptions: {
+      pascalDialect: cfg.get<string>('pascal.dialect', 'freepascal'),
+      pascalValidators: cfg.get<object>('pascal.validators', {})
+    }
+  };
+  const client = new LanguageClient('pascalCocoR', 'Pascal Coco/R Language Server', serverOptions, clientOptions);
+  context.subscriptions.push(client);
+  void client.start();
+}
+
+export function deactivate(): void {}

+ 0 - 0
extensions/modula2-language/src/pascalListing.ts → extensions/pascal-language/src/pascalListing.ts


+ 101 - 0
extensions/pascal-language/src/server.ts

@@ -0,0 +1,101 @@
+import {
+  createConnection, ProposedFeatures, InitializeParams, InitializeResult,
+  TextDocuments, TextDocumentSyncKind, Diagnostic, DiagnosticSeverity,
+  DidChangeConfigurationParams
+} from 'vscode-languageserver/node';
+import { TextDocument } from 'vscode-languageserver-textdocument';
+import * as path from 'path';
+import * as os from 'os';
+import { promises as fs } from 'fs';
+import { spawn } from 'child_process';
+import { parseListing } from './pascalListing';
+
+const connection = createConnection(ProposedFeatures.all);
+const documents = new TextDocuments(TextDocument);
+let pascalDialect = 'freepascal';
+let pascalValidators: Record<string, string> = {};
+
+connection.onInitialize((params: InitializeParams): InitializeResult => {
+  const options = (params.initializationOptions || {}) as {
+    pascalDialect?: string; pascalValidators?: Record<string, string>;
+  };
+  pascalDialect = options.pascalDialect || 'freepascal';
+  pascalValidators = options.pascalValidators || {};
+  return {
+    capabilities: {
+      textDocumentSync: TextDocumentSyncKind.Full
+    }
+  };
+});
+
+connection.onDidChangeConfiguration((params: DidChangeConfigurationParams) => {
+  const settings = (params.settings || {}) as {
+    modula2?: {
+      pascal?: { dialect?: string; validators?: Record<string, string> };
+    };
+  };
+  pascalDialect = settings.modula2?.pascal?.dialect || pascalDialect;
+  pascalValidators = settings.modula2?.pascal?.validators || pascalValidators;
+});
+
+documents.onDidOpen(e => validate(e.document));
+documents.onDidChangeContent(e => validate(e.document));
+documents.onDidClose(e => connection.sendDiagnostics({ uri: e.document.uri, diagnostics: [] }));
+
+/** Validate a Pascal document with the configured dialect validator.
+ *
+ *  Each pilot validator takes the source file as its single argument and
+ *  writes `<base>.LST` next to it; diagnostics come from that listing, whose
+ *  `***** ^ message` lines point at the preceding source line.
+ *  No validator configured (or dialect unknown) means no diagnostics.
+ */
+async function validate(document: TextDocument): Promise<void> {
+  const command = pascalValidators[pascalDialect];
+  if (!command) {
+    connection.sendDiagnostics({ uri: document.uri, diagnostics: [] });
+    return;
+  }
+  const filePath = uriToFilePath(document.uri);
+  const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'pascal-lsp-'));
+  const base = path.basename(filePath, path.extname(filePath));
+  const tempFile = path.join(dir, base + '.pas');
+  try {
+    await fs.writeFile(tempFile, document.getText(), 'utf8');
+    await execute(command, [tempFile], path.dirname(filePath));
+    let listing = '';
+    try {
+      listing = await fs.readFile(path.join(dir, base + '.LST'), 'utf8');
+    } catch {
+      listing = '';
+    }
+    connection.sendDiagnostics({ uri: document.uri, diagnostics: parseListing(listing) });
+  } catch (error) {
+    connection.sendDiagnostics({ uri: document.uri, diagnostics: [{
+      severity: DiagnosticSeverity.Warning,
+      range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } },
+      message: `Pascal validator: ${error instanceof Error ? error.message : String(error)}`,
+      source: 'Pascal'
+    }] });
+  } finally {
+    await fs.rm(dir, { recursive: true, force: true });
+  }
+}
+
+function execute(command: string, args: string[], cwd: string): Promise<string> {
+  return new Promise((resolve, reject) => {
+    const child = spawn(command, args, { cwd, shell: false });
+    let output = '';
+    child.stdout.on('data', d => output += d.toString());
+    child.stderr.on('data', d => output += d.toString());
+    child.on('error', reject);
+    child.on('close', () => resolve(output));
+  });
+}
+
+function uriToFilePath(uri: string): string {
+  if (uri.startsWith('file://')) return decodeURIComponent(new URL(uri).pathname);
+  return uri;
+}
+
+documents.listen(connection);
+connection.listen();

+ 8 - 0
extensions/pascal-language/tsconfig.json

@@ -0,0 +1,8 @@
+{
+  "compilerOptions": {
+    "target": "ES2022", "module": "commonjs", "lib": ["ES2022"],
+    "outDir": "out", "rootDir": "src", "strict": true,
+    "sourceMap": true, "esModuleInterop": true, "skipLibCheck": true
+  },
+  "include": ["src/**/*.ts"]
+}

+ 4 - 2
package.json

@@ -14,12 +14,14 @@
     "extensions/modula2-debug",
     "extensions/modula2-language",
     "extensions/modula2-project",
-    "extensions/modula2-workbench"
+    "extensions/modula2-workbench",
+    "extensions/pascal-language",
+    "extensions/oberon-language"
   ],
   "scripts": {
     "prepare": "yarn --cwd extensions/modula2-compiler prepare && yarn --cwd extensions/modula2-debug prepare && yarn --cwd extensions/modula2-language prepare && yarn --cwd extensions/modula2-project prepare && yarn --cwd extensions/modula2-workbench prepare",
     "build": "yarn build:language && yarn build:compiler && yarn build:debug && yarn build:project && yarn build:workbench && yarn build:browser",
-    "build:language": "yarn --cwd extensions/modula2-language build && bash scripts/sync-language-plugin.sh",
+    "build:language": "yarn --cwd extensions/modula2-language build && yarn --cwd extensions/pascal-language build && yarn --cwd extensions/oberon-language build && bash scripts/sync-language-plugin.sh",
     "build:compiler": "yarn --cwd extensions/modula2-compiler build",
     "build:debug": "yarn --cwd extensions/modula2-debug build",
     "build:project": "yarn --cwd extensions/modula2-project build",

+ 7 - 5
scripts/sync-language-plugin.sh

@@ -1,8 +1,10 @@
 #!/usr/bin/env bash
 set -euo pipefail
 ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
-rm -rf "$ROOT/plugins/modula2-language"
-mkdir -p "$ROOT/plugins"
-cp -a "$ROOT/extensions/modula2-language" "$ROOT/plugins/modula2-language"
-rm -rf "$ROOT/plugins/modula2-language/src" "$ROOT/plugins/modula2-language/node_modules"
-echo "Language plugin synchronized to plugins/modula2-language"
+for plug in modula2-language pascal-language oberon-language; do
+  rm -rf "$ROOT/plugins/$plug"
+  mkdir -p "$ROOT/plugins"
+  cp -a "$ROOT/extensions/$plug" "$ROOT/plugins/$plug"
+  rm -rf "$ROOT/plugins/$plug/src" "$ROOT/plugins/$plug/node_modules"
+  echo "Language plugin synchronized to plugins/$plug"
+done