|
|
@@ -0,0 +1,477 @@
|
|
|
+/** Local Pascal symbol model for Outline, completion and hover.
|
|
|
+ *
|
|
|
+ * Conservative by design: only declaration shapes recognised with full
|
|
|
+ * confidence are emitted (misses are acceptable, wrong entries are not).
|
|
|
+ * Bodies are skipped with depth counting, so statement-level noise can
|
|
|
+ * never leak into the symbol table. Scopes: unit/program globals plus one
|
|
|
+ * routine level (params and locals); deeper nesting folds into its owner.
|
|
|
+ */
|
|
|
+
|
|
|
+export interface PasSymbol {
|
|
|
+ name: string;
|
|
|
+ kind: 'procedure' | 'function' | 'constant' | 'type' | 'variable' | 'parameter' | 'field' | 'module';
|
|
|
+ detail: string;
|
|
|
+ line: number; ch: number; endLine: number; endCh: number;
|
|
|
+ children: PasSymbol[];
|
|
|
+ /** Last line covered by this scope (routines/modules), if known. */
|
|
|
+ scopeEndLine?: number;
|
|
|
+ scopeEndCh?: number;
|
|
|
+}
|
|
|
+
|
|
|
+interface Tok {
|
|
|
+ text: string;
|
|
|
+ upper: string;
|
|
|
+ line: number; ch: number; endLine: number; endCh: number;
|
|
|
+}
|
|
|
+
|
|
|
+const WS = /\s/;
|
|
|
+
|
|
|
+/** Strip comments and string literals (length-preserving) so the
|
|
|
+ * declaration scan never sees keywords inside them. */
|
|
|
+function mask(text: string): string {
|
|
|
+ const out = text.split('');
|
|
|
+ const n = text.length;
|
|
|
+ let i = 0;
|
|
|
+ const blank = (a: number, b: number) => { for (let k = a; k < b; k++) if (out[k] !== '\n') out[k] = ' '; };
|
|
|
+ while (i < n) {
|
|
|
+ const c = text[i];
|
|
|
+ const nx = i + 1 < n ? text[i + 1] : '';
|
|
|
+ if (c === '(' && nx === '*') {
|
|
|
+ let depth = 1;
|
|
|
+ let j = i + 2;
|
|
|
+ while (j < n && depth > 0) {
|
|
|
+ if (text[j] === '(' && text[j + 1] === '*') { depth++; j += 2; }
|
|
|
+ else if (text[j] === '*' && text[j + 1] === ')') { depth--; j += 2; }
|
|
|
+ else j++;
|
|
|
+ }
|
|
|
+ blank(i, j);
|
|
|
+ i = j;
|
|
|
+ } else if (c === '{') {
|
|
|
+ let j = i + 1;
|
|
|
+ while (j < n && text[j] !== '}') j++;
|
|
|
+ blank(i, Math.min(j + 1, n));
|
|
|
+ i = Math.min(j + 1, n);
|
|
|
+ } else if (c === "'") {
|
|
|
+ let j = i + 1;
|
|
|
+ while (j < n) {
|
|
|
+ if (text[j] === "'") {
|
|
|
+ if (text[j + 1] === "'") j += 2;
|
|
|
+ else { j++; break; }
|
|
|
+ } else j++;
|
|
|
+ }
|
|
|
+ blank(i, j);
|
|
|
+ i = j;
|
|
|
+ } else {
|
|
|
+ i++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return out.join('');
|
|
|
+}
|
|
|
+
|
|
|
+function lex(masked: string): Tok[] {
|
|
|
+ const toks: Tok[] = [];
|
|
|
+ const re = /[A-Za-z_][A-Za-z0-9_]*|\d+(?:\.\d+)?(?:[Ee][+-]?\d+)?|:=|\.\.|<=|>=|<>|[^\sA-Za-z0-9_]/g;
|
|
|
+ let m: RegExpExecArray | null;
|
|
|
+ while ((m = re.exec(masked)) !== null) {
|
|
|
+ const before = masked.slice(0, m.index).split('\n');
|
|
|
+ const line = before.length - 1;
|
|
|
+ const ch = before[before.length - 1].length;
|
|
|
+ const mid = m[0].split('\n');
|
|
|
+ toks.push({
|
|
|
+ text: m[0], upper: m[0].toUpperCase(),
|
|
|
+ line, ch,
|
|
|
+ endLine: line + mid.length - 1,
|
|
|
+ endCh: mid.length > 1 ? mid[mid.length - 1].length : ch + m[0].length
|
|
|
+ });
|
|
|
+ }
|
|
|
+ return toks;
|
|
|
+}
|
|
|
+
|
|
|
+const isIdent = (t: Tok) => /^[A-Za-z_]/.test(t.text);
|
|
|
+
|
|
|
+class Parser {
|
|
|
+ pos = 0;
|
|
|
+ constructor(readonly toks: Tok[]) {}
|
|
|
+
|
|
|
+ peek(off = 0): Tok | null {
|
|
|
+ return this.pos + off < this.toks.length ? this.toks[this.pos + off] : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ at(word: string): boolean {
|
|
|
+ const t = this.peek();
|
|
|
+ return !!t && t.upper === word;
|
|
|
+ }
|
|
|
+
|
|
|
+ next(): Tok {
|
|
|
+ return this.toks[this.pos++];
|
|
|
+ }
|
|
|
+
|
|
|
+ expect(word: string): boolean {
|
|
|
+ if (this.at(word)) { this.next(); return true; }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** Skip a parenthesis/bracket group, returns true when balanced. */
|
|
|
+ skipGroup(open: string, close: string): boolean {
|
|
|
+ if (!this.at(open)) return false;
|
|
|
+ let depth = 0;
|
|
|
+ while (this.pos < this.toks.length) {
|
|
|
+ const t = this.next();
|
|
|
+ if (t.upper === open) depth++;
|
|
|
+ else if (t.upper === close) {
|
|
|
+ depth--;
|
|
|
+ if (depth === 0) return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** Skip a routine body: declarations then an optional compound. */
|
|
|
+ skipBody(name: string): void {
|
|
|
+ for (;;) {
|
|
|
+ const t = this.peek();
|
|
|
+ if (!t) return;
|
|
|
+ if (t.upper === 'BEGIN') {
|
|
|
+ this.skipCompound();
|
|
|
+ // trailing name after END (Oberon families)
|
|
|
+ if (this.at('END')) { this.next(); if (this.peek()?.upper !== '.' && /^[A-Za-z_]/.test(this.peek()?.text ?? '')) this.next(); }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (['CONST', 'TYPE', 'VAR', 'LABEL', 'PROCEDURE', 'FUNCTION'].includes(t.upper)) {
|
|
|
+ this.skipSection();
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (t.upper === 'END') return;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ skipCompound(): void {
|
|
|
+ if (!this.at('BEGIN')) return;
|
|
|
+ this.next();
|
|
|
+ let depth = 1;
|
|
|
+ while (this.pos < this.toks.length && depth > 0) {
|
|
|
+ const t = this.next();
|
|
|
+ if (t.upper === 'BEGIN' || t.upper === 'CASE' || t.upper === 'RECORD') depth++;
|
|
|
+ else if (t.upper === 'END') depth--;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** Skip one declaration section (const/type/var/label). */
|
|
|
+ skipSection(): void {
|
|
|
+ const kw = this.peek()?.upper;
|
|
|
+ if (kw !== 'CONST' && kw !== 'TYPE' && kw !== 'VAR' && kw !== 'LABEL') return;
|
|
|
+ this.next();
|
|
|
+ // items separated by ';' until a section starter, routine, BEGIN or END
|
|
|
+ for (;;) {
|
|
|
+ const t = this.peek();
|
|
|
+ if (!t) return;
|
|
|
+ if (['CONST', 'TYPE', 'VAR', 'LABEL', 'PROCEDURE', 'FUNCTION', 'BEGIN', 'END', 'IMPLEMENTATION', 'INTERFACE', 'FINALIZATION', 'INITIALIZATION'].includes(t.upper)) return;
|
|
|
+ this.skipDeclItem();
|
|
|
+ if (this.at(';')) this.next();
|
|
|
+ else return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** Skip one `name [, ...] ...` declaration item up to `;` at depth 0. */
|
|
|
+ skipDeclItem(): void {
|
|
|
+ let depth = 0;
|
|
|
+ while (this.pos < this.toks.length) {
|
|
|
+ const t = this.peek()!;
|
|
|
+ if (t.upper === '(' || t.upper === '[') depth++;
|
|
|
+ else if (t.upper === ')' || t.upper === ']') depth--;
|
|
|
+ else if (t.upper === 'RECORD') depth++;
|
|
|
+ else if (t.upper === 'END' && depth > 0) depth--;
|
|
|
+ else if (depth === 0 && (t.upper === ';' || ['CONST', 'TYPE', 'VAR', 'LABEL', 'PROCEDURE', 'FUNCTION', 'BEGIN', 'END'].includes(t.upper))) return;
|
|
|
+ this.next();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** Parse an identifier list, returning the name tokens. */
|
|
|
+ nameList(): Tok[] {
|
|
|
+ const out: Tok[] = [];
|
|
|
+ for (;;) {
|
|
|
+ const t = this.peek();
|
|
|
+ if (!t || !isIdent(t)) return out;
|
|
|
+ out.push(this.next());
|
|
|
+ if (this.at(',')) { this.next(); continue; }
|
|
|
+ return out;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+/** Parse the declarations of one scope; stops before BEGIN/END/EOF. */
|
|
|
+function parseScope(p: Parser, out: PasSymbol[], stopAtBegin: boolean): void {
|
|
|
+ for (;;) {
|
|
|
+ const t = p.peek();
|
|
|
+ if (!t) return;
|
|
|
+ const u = t.upper;
|
|
|
+ if (u === 'BEGIN') {
|
|
|
+ if (stopAtBegin) return;
|
|
|
+ p.skipBody('');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (u === 'END' || u === 'IMPLEMENTATION' || u === 'INTERFACE' || u === 'FINALIZATION' || u === 'INITIALIZATION') return;
|
|
|
+ if (u === 'LABEL') { p.skipSection(); continue; }
|
|
|
+ if (u === 'CONST') {
|
|
|
+ p.next();
|
|
|
+ for (;;) {
|
|
|
+ const n = p.peek();
|
|
|
+ if (!n || !isIdent(n)) break;
|
|
|
+ if (['TYPE', 'VAR', 'CONST', 'LABEL', 'PROCEDURE', 'FUNCTION', 'BEGIN', 'END'].includes(n.upper)) break;
|
|
|
+ const nm = p.next();
|
|
|
+ if (!p.at('=')) { p.skipDeclItem(); if (p.at(';')) p.next(); continue; }
|
|
|
+ p.next();
|
|
|
+ // value: skip to ';' at depth 0
|
|
|
+ p.skipDeclItem();
|
|
|
+ out.push({ name: nm.text, kind: 'constant', detail: `CONST ${nm.text}`, line: nm.line, ch: nm.ch, endLine: nm.endLine, endCh: nm.endCh, children: [] });
|
|
|
+ if (p.at(';')) p.next();
|
|
|
+ else break;
|
|
|
+ }
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (u === 'TYPE') {
|
|
|
+ p.next();
|
|
|
+ for (;;) {
|
|
|
+ const n = p.peek();
|
|
|
+ if (!n || !isIdent(n)) break;
|
|
|
+ if (['TYPE', 'VAR', 'CONST', 'LABEL', 'PROCEDURE', 'FUNCTION', 'BEGIN', 'END'].includes(n.upper)) break;
|
|
|
+ const nm = p.next();
|
|
|
+ if (!p.at('=')) { p.skipDeclItem(); if (p.at(';')) p.next(); continue; }
|
|
|
+ p.next();
|
|
|
+ p.skipDeclItem();
|
|
|
+ out.push({ name: nm.text, kind: 'type', detail: `TYPE ${nm.text}`, line: nm.line, ch: nm.ch, endLine: nm.endLine, endCh: nm.endCh, children: [] });
|
|
|
+ if (p.at(';')) p.next();
|
|
|
+ else break;
|
|
|
+ }
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (u === 'VAR' || u === 'THREADVAR') {
|
|
|
+ p.next();
|
|
|
+ for (;;) {
|
|
|
+ const n = p.peek();
|
|
|
+ if (!n || !isIdent(n)) break;
|
|
|
+ if (['TYPE', 'VAR', 'CONST', 'LABEL', 'PROCEDURE', 'FUNCTION', 'BEGIN', 'END'].includes(n.upper)) break;
|
|
|
+ const names = p.nameList();
|
|
|
+ if (!p.at(':')) { p.skipDeclItem(); if (p.at(';')) p.next(); continue; }
|
|
|
+ p.next();
|
|
|
+ // capture type text up to ';' or '=' (initializer) at depth 0
|
|
|
+ const typeToks: string[] = [];
|
|
|
+ let depth = 0;
|
|
|
+ while (p.pos < p.toks.length) {
|
|
|
+ const k = p.peek()!;
|
|
|
+ if (k.upper === '(' || k.upper === '[') depth++;
|
|
|
+ else if (k.upper === ')' || k.upper === ']') depth--;
|
|
|
+ else if (k.upper === 'RECORD') depth++;
|
|
|
+ else if (k.upper === 'END' && depth > 0) depth--;
|
|
|
+ else if (depth === 0 && (k.upper === ';' || k.upper === '=')) break;
|
|
|
+ else if (depth === 0 && ['CONST', 'TYPE', 'VAR', 'LABEL', 'PROCEDURE', 'FUNCTION', 'BEGIN', 'END'].includes(k.upper)) break;
|
|
|
+ typeToks.push(k.text);
|
|
|
+ p.next();
|
|
|
+ }
|
|
|
+ const typeText = typeToks.join(' ').replace(/\s+([,;:()[\]])/g, '$1').replace(/([(\[])\s+/g, '$1');
|
|
|
+ if (p.at('=')) { p.next(); p.skipDeclItem(); }
|
|
|
+ for (const nm of names) {
|
|
|
+ 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: [] });
|
|
|
+ }
|
|
|
+ if (p.at(';')) p.next();
|
|
|
+ else break;
|
|
|
+ }
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (u === 'PROCEDURE' || u === 'FUNCTION') {
|
|
|
+ const isFunc = u === 'FUNCTION';
|
|
|
+ const kw = p.next();
|
|
|
+ // optional generic prefix / class / receiver: find the routine name.
|
|
|
+ // Name = last ident before the formal list, result type or ';'.
|
|
|
+ // Stops at '(' (formals parsed separately below); '<...>' type
|
|
|
+ // parameter lists are skipped via angle depth.
|
|
|
+ let depth = 0;
|
|
|
+ let adepth = 0;
|
|
|
+ let nameTok: Tok | null = null;
|
|
|
+ let lastIdent: Tok | null = null;
|
|
|
+ while (p.pos < p.toks.length) {
|
|
|
+ const k = p.peek()!;
|
|
|
+ if (k.upper === '<') adepth++;
|
|
|
+ else if (k.upper === '>') adepth--;
|
|
|
+ else if (k.upper === '(' && adepth === 0) break;
|
|
|
+ else if (k.upper === '(') depth++;
|
|
|
+ else if (k.upper === ')') depth--;
|
|
|
+ else if (depth === 0 && adepth === 0 && (k.upper === ';' || k.upper === ':')) break;
|
|
|
+ else if (depth === 0 && adepth === 0 && k.upper === '.') { lastIdent = null; }
|
|
|
+ else if (depth === 0 && adepth === 0 && isIdent(k)) lastIdent = k;
|
|
|
+ if (depth === 0 && adepth === 0 && ['BEGIN', 'END', 'CONST', 'TYPE', 'VAR', 'PROCEDURE', 'FUNCTION'].includes(k.upper)) break;
|
|
|
+ p.next();
|
|
|
+ }
|
|
|
+ nameTok = lastIdent;
|
|
|
+ // formal params
|
|
|
+ const params: PasSymbol[] = [];
|
|
|
+ if (p.at('(')) {
|
|
|
+ p.next();
|
|
|
+ let pd = 1;
|
|
|
+ const pnames: Tok[] = [];
|
|
|
+ let ptype: string[] = [];
|
|
|
+ let inType = false;
|
|
|
+ const flush = () => {
|
|
|
+ const tt = ptype.join(' ').replace(/\s+([,;:()[\]])/g, '$1').replace(/([(\[])\s+/g, '$1');
|
|
|
+ for (const nm of pnames) {
|
|
|
+ params.push({ name: nm.text, kind: 'parameter', detail: `${nm.text}: ${tt}`, line: nm.line, ch: nm.ch, endLine: nm.endLine, endCh: nm.endCh, children: [] });
|
|
|
+ }
|
|
|
+ pnames.length = 0; ptype = []; inType = false;
|
|
|
+ };
|
|
|
+ while (p.pos < p.toks.length && pd > 0) {
|
|
|
+ const k = p.next();
|
|
|
+ if (k.upper === '(') { pd++; if (inType) ptype.push(k.text); continue; }
|
|
|
+ if (k.upper === ')') { pd--; if (pd === 0) { if (pnames.length || ptype.length) flush(); break; } if (inType) ptype.push(k.text); continue; }
|
|
|
+ if (pd !== 1) { if (inType) ptype.push(k.text); continue; }
|
|
|
+ if (k.upper === ';') { flush(); continue; }
|
|
|
+ if (k.upper === ':' && !inType) { inType = true; continue; }
|
|
|
+ if (k.upper === ',') continue;
|
|
|
+ if (k.upper === '=' && inType) {
|
|
|
+ // default value: skip to ';' or ')' at depth 1
|
|
|
+ let dd = 1;
|
|
|
+ while (p.pos < p.toks.length && dd > 0) {
|
|
|
+ const q = p.next();
|
|
|
+ if (q.upper === '(') dd++;
|
|
|
+ else if (q.upper === ')') { dd--; if (dd === 0) { pd--; if (pnames.length || ptype.length) flush(); break; } }
|
|
|
+ else if (q.upper === ';' && dd === 1) { flush(); break; }
|
|
|
+ }
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (['VAR', 'OUT', 'CONST'].includes(k.upper) && !inType && pnames.length === 0) continue;
|
|
|
+ if (isIdent(k)) { if (inType) ptype.push(k.text); else pnames.push(k); }
|
|
|
+ else if (inType) ptype.push(k.text);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // result type
|
|
|
+ let restype = '';
|
|
|
+ if (p.at(':')) {
|
|
|
+ p.next();
|
|
|
+ const rt: string[] = [];
|
|
|
+ while (p.pos < p.toks.length) {
|
|
|
+ const k = p.peek()!;
|
|
|
+ if (k.upper === ';') break;
|
|
|
+ rt.push(k.text);
|
|
|
+ p.next();
|
|
|
+ }
|
|
|
+ restype = rt.join(' ').replace(/\s+([,;:()[\]])/g, '$1').replace(/([(\[])\s+/g, '$1');
|
|
|
+ }
|
|
|
+ if (!nameTok) {
|
|
|
+ // unrecognised header: skip one item and continue (never emit)
|
|
|
+ p.skipDeclItem();
|
|
|
+ if (p.at(';')) p.next();
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ const sym: PasSymbol = {
|
|
|
+ name: nameTok.text,
|
|
|
+ kind: isFunc ? 'function' : 'procedure',
|
|
|
+ detail: `${isFunc ? 'FUNCTION' : 'PROCEDURE'} ${nameTok.text}${restype ? ': ' + restype : ''}`,
|
|
|
+ line: kw.line, ch: kw.ch, endLine: nameTok.endLine, endCh: nameTok.endCh,
|
|
|
+ children: params
|
|
|
+ };
|
|
|
+ out.push(sym);
|
|
|
+ if (p.at(';')) p.next();
|
|
|
+ // directives or body: skip directives (words + ';') then body
|
|
|
+ for (;;) {
|
|
|
+ const k = p.peek();
|
|
|
+ if (!k) break;
|
|
|
+ if (k.upper === 'BEGIN') { p.skipBody(nameTok.text); break; }
|
|
|
+ if (k.upper === 'FORWARD' || k.upper === 'EXTERNAL') {
|
|
|
+ // bodyless: consume through ';'
|
|
|
+ while (p.pos < p.toks.length && !p.at(';') && !['BEGIN', 'END', 'PROCEDURE', 'FUNCTION', 'CONST', 'TYPE', 'VAR'].includes(p.peek()!.upper)) p.next();
|
|
|
+ if (p.at(';')) p.next();
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ if (k.upper === 'END' || k.upper === 'PROCEDURE' || k.upper === 'FUNCTION') break;
|
|
|
+ // nested declarations belong to this routine: parse one level, then body
|
|
|
+ if (['CONST', 'TYPE', 'VAR', 'LABEL'].includes(k.upper)) {
|
|
|
+ parseScope(p, sym.children, true);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ // unknown directive word: skip to ';' (covers calling conventions)
|
|
|
+ if (isIdent(k)) {
|
|
|
+ p.next();
|
|
|
+ if (p.at(';')) { p.next(); continue; }
|
|
|
+ // not a lone directive: treat rest as body
|
|
|
+ p.skipBody(nameTok.text);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ const tail = p.toks[p.pos - 1];
|
|
|
+ if (tail) { sym.scopeEndLine = tail.endLine; sym.scopeEndCh = tail.endCh; }
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ // anything else at scope level: skip one item to stay in sync
|
|
|
+ p.skipDeclItem();
|
|
|
+ if (p.at(';')) p.next();
|
|
|
+ else if (p.pos < p.toks.length) p.next();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** Entry: top-level symbols of a Pascal unit/program. */
|
|
|
+export function parsePascal(text: string): PasSymbol[] {
|
|
|
+ const toks = lex(mask(text));
|
|
|
+ const p = new Parser(toks);
|
|
|
+ // skip program/unit/library header up to the first ';' at depth 0
|
|
|
+ const first = p.peek();
|
|
|
+ if (first && ['PROGRAM', 'UNIT', 'LIBRARY'].includes(first.upper)) {
|
|
|
+ p.next();
|
|
|
+ let depth = 0;
|
|
|
+ while (p.pos < p.toks.length) {
|
|
|
+ const t = p.next();
|
|
|
+ if (t.upper === '(') depth++;
|
|
|
+ else if (t.upper === ')') depth--;
|
|
|
+ else if (t.upper === ';' && depth === 0) break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const out: PasSymbol[] = [];
|
|
|
+ parseScope(p, out, false);
|
|
|
+ // module wrapper for outline friendliness when a header was seen
|
|
|
+ if (first && ['PROGRAM', 'UNIT', 'LIBRARY'].includes(first.upper)) {
|
|
|
+ const nm = p.toks.length > 1 ? p.toks[1] : null;
|
|
|
+ if (nm && /^[A-Za-z_]/.test(nm.text)) {
|
|
|
+ const last = p.toks[p.toks.length - 1];
|
|
|
+ const mod: PasSymbol = { name: nm.text, kind: 'module', detail: `${first.text.toUpperCase()} ${nm.text}`, line: first.line, ch: first.ch, endLine: nm.endLine, endCh: nm.endCh, children: out };
|
|
|
+ if (last) { mod.scopeEndLine = last.endLine; mod.scopeEndCh = last.endCh; }
|
|
|
+ return [mod];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return out;
|
|
|
+}
|
|
|
+
|
|
|
+/** True when (line, ch) lies within the symbol's scope, if known. */
|
|
|
+function inScope(s: PasSymbol, line: number, ch: number): boolean {
|
|
|
+ if (s.scopeEndLine === undefined) return true;
|
|
|
+ if (line < s.line) return false;
|
|
|
+ if (line > s.scopeEndLine) return false;
|
|
|
+ if (line === s.scopeEndLine && s.scopeEndCh !== undefined && ch > s.scopeEndCh) return false;
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+/** Symbols visible at a position: globals plus the innermost enclosing
|
|
|
+ * routine's params and locals. Forgiving by design (completion-only). */
|
|
|
+export function visibleAt(symbols: PasSymbol[], line: number, ch: number): PasSymbol[] {
|
|
|
+ const top: PasSymbol[] = [];
|
|
|
+ for (const s of symbols) {
|
|
|
+ if (s.kind === 'module') top.push(...s.children);
|
|
|
+ else top.push(s);
|
|
|
+ }
|
|
|
+ const result: PasSymbol[] = [];
|
|
|
+ for (const s of top) {
|
|
|
+ if (s.kind === 'constant' || s.kind === 'type' || s.kind === 'variable') result.push(s);
|
|
|
+ }
|
|
|
+ // innermost enclosing routine first
|
|
|
+ const routines = top.filter(s => (s.kind === 'procedure' || s.kind === 'function') && inScope(s, line, ch));
|
|
|
+ routines.sort((a, b) => (b.line - a.line) || ((b.ch ?? 0) - (a.ch ?? 0)));
|
|
|
+ for (const r of routines) {
|
|
|
+ result.push(...r.children);
|
|
|
+ result.push(r);
|
|
|
+ }
|
|
|
+ // top-level routines are callable throughout the unit
|
|
|
+ for (const s of top) {
|
|
|
+ if ((s.kind === 'procedure' || s.kind === 'function') && !result.includes(s)) result.push(s);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+}
|