oberonSymbols.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595
  1. /** Local Oberon symbol model for Outline, completion and hover.
  2. *
  3. * Conservative by design: only declaration shapes recognised with full
  4. * confidence are emitted (misses are acceptable, wrong entries are not).
  5. * Bodies are skipped with depth counting, so statement-level noise can
  6. * never leak into the symbol table. Scopes: module globals plus one
  7. * routine level (receiver, params and locals); deeper nesting folds
  8. * into its owner. Covers Oberon-07, Oberon-2 and Ulm Oberon shapes;
  9. * Oberon is case-sensitive throughout.
  10. */
  11. export interface ObSymbol {
  12. name: string;
  13. kind: 'procedure' | 'function' | 'constant' | 'type' | 'variable' | 'parameter' | 'field' | 'module';
  14. detail: string;
  15. line: number; ch: number; endLine: number; endCh: number;
  16. children: ObSymbol[];
  17. /** Last line covered by this scope (routines/modules), if known. */
  18. scopeEndLine?: number;
  19. scopeEndCh?: number;
  20. }
  21. export interface Tok {
  22. text: string;
  23. upper: string;
  24. line: number; ch: number; endLine: number; endCh: number;
  25. }
  26. /** Strip comments and string literals (length-preserving) so the
  27. * declaration scan never sees keywords inside them. Exported for the
  28. * resolver's occurrence scan, which needs identical masking. */
  29. export function mask(text: string): string {
  30. const out = text.split('');
  31. const n = text.length;
  32. let i = 0;
  33. const blank = (a: number, b: number) => { for (let k = a; k < b; k++) if (out[k] !== '\n') out[k] = ' '; };
  34. while (i < n) {
  35. const c = text[i];
  36. const nx = i + 1 < n ? text[i + 1] : '';
  37. if (c === '(' && nx === '*') {
  38. let depth = 1;
  39. let j = i + 2;
  40. while (j < n && depth > 0) {
  41. if (text[j] === '(' && text[j + 1] === '*') { depth++; j += 2; }
  42. else if (text[j] === '*' && text[j + 1] === ')') { depth--; j += 2; }
  43. else j++;
  44. }
  45. blank(i, j);
  46. i = j;
  47. } else if (c === '"' || c === "'") {
  48. let j = i + 1;
  49. while (j < n && text[j] !== c && text[j] !== '\n') j++;
  50. blank(i, Math.min(j + 1, n));
  51. i = Math.min(j + 1, n);
  52. } else {
  53. i++;
  54. }
  55. }
  56. return out.join('');
  57. }
  58. export function lex(masked: string): Tok[] {
  59. const toks: Tok[] = [];
  60. const re = /[A-Za-z_][A-Za-z0-9_]*|\d+(?:\.\d+)?(?:[EDed][+-]?\d+)?|:=|<=|>=|\.\.|[^\sA-Za-z0-9_]/g;
  61. let m: RegExpExecArray | null;
  62. // Incremental line tracking (O(n)); never re-slices from the file start.
  63. let line = 0;
  64. let prevEnd = 0;
  65. while ((m = re.exec(masked)) !== null) {
  66. for (let k = prevEnd; k < m.index; k++) {
  67. if (masked[k] === '\n') line++;
  68. }
  69. const ch = m.index - (masked.lastIndexOf('\n', m.index - 1) + 1);
  70. const mid = m[0].split('\n');
  71. toks.push({
  72. text: m[0], upper: m[0].toUpperCase(),
  73. line, ch,
  74. endLine: line + mid.length - 1,
  75. endCh: mid.length > 1 ? mid[mid.length - 1].length : ch + m[0].length
  76. });
  77. line += mid.length - 1;
  78. prevEnd = m.index + m[0].length;
  79. }
  80. return toks;
  81. }
  82. export const isIdent = (t: Tok) => /^[A-Za-z_]/.test(t.text);
  83. class Parser {
  84. pos = 0;
  85. constructor(readonly toks: Tok[], readonly marks?: Set<string>) {}
  86. peek(off = 0): Tok | null {
  87. return this.pos + off < this.toks.length ? this.toks[this.pos + off] : null;
  88. }
  89. at(word: string): boolean {
  90. const t = this.peek();
  91. return !!t && t.upper === word;
  92. }
  93. next(): Tok {
  94. return this.toks[this.pos++];
  95. }
  96. /** Skip a parenthesis/bracket group, returns true when balanced. */
  97. skipGroup(open: string, close: string): boolean {
  98. if (!this.at(open)) return false;
  99. let depth = 0;
  100. while (this.pos < this.toks.length) {
  101. const t = this.next();
  102. if (t.upper === open) depth++;
  103. else if (t.upper === close) {
  104. depth--;
  105. if (depth === 0) return true;
  106. }
  107. }
  108. return false;
  109. }
  110. /** Skip a routine body: declarations then an optional compound. */
  111. skipBody(): void {
  112. for (;;) {
  113. const t = this.peek();
  114. if (!t) return;
  115. if (t.upper === 'BEGIN') {
  116. this.skipCompound();
  117. return;
  118. }
  119. if (['CONST', 'TYPE', 'VAR', 'PROCEDURE'].includes(t.upper)) {
  120. this.skipSection();
  121. continue;
  122. }
  123. return;
  124. }
  125. }
  126. skipCompound(): void {
  127. if (!this.at('BEGIN')) return;
  128. this.next();
  129. let depth = 1;
  130. while (this.pos < this.toks.length && depth > 0) {
  131. const t = this.next();
  132. // BEGIN/CASE/RECORD/LOOP/WITH each consume their own END.
  133. if (t.upper === 'BEGIN' || t.upper === 'CASE' || t.upper === 'RECORD' ||
  134. t.upper === 'LOOP' || t.upper === 'WITH') depth++;
  135. else if (t.upper === 'END') depth--;
  136. }
  137. }
  138. /** Skip one declaration section (const/type/var). */
  139. skipSection(): void {
  140. const kw = this.peek()?.upper;
  141. if (kw !== 'CONST' && kw !== 'TYPE' && kw !== 'VAR') return;
  142. this.next();
  143. for (;;) {
  144. const t = this.peek();
  145. if (!t) return;
  146. if (['CONST', 'TYPE', 'VAR', 'PROCEDURE', 'BEGIN', 'END'].includes(t.upper)) return;
  147. this.skipDeclItem();
  148. if (this.at(';')) this.next();
  149. else return;
  150. }
  151. }
  152. /** Skip one declaration item up to `;` at depth 0. */
  153. skipDeclItem(): void {
  154. let depth = 0;
  155. while (this.pos < this.toks.length) {
  156. const t = this.peek()!;
  157. if (t.upper === '(' || t.upper === '[') depth++;
  158. else if (t.upper === ')' || t.upper === ']') depth--;
  159. else if (t.upper === 'RECORD' || t.upper === 'CASE') depth++;
  160. else if (t.upper === 'END' && depth > 0) depth--;
  161. else if (depth === 0 && (t.upper === ';' || ['CONST', 'TYPE', 'VAR', 'PROCEDURE', 'BEGIN', 'END'].includes(t.upper))) return;
  162. this.next();
  163. }
  164. }
  165. /** Parse an identifier list with optional `*`/`-` export marks,
  166. * returning the name tokens. Mark positions are recorded for the
  167. * formatter (export marks attach; operators keep spaces). */
  168. nameList(): Tok[] {
  169. const out: Tok[] = [];
  170. for (;;) {
  171. const t = this.peek();
  172. if (!t || !isIdent(t)) return out;
  173. out.push(this.next());
  174. const mark = this.peek();
  175. if (mark?.text === '*' || mark?.text === '-') {
  176. this.next();
  177. this.marks?.add(`${mark.line}:${mark.ch}`);
  178. }
  179. if (this.at(',')) { this.next(); continue; }
  180. return out;
  181. }
  182. }
  183. /** Consume an optional `*`/`-` export mark after a declared name. */
  184. exportMark(): void {
  185. const mark = this.peek();
  186. if (mark?.text === '*' || mark?.text === '-') {
  187. this.next();
  188. this.marks?.add(`${mark.line}:${mark.ch}`);
  189. }
  190. }
  191. }
  192. /** Parse a `RECORD ... END` field list; RECORD already consumed.
  193. *
  194. * Only plain `name [, ...] : type;` items at depth 1 become fields
  195. * (export marks stripped). Anything unrecognised is skipped without
  196. * emitting. The closing END is consumed. */
  197. export function parseRecordFields(p: Parser): ObSymbol[] {
  198. const fields: ObSymbol[] = [];
  199. let depth = 1;
  200. for (;;) {
  201. const t = p.peek();
  202. if (!t) return fields;
  203. if (t.upper === 'RECORD' || t.upper === 'CASE') { depth++; p.next(); continue; }
  204. if (t.upper === 'END') {
  205. depth--;
  206. p.next();
  207. if (depth <= 0) return fields;
  208. continue;
  209. }
  210. if (depth !== 1) { p.next(); continue; }
  211. if (!isIdent(t)) {
  212. p.skipDeclItem();
  213. if (p.at(';')) p.next();
  214. else if (p.peek() && p.peek()!.upper !== 'END') p.next();
  215. continue;
  216. }
  217. if (['CONST', 'TYPE', 'VAR', 'PROCEDURE', 'BEGIN'].includes(t.upper)) {
  218. while (p.pos < p.toks.length && depth > 0) {
  219. const k = p.next();
  220. if (k.upper === 'RECORD' || k.upper === 'CASE') depth++;
  221. else if (k.upper === 'END') depth--;
  222. }
  223. return fields;
  224. }
  225. const names = p.nameList();
  226. if (!p.at(':')) { p.skipDeclItem(); if (p.at(';')) p.next(); continue; }
  227. p.next();
  228. const typeToks: string[] = [];
  229. let dd = 0;
  230. while (p.pos < p.toks.length) {
  231. const k = p.peek()!;
  232. if (k.upper === '(' || k.upper === '[') dd++;
  233. else if (k.upper === ')' || k.upper === ']') dd--;
  234. else if (k.upper === 'RECORD' || k.upper === 'CASE') dd++;
  235. else if (k.upper === 'END' && dd > 0) dd--;
  236. else if (dd === 0 && (k.upper === ';' || k.upper === 'END')) break;
  237. else if (dd === 0 && ['CONST', 'TYPE', 'VAR', 'PROCEDURE', 'BEGIN'].includes(k.upper)) break;
  238. typeToks.push(k.text);
  239. p.next();
  240. }
  241. const typeText = typeToks.join(' ').replace(/\s+([,;:()[\]])/g, '$1').replace(/([(\[])\s+/g, '$1').replace(/\s*\.\s*/g, '.');
  242. for (const nm of names) {
  243. fields.push({
  244. name: nm.text, kind: 'field', detail: `${nm.text}: ${typeText}`,
  245. line: nm.line, ch: nm.ch, endLine: nm.endLine, endCh: nm.endCh, children: []
  246. });
  247. }
  248. if (p.at(';')) p.next();
  249. }
  250. }
  251. /** An `IMPORT` clause entry: local name, real module, positions. */
  252. export interface ObImport {
  253. /** Name used in this file (alias when `alias := Module` was written). */
  254. name: string;
  255. /** Real module name (after `:=`, or the name itself). */
  256. module: string;
  257. line: number; ch: number; endCh: number;
  258. }
  259. /** All `IMPORT a, b, alias := C, ...;` entries at paren-depth 0.
  260. *
  261. * An entry not shaped `ident [:=> ident]` (or a clause without a
  262. * terminating `;`) yields nothing. */
  263. export function parseImports(text: string): ObImport[] {
  264. const toks = lex(mask(text));
  265. const out: ObImport[] = [];
  266. let depth = 0;
  267. for (let k = 0; k < toks.length; k++) {
  268. const t = toks[k];
  269. if (t.upper === '(') { depth++; continue; }
  270. if (t.upper === ')') { depth--; continue; }
  271. if (depth !== 0 || t.upper !== 'IMPORT') continue;
  272. let j = k + 1;
  273. let ok = false;
  274. for (;;) {
  275. const u = toks[j];
  276. if (!u) break;
  277. if (u.upper === ';') { ok = true; break; }
  278. if (u.upper === 'BEGIN' || u.upper === 'END' || u.upper === 'CONST' ||
  279. u.upper === 'TYPE' || u.upper === 'VAR' || u.upper === 'PROCEDURE') break;
  280. if (!isIdent(u)) {
  281. if (u.upper !== ',') break;
  282. j++;
  283. continue;
  284. }
  285. // ident [ := ident ] followed by ',' or ';'
  286. let real = u.text;
  287. let endCh = u.endCh;
  288. let jj = j + 1;
  289. if (toks[jj]?.text === ':=') {
  290. const mod = toks[jj + 1];
  291. if (!mod || !isIdent(mod)) break;
  292. real = mod.text;
  293. endCh = mod.endCh;
  294. jj += 2;
  295. }
  296. const after = toks[jj];
  297. if (!after || (after.upper !== ',' && after.upper !== ';')) break;
  298. out.push({ name: u.text, module: real, line: u.line, ch: u.ch, endCh });
  299. j = jj;
  300. if (after.upper === ';') { ok = true; break; }
  301. j++;
  302. }
  303. if (ok) k = j;
  304. }
  305. return out;
  306. }
  307. /** Parse the declarations of one scope; stops before BEGIN/END/EOF. */
  308. function parseScope(p: Parser, out: ObSymbol[], stopAtBegin: boolean): void {
  309. for (;;) {
  310. const t = p.peek();
  311. if (!t) return;
  312. const u = t.upper;
  313. if (u === 'BEGIN') {
  314. if (stopAtBegin) return;
  315. p.skipBody();
  316. return;
  317. }
  318. if (u === 'END') return;
  319. if (u === 'CONST') {
  320. p.next();
  321. for (;;) {
  322. const n = p.peek();
  323. if (!n || !isIdent(n)) break;
  324. if (['TYPE', 'VAR', 'CONST', 'PROCEDURE', 'BEGIN', 'END'].includes(n.upper)) break;
  325. const nm = p.next();
  326. p.exportMark();
  327. if (!p.at('=')) { p.skipDeclItem(); if (p.at(';')) p.next(); continue; }
  328. p.next();
  329. p.skipDeclItem();
  330. out.push({ name: nm.text, kind: 'constant', detail: `CONST ${nm.text}`, line: nm.line, ch: nm.ch, endLine: nm.endLine, endCh: nm.endCh, children: [] });
  331. if (p.at(';')) p.next();
  332. else break;
  333. }
  334. continue;
  335. }
  336. if (u === 'TYPE') {
  337. p.next();
  338. for (;;) {
  339. const n = p.peek();
  340. if (!n || !isIdent(n)) break;
  341. if (['TYPE', 'VAR', 'CONST', 'PROCEDURE', 'BEGIN', 'END'].includes(n.upper)) break;
  342. const nm = p.next();
  343. p.exportMark();
  344. if (!p.at('=')) { p.skipDeclItem(); if (p.at(';')) p.next(); continue; }
  345. p.next();
  346. const typeChildren: ObSymbol[] = [];
  347. if (p.at('RECORD')) {
  348. p.next();
  349. typeChildren.push(...parseRecordFields(p));
  350. } else {
  351. p.skipDeclItem();
  352. }
  353. out.push({ name: nm.text, kind: 'type', detail: `TYPE ${nm.text}`, line: nm.line, ch: nm.ch, endLine: nm.endLine, endCh: nm.endCh, children: typeChildren });
  354. if (p.at(';')) p.next();
  355. else break;
  356. }
  357. continue;
  358. }
  359. if (u === 'VAR') {
  360. p.next();
  361. for (;;) {
  362. const n = p.peek();
  363. if (!n || !isIdent(n)) break;
  364. if (['TYPE', 'VAR', 'CONST', 'PROCEDURE', 'BEGIN', 'END'].includes(n.upper)) break;
  365. const names = p.nameList();
  366. if (!p.at(':')) { p.skipDeclItem(); if (p.at(';')) p.next(); continue; }
  367. p.next();
  368. if (p.at('RECORD')) {
  369. p.next();
  370. const fields = parseRecordFields(p);
  371. for (const nm of names) {
  372. out.push({ name: nm.text, kind: 'variable', detail: `VAR ${nm.text}: RECORD`, line: nm.line, ch: nm.ch, endLine: nm.endLine, endCh: nm.endCh, children: fields });
  373. }
  374. if (p.at(';')) p.next();
  375. else break;
  376. continue;
  377. }
  378. // capture type text up to ';' at depth 0
  379. const typeToks: string[] = [];
  380. let depth = 0;
  381. while (p.pos < p.toks.length) {
  382. const k = p.peek()!;
  383. if (k.upper === '(' || k.upper === '[') depth++;
  384. else if (k.upper === ')' || k.upper === ']') depth--;
  385. else if (k.upper === 'RECORD' || k.upper === 'CASE') depth++;
  386. else if (k.upper === 'END' && depth > 0) depth--;
  387. else if (depth === 0 && k.upper === ';') break;
  388. else if (depth === 0 && ['CONST', 'TYPE', 'VAR', 'PROCEDURE', 'BEGIN', 'END'].includes(k.upper)) break;
  389. typeToks.push(k.text);
  390. p.next();
  391. }
  392. const typeText = typeToks.join(' ').replace(/\s+([,;:()[\]])/g, '$1').replace(/([(\[])\s+/g, '$1').replace(/\s*\.\s*/g, '.');
  393. for (const nm of names) {
  394. out.push({ name: nm.text, kind: 'variable', detail: `VAR ${nm.text}: ${typeText}`, line: nm.line, ch: nm.ch, endLine: nm.endLine, endCh: nm.endCh, children: [] });
  395. }
  396. if (p.at(';')) p.next();
  397. else break;
  398. }
  399. continue;
  400. }
  401. if (u === 'PROCEDURE') {
  402. const kw = p.next();
  403. // Optional receiver `( [VAR] self [, ...] : Type )`: a type-bound
  404. // procedure. Its names are in scope in the body, like parameters.
  405. const receiver: Tok[] = [];
  406. if (p.at('(')) {
  407. p.next();
  408. let rd = 1;
  409. let sawColon = false;
  410. while (p.pos < p.toks.length && rd > 0) {
  411. const q = p.next();
  412. if (q.upper === '(') { rd++; continue; }
  413. if (q.upper === ')') {
  414. rd--;
  415. if (rd === 0) break;
  416. continue;
  417. }
  418. if (rd !== 1) continue;
  419. if (q.upper === ':') { sawColon = true; continue; }
  420. if (q.upper === ';' || q.upper === ',') continue;
  421. if (!sawColon && isIdent(q) && q.upper !== 'VAR' && q.upper !== 'IN') receiver.push(q);
  422. }
  423. if (rd !== 0) { p.skipDeclItem(); if (p.at(';')) p.next(); continue; }
  424. }
  425. const nt = p.peek();
  426. if (!nt || !isIdent(nt)) {
  427. p.skipDeclItem();
  428. if (p.at(';')) p.next();
  429. continue;
  430. }
  431. const nameTok = p.next();
  432. p.exportMark();
  433. // formal params
  434. const params: ObSymbol[] = [];
  435. if (p.at('(')) {
  436. p.next();
  437. let pd = 1;
  438. const pnames: Tok[] = [];
  439. let ptype: string[] = [];
  440. let inType = false;
  441. const flush = () => {
  442. const tt = ptype.join(' ').replace(/\s+([,;:()[\]])/g, '$1').replace(/([(\[])\s+/g, '$1').replace(/\s*\.\s*/g, '.');
  443. for (const nm of pnames) {
  444. params.push({ name: nm.text, kind: 'parameter', detail: `${nm.text}: ${tt}`, line: nm.line, ch: nm.ch, endLine: nm.endLine, endCh: nm.endCh, children: [] });
  445. }
  446. pnames.length = 0; ptype = []; inType = false;
  447. };
  448. while (p.pos < p.toks.length && pd > 0) {
  449. const k = p.next();
  450. if (k.upper === '(') { pd++; if (inType) ptype.push(k.text); continue; }
  451. if (k.upper === ')') { pd--; if (pd === 0) { if (pnames.length || ptype.length) flush(); break; } if (inType) ptype.push(k.text); continue; }
  452. if (pd !== 1) { if (inType) ptype.push(k.text); continue; }
  453. if (k.upper === ';') { flush(); continue; }
  454. if (k.upper === ':' && !inType) { inType = true; continue; }
  455. if (k.upper === ',') continue;
  456. if ((k.upper === 'VAR' || k.upper === 'IN') && !inType && pnames.length === 0) continue;
  457. if (k.text === '*' || k.text === '-') continue;
  458. if (isIdent(k)) { if (inType) ptype.push(k.text); else pnames.push(k); }
  459. else if (inType) ptype.push(k.text);
  460. }
  461. }
  462. // result type
  463. let restype = '';
  464. if (p.at(':')) {
  465. p.next();
  466. const rt: string[] = [];
  467. while (p.pos < p.toks.length) {
  468. const k = p.peek()!;
  469. if (k.upper === ';') break;
  470. rt.push(k.text);
  471. p.next();
  472. }
  473. restype = rt.join(' ').replace(/\s+([,;:()[\]])/g, '$1').replace(/([(\[])\s+/g, '$1').replace(/\s*\.\s*/g, '.');
  474. }
  475. const sym: ObSymbol = {
  476. name: nameTok.text,
  477. kind: 'procedure',
  478. detail: `PROCEDURE ${nameTok.text}${restype ? ': ' + restype : ''}`,
  479. line: kw.line, ch: kw.ch, endLine: nameTok.endLine, endCh: nameTok.endCh,
  480. children: [
  481. ...receiver.map(nm => ({
  482. name: nm.text, kind: 'parameter' as const,
  483. detail: `${nm.text}: receiver`,
  484. line: nm.line, ch: nm.ch, endLine: nm.endLine, endCh: nm.endCh, children: [] as ObSymbol[]
  485. })),
  486. ...params
  487. ]
  488. };
  489. out.push(sym);
  490. if (p.at(';')) p.next();
  491. for (;;) {
  492. const k = p.peek();
  493. if (!k) break;
  494. if (k.upper === 'BEGIN') { p.skipBody(); break; }
  495. if (k.upper === 'END' || k.upper === 'PROCEDURE') break;
  496. if (['CONST', 'TYPE', 'VAR'].includes(k.upper)) {
  497. parseScope(p, sym.children, true);
  498. continue;
  499. }
  500. break;
  501. }
  502. const tail = p.toks[p.pos - 1];
  503. if (tail) { sym.scopeEndLine = tail.endLine; sym.scopeEndCh = tail.endCh; }
  504. continue;
  505. }
  506. // anything else at scope level: skip one item to stay in sync
  507. p.skipDeclItem();
  508. if (p.at(';')) p.next();
  509. else if (p.pos < p.toks.length) p.next();
  510. }
  511. }
  512. /** Entry: top-level symbols of an Oberon module.
  513. * When `marks` is given, export-mark `*`/`-` positions (`line:ch`)
  514. * are collected for the formatter. */
  515. export function parseOberon(text: string, marks?: Set<string>): ObSymbol[] {
  516. const toks = lex(mask(text));
  517. const p = new Parser(toks, marks);
  518. // skip an optional DEFINITION prefix and the MODULE header to ';'
  519. if (p.at('DEFINITION')) p.next();
  520. if (p.at('MODULE')) {
  521. p.next();
  522. let depth = 0;
  523. while (p.pos < p.toks.length) {
  524. const t = p.next();
  525. if (t.upper === '(') depth++;
  526. else if (t.upper === ')') depth--;
  527. else if (t.upper === ';' && depth === 0) break;
  528. }
  529. }
  530. const out: ObSymbol[] = [];
  531. parseScope(p, out, false);
  532. const first = toks.length > 0 ? toks[0] : null;
  533. if (first && first.upper === 'MODULE' && toks.length > 1 && isIdent(toks[1])) {
  534. const nm = toks[1];
  535. const last = toks[toks.length - 1];
  536. const mod: ObSymbol = { name: nm.text, kind: 'module', detail: `MODULE ${nm.text}`, line: first.line, ch: first.ch, endLine: nm.endLine, endCh: nm.endCh, children: out };
  537. if (last) { mod.scopeEndLine = last.endLine; mod.scopeEndCh = last.endCh; }
  538. return [mod];
  539. }
  540. return out;
  541. }
  542. /** True when (line, ch) lies within the symbol's scope, if known. */
  543. function inScope(s: ObSymbol, line: number, ch: number): boolean {
  544. if (s.scopeEndLine === undefined) return true;
  545. if (line < s.line) return false;
  546. if (line > s.scopeEndLine) return false;
  547. if (line === s.scopeEndLine && s.scopeEndCh !== undefined && ch > s.scopeEndCh) return false;
  548. return true;
  549. }
  550. /** Symbols visible at a position: globals plus the innermost enclosing
  551. * routine's receiver, params and locals. Forgiving (completion-only). */
  552. export function visibleAt(symbols: ObSymbol[], line: number, ch: number): ObSymbol[] {
  553. const top: ObSymbol[] = [];
  554. for (const s of symbols) {
  555. if (s.kind === 'module') top.push(...s.children);
  556. else top.push(s);
  557. }
  558. const result: ObSymbol[] = [];
  559. for (const s of top) {
  560. if (s.kind === 'constant' || s.kind === 'type' || s.kind === 'variable') result.push(s);
  561. }
  562. const routines = top.filter(s => s.kind === 'procedure' && inScope(s, line, ch));
  563. routines.sort((a, b) => (b.line - a.line) || ((b.ch ?? 0) - (a.ch ?? 0)));
  564. for (const r of routines) {
  565. result.push(...r.children);
  566. result.push(r);
  567. }
  568. for (const s of top) {
  569. if (s.kind === 'procedure' && !result.includes(s)) result.push(s);
  570. }
  571. return result;
  572. }