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'; import { parsePascal, visibleAt, PasSymbol } from './pascalSymbols'; import { CompletionItem, CompletionItemKind, DocumentSymbol, SymbolKind, Hover } from 'vscode-languageserver/node'; const connection = createConnection(ProposedFeatures.all); const documents = new TextDocuments(TextDocument); let pascalDialect = 'freepascal'; let pascalValidators: Record = {}; connection.onInitialize((params: InitializeParams): InitializeResult => { const options = (params.initializationOptions || {}) as { pascalDialect?: string; pascalValidators?: Record; }; pascalDialect = options.pascalDialect || 'freepascal'; pascalValidators = options.pascalValidators || {}; return { capabilities: { textDocumentSync: TextDocumentSyncKind.Full, completionProvider: { triggerCharacters: ['.'] }, hoverProvider: true, documentSymbolProvider: true } }; }); connection.onDidChangeConfiguration((params: DidChangeConfigurationParams) => { const settings = (params.settings || {}) as { modula2?: { pascal?: { dialect?: string; validators?: Record }; }; }; 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 `.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 { 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 { 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; } // ---- Local intelligence (Outline, completion, hover) ---- const PASCAL_KEYWORDS = [ 'AND', 'ARRAY', 'BEGIN', 'CASE', 'CONST', 'DIV', 'DO', 'DOWNTO', 'ELSE', 'END', 'FILE', 'FOR', 'FUNCTION', 'GOTO', 'IF', 'IN', 'LABEL', 'MOD', 'NIL', 'NOT', 'OF', 'OR', 'PACKED', 'PROCEDURE', 'PROGRAM', 'RECORD', 'REPEAT', 'SET', 'THEN', 'TO', 'TYPE', 'UNIT', 'UNTIL', 'USES', 'VAR', 'WHILE', 'WITH', 'FORWARD' ]; const PASCAL_BUILTINS = [ 'READ', 'READLN', 'WRITE', 'WRITELN', 'EOF', 'EOLN', 'ODD', 'ORD', 'CHR', 'ABS', 'SQR', 'TRUNC', 'NEW', 'SIN', 'COS', 'EXP', 'LN' ]; function symbolKind(kind: PasSymbol['kind']): SymbolKind { switch (kind) { case 'module': return SymbolKind.Module; case 'procedure': case 'function': return SymbolKind.Function; case 'constant': return SymbolKind.Constant; case 'type': return SymbolKind.Class; case 'variable': case 'parameter': case 'field': return SymbolKind.Variable; } } function toDocumentSymbol(s: PasSymbol): DocumentSymbol { const endLine = s.scopeEndLine ?? s.endLine; const endCh = s.scopeEndCh ?? s.endCh; return { name: s.name, kind: symbolKind(s.kind), range: { start: { line: s.line, character: s.ch }, end: { line: endLine, character: endCh } }, selectionRange: { start: { line: s.line, character: s.ch }, end: { line: s.endLine, character: s.endCh } }, detail: s.detail, children: s.children.map(toDocumentSymbol) }; } connection.onDocumentSymbol(params => { const document = documents.get(params.textDocument.uri); if (!document) return []; try { return parsePascal(document.getText()).map(toDocumentSymbol); } catch { return []; } }); connection.onCompletion(params => { const document = documents.get(params.textDocument.uri); if (!document) return []; let symbols: PasSymbol[] = []; try { symbols = parsePascal(document.getText()); } catch { symbols = []; } const vis = visibleAt(symbols, params.position.line, params.position.character); const items: CompletionItem[] = vis.map(s => ({ label: s.name, kind: s.kind === 'procedure' || s.kind === 'function' ? CompletionItemKind.Function : s.kind === 'constant' ? CompletionItemKind.Constant : s.kind === 'type' ? CompletionItemKind.Class : CompletionItemKind.Variable, detail: s.detail } as CompletionItem)); for (const kw of PASCAL_KEYWORDS) { items.push({ label: kw, kind: CompletionItemKind.Keyword, detail: 'Pascal keyword' } as CompletionItem); } for (const b of PASCAL_BUILTINS) { items.push({ label: b, kind: CompletionItemKind.Function, detail: 'Pascal predefined routine' } as CompletionItem); } return items; }); connection.onHover(params => { const document = documents.get(params.textDocument.uri); if (!document) return null; let symbols: PasSymbol[] = []; try { symbols = parsePascal(document.getText()); } catch { return null; } const offset = document.offsetAt(params.position); const text = document.getText(); let start = offset; while (start > 0 && /[A-Za-z0-9_]/.test(text[start - 1] ?? '')) start--; let end = offset; while (end < text.length && /[A-Za-z0-9_]/.test(text[end] ?? '')) end++; const word = text.slice(start, end); if (!word) return null; const upper = word.toUpperCase(); const hit = visibleAt(symbols, params.position.line, params.position.character) .find(s => s.name.toUpperCase() === upper); const show = hit ?? (PASCAL_BUILTINS.includes(upper) ? { name: upper, kind: 'procedure' as const, detail: 'Pascal predefined routine' } : null); if (!show) return null; const hover: Hover = { contents: { kind: 'markdown', value: ['```pascal', show.detail || show.name, '```', `*${show.kind}*`].join('\n') } }; return hover; }); documents.listen(connection); connection.listen();