server.ts 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. import {
  2. createConnection, ProposedFeatures, InitializeParams, InitializeResult,
  3. TextDocuments, TextDocumentSyncKind, Diagnostic, DiagnosticSeverity,
  4. DidChangeConfigurationParams
  5. } from 'vscode-languageserver/node';
  6. import { TextDocument } from 'vscode-languageserver-textdocument';
  7. import * as path from 'path';
  8. import * as os from 'os';
  9. import { promises as fs } from 'fs';
  10. import { spawn } from 'child_process';
  11. import { parseListing } from './pascalListing';
  12. import { parsePascal, visibleAt, PasSymbol } from './pascalSymbols';
  13. import {
  14. CompletionItem, CompletionItemKind, DocumentSymbol, SymbolKind, Hover
  15. } from 'vscode-languageserver/node';
  16. const connection = createConnection(ProposedFeatures.all);
  17. const documents = new TextDocuments(TextDocument);
  18. let pascalDialect = 'freepascal';
  19. let pascalValidators: Record<string, string> = {};
  20. connection.onInitialize((params: InitializeParams): InitializeResult => {
  21. const options = (params.initializationOptions || {}) as {
  22. pascalDialect?: string; pascalValidators?: Record<string, string>;
  23. };
  24. pascalDialect = options.pascalDialect || 'freepascal';
  25. pascalValidators = options.pascalValidators || {};
  26. return {
  27. capabilities: {
  28. textDocumentSync: TextDocumentSyncKind.Full,
  29. completionProvider: { triggerCharacters: ['.'] },
  30. hoverProvider: true,
  31. documentSymbolProvider: true
  32. }
  33. };
  34. });
  35. connection.onDidChangeConfiguration((params: DidChangeConfigurationParams) => {
  36. const settings = (params.settings || {}) as {
  37. modula2?: {
  38. pascal?: { dialect?: string; validators?: Record<string, string> };
  39. };
  40. };
  41. pascalDialect = settings.modula2?.pascal?.dialect || pascalDialect;
  42. pascalValidators = settings.modula2?.pascal?.validators || pascalValidators;
  43. });
  44. documents.onDidOpen(e => validate(e.document));
  45. documents.onDidChangeContent(e => validate(e.document));
  46. documents.onDidClose(e => connection.sendDiagnostics({ uri: e.document.uri, diagnostics: [] }));
  47. /** Validate a Pascal document with the configured dialect validator.
  48. *
  49. * Each pilot validator takes the source file as its single argument and
  50. * writes `<base>.LST` next to it; diagnostics come from that listing, whose
  51. * `***** ^ message` lines point at the preceding source line.
  52. * No validator configured (or dialect unknown) means no diagnostics.
  53. */
  54. async function validate(document: TextDocument): Promise<void> {
  55. const command = pascalValidators[pascalDialect];
  56. if (!command) {
  57. connection.sendDiagnostics({ uri: document.uri, diagnostics: [] });
  58. return;
  59. }
  60. const filePath = uriToFilePath(document.uri);
  61. const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'pascal-lsp-'));
  62. const base = path.basename(filePath, path.extname(filePath));
  63. const tempFile = path.join(dir, base + '.pas');
  64. try {
  65. await fs.writeFile(tempFile, document.getText(), 'utf8');
  66. await execute(command, [tempFile], path.dirname(filePath));
  67. let listing = '';
  68. try {
  69. listing = await fs.readFile(path.join(dir, base + '.LST'), 'utf8');
  70. } catch {
  71. listing = '';
  72. }
  73. connection.sendDiagnostics({ uri: document.uri, diagnostics: parseListing(listing) });
  74. } catch (error) {
  75. connection.sendDiagnostics({ uri: document.uri, diagnostics: [{
  76. severity: DiagnosticSeverity.Warning,
  77. range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } },
  78. message: `Pascal validator: ${error instanceof Error ? error.message : String(error)}`,
  79. source: 'Pascal'
  80. }] });
  81. } finally {
  82. await fs.rm(dir, { recursive: true, force: true });
  83. }
  84. }
  85. function execute(command: string, args: string[], cwd: string): Promise<string> {
  86. return new Promise((resolve, reject) => {
  87. const child = spawn(command, args, { cwd, shell: false });
  88. let output = '';
  89. child.stdout.on('data', d => output += d.toString());
  90. child.stderr.on('data', d => output += d.toString());
  91. child.on('error', reject);
  92. child.on('close', () => resolve(output));
  93. });
  94. }
  95. function uriToFilePath(uri: string): string {
  96. if (uri.startsWith('file://')) return decodeURIComponent(new URL(uri).pathname);
  97. return uri;
  98. }
  99. // ---- Local intelligence (Outline, completion, hover) ----
  100. const PASCAL_KEYWORDS = [
  101. 'AND', 'ARRAY', 'BEGIN', 'CASE', 'CONST', 'DIV', 'DO', 'DOWNTO',
  102. 'ELSE', 'END', 'FILE', 'FOR', 'FUNCTION', 'GOTO', 'IF', 'IN',
  103. 'LABEL', 'MOD', 'NIL', 'NOT', 'OF', 'OR', 'PACKED', 'PROCEDURE',
  104. 'PROGRAM', 'RECORD', 'REPEAT', 'SET', 'THEN', 'TO', 'TYPE', 'UNIT',
  105. 'UNTIL', 'USES', 'VAR', 'WHILE', 'WITH', 'FORWARD'
  106. ];
  107. const PASCAL_BUILTINS = [
  108. 'READ', 'READLN', 'WRITE', 'WRITELN', 'EOF', 'EOLN', 'ODD', 'ORD',
  109. 'CHR', 'ABS', 'SQR', 'TRUNC', 'NEW', 'SIN', 'COS', 'EXP', 'LN'
  110. ];
  111. function symbolKind(kind: PasSymbol['kind']): SymbolKind {
  112. switch (kind) {
  113. case 'module': return SymbolKind.Module;
  114. case 'procedure':
  115. case 'function': return SymbolKind.Function;
  116. case 'constant': return SymbolKind.Constant;
  117. case 'type': return SymbolKind.Class;
  118. case 'variable':
  119. case 'parameter':
  120. case 'field': return SymbolKind.Variable;
  121. }
  122. }
  123. function toDocumentSymbol(s: PasSymbol): DocumentSymbol {
  124. const endLine = s.scopeEndLine ?? s.endLine;
  125. const endCh = s.scopeEndCh ?? s.endCh;
  126. return {
  127. name: s.name,
  128. kind: symbolKind(s.kind),
  129. range: {
  130. start: { line: s.line, character: s.ch },
  131. end: { line: endLine, character: endCh }
  132. },
  133. selectionRange: {
  134. start: { line: s.line, character: s.ch },
  135. end: { line: s.endLine, character: s.endCh }
  136. },
  137. detail: s.detail,
  138. children: s.children.map(toDocumentSymbol)
  139. };
  140. }
  141. connection.onDocumentSymbol(params => {
  142. const document = documents.get(params.textDocument.uri);
  143. if (!document) return [];
  144. try {
  145. return parsePascal(document.getText()).map(toDocumentSymbol);
  146. } catch {
  147. return [];
  148. }
  149. });
  150. connection.onCompletion(params => {
  151. const document = documents.get(params.textDocument.uri);
  152. if (!document) return [];
  153. let symbols: PasSymbol[] = [];
  154. try {
  155. symbols = parsePascal(document.getText());
  156. } catch {
  157. symbols = [];
  158. }
  159. const vis = visibleAt(symbols, params.position.line, params.position.character);
  160. const items: CompletionItem[] = vis.map(s => ({
  161. label: s.name,
  162. kind: s.kind === 'procedure' || s.kind === 'function'
  163. ? CompletionItemKind.Function
  164. : s.kind === 'constant' ? CompletionItemKind.Constant
  165. : s.kind === 'type' ? CompletionItemKind.Class
  166. : CompletionItemKind.Variable,
  167. detail: s.detail
  168. } as CompletionItem));
  169. for (const kw of PASCAL_KEYWORDS) {
  170. items.push({ label: kw, kind: CompletionItemKind.Keyword, detail: 'Pascal keyword' } as CompletionItem);
  171. }
  172. for (const b of PASCAL_BUILTINS) {
  173. items.push({ label: b, kind: CompletionItemKind.Function, detail: 'Pascal predefined routine' } as CompletionItem);
  174. }
  175. return items;
  176. });
  177. connection.onHover(params => {
  178. const document = documents.get(params.textDocument.uri);
  179. if (!document) return null;
  180. let symbols: PasSymbol[] = [];
  181. try {
  182. symbols = parsePascal(document.getText());
  183. } catch {
  184. return null;
  185. }
  186. const offset = document.offsetAt(params.position);
  187. const text = document.getText();
  188. let start = offset;
  189. while (start > 0 && /[A-Za-z0-9_]/.test(text[start - 1] ?? '')) start--;
  190. let end = offset;
  191. while (end < text.length && /[A-Za-z0-9_]/.test(text[end] ?? '')) end++;
  192. const word = text.slice(start, end);
  193. if (!word) return null;
  194. const upper = word.toUpperCase();
  195. const hit = visibleAt(symbols, params.position.line, params.position.character)
  196. .find(s => s.name.toUpperCase() === upper);
  197. const show = hit ?? (PASCAL_BUILTINS.includes(upper)
  198. ? { name: upper, kind: 'procedure' as const, detail: 'Pascal predefined routine' }
  199. : null);
  200. if (!show) return null;
  201. const hover: Hover = {
  202. contents: {
  203. kind: 'markdown',
  204. value: ['```pascal', show.detail || show.name, '```', `*${show.kind}*`].join('\n')
  205. }
  206. };
  207. return hover;
  208. });
  209. documents.listen(connection);
  210. connection.listen();