| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226 |
- 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<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,
- completionProvider: { triggerCharacters: ['.'] },
- hoverProvider: true,
- documentSymbolProvider: true
- }
- };
- });
- 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;
- }
- // ---- 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();
|