diagnostics.ts 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. import * as vscode from 'vscode';
  2. const MODULE_HEADER = /\b(DEFINITION\s+)?(IMPLEMENTATION\s+)?MODULE\s+([A-Za-z_][A-Za-z0-9_]*)/i;
  3. const END_MODULE = /\bEND\s+([A-Za-z_][A-Za-z0-9_]*)\s*(\.)?/gi;
  4. interface ScanResult {
  5. diagnostics: vscode.Diagnostic[];
  6. code: string;
  7. }
  8. function positionAt(document: vscode.TextDocument, offset: number): vscode.Position {
  9. return document.positionAt(Math.max(0, Math.min(offset, document.getText().length)));
  10. }
  11. function rangeAt(document: vscode.TextDocument, start: number, end: number): vscode.Range {
  12. return new vscode.Range(positionAt(document, start), positionAt(document, end));
  13. }
  14. function diagnostic(
  15. document: vscode.TextDocument,
  16. start: number,
  17. end: number,
  18. message: string,
  19. severity = vscode.DiagnosticSeverity.Error
  20. ): vscode.Diagnostic {
  21. const item = new vscode.Diagnostic(rangeAt(document, start, end), message, severity);
  22. item.source = 'Modula-2';
  23. return item;
  24. }
  25. /**
  26. * Lightweight diagnostics used until the Coco/R parser is connected.
  27. *
  28. * This intentionally checks only constructs that can be recognised without
  29. * pretending to be a complete Modula-2 parser: unterminated comments/strings,
  30. * module headers, and the final module END. The real syntax diagnostics will
  31. * be supplied by the Coco/R parser in the next stage.
  32. */
  33. function scan(document: vscode.TextDocument): ScanResult {
  34. const text = document.getText();
  35. const diagnostics: vscode.Diagnostic[] = [];
  36. let code = '';
  37. let i = 0;
  38. while (i < text.length) {
  39. const ch = text[i];
  40. const next = text[i + 1];
  41. if (ch === '(' && next === '*') {
  42. const start = i;
  43. i += 2;
  44. let closed = false;
  45. while (i < text.length) {
  46. if (text[i] === '*' && text[i + 1] === ')') {
  47. i += 2;
  48. closed = true;
  49. break;
  50. }
  51. i++;
  52. }
  53. if (!closed) {
  54. diagnostics.push(diagnostic(document, start, Math.min(start + 2, text.length), 'Unterminated Modula-2 comment.'));
  55. }
  56. code += ' ';
  57. continue;
  58. }
  59. if (ch === '"') {
  60. const start = i;
  61. i++;
  62. let closed = false;
  63. while (i < text.length) {
  64. if (text[i] === '\\') {
  65. i += 2;
  66. continue;
  67. }
  68. if (text[i] === '"') {
  69. i++;
  70. closed = true;
  71. break;
  72. }
  73. i++;
  74. }
  75. if (!closed) {
  76. diagnostics.push(diagnostic(document, start, Math.min(start + 1, text.length), 'Unterminated string literal.'));
  77. }
  78. code += ' ';
  79. continue;
  80. }
  81. code += ch;
  82. i++;
  83. }
  84. const header = MODULE_HEADER.exec(code);
  85. if (!header) {
  86. const isDefinition = document.languageId === 'modula2-definition';
  87. const expected = isDefinition ? 'DEFINITION MODULE' : 'MODULE or IMPLEMENTATION MODULE';
  88. diagnostics.push(diagnostic(document, 0, Math.min(code.length, 1), `Expected a ${expected} header.`));
  89. return { diagnostics, code };
  90. }
  91. const moduleName = header[3];
  92. const headerEnd = header.index + header[0].length;
  93. const semicolon = code.indexOf(';', headerEnd);
  94. if (semicolon < 0) {
  95. diagnostics.push(diagnostic(document, headerEnd, Math.min(headerEnd + moduleName.length, code.length), 'Expected ";" after the module header.'));
  96. }
  97. END_MODULE.lastIndex = 0;
  98. let match: RegExpExecArray | null = null;
  99. let lastEnd: RegExpExecArray | null = null;
  100. while ((match = END_MODULE.exec(code)) !== null) {
  101. lastEnd = match;
  102. }
  103. if (!lastEnd) {
  104. diagnostics.push(diagnostic(document, Math.max(0, code.trimEnd().length - 1), code.length, `Expected "END ${moduleName}." at the end of the module.`));
  105. } else {
  106. const endName = lastEnd[1];
  107. const nameStart = lastEnd.index + lastEnd[0].indexOf(endName);
  108. if (endName.toUpperCase() !== moduleName.toUpperCase()) {
  109. diagnostics.push(diagnostic(document, nameStart, nameStart + endName.length, `Module name "${moduleName}" expected after END, found "${endName}".`));
  110. }
  111. if (!lastEnd[2]) {
  112. const dot = lastEnd.index + lastEnd[0].length;
  113. diagnostics.push(diagnostic(document, dot, Math.min(dot + 1, code.length), `Expected "." after END ${endName}.`));
  114. }
  115. }
  116. return { diagnostics, code };
  117. }
  118. export function createDiagnostics(context: vscode.ExtensionContext): void {
  119. const collection = vscode.languages.createDiagnosticCollection('modula2');
  120. context.subscriptions.push(collection);
  121. const update = (document: vscode.TextDocument): void => {
  122. if (document.languageId !== 'modula2' && document.languageId !== 'modula2-definition') {
  123. return;
  124. }
  125. collection.set(document.uri, scan(document).diagnostics);
  126. };
  127. for (const document of vscode.workspace.textDocuments) {
  128. update(document);
  129. }
  130. context.subscriptions.push(
  131. vscode.workspace.onDidOpenTextDocument(update),
  132. vscode.workspace.onDidChangeTextDocument(event => update(event.document)),
  133. vscode.workspace.onDidCloseTextDocument(document => collection.delete(document.uri))
  134. );
  135. }