|
@@ -37,6 +37,8 @@ export interface M2Import {
|
|
|
names: string[];
|
|
names: string[];
|
|
|
/** True for plain `IMPORT m` (all exports visible as m.X). */
|
|
/** True for plain `IMPORT m` (all exports visible as m.X). */
|
|
|
all: boolean;
|
|
all: boolean;
|
|
|
|
|
+ /** Source range of the whole IMPORT/FROM clause. */
|
|
|
|
|
+ range: M2Range;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
export interface M2Unit {
|
|
export interface M2Unit {
|
|
@@ -153,23 +155,27 @@ class Parser {
|
|
|
private parseImportSeq(unit: M2Unit): void {
|
|
private parseImportSeq(unit: M2Unit): void {
|
|
|
for (;;) {
|
|
for (;;) {
|
|
|
if (this.atKw('FROM')) {
|
|
if (this.atKw('FROM')) {
|
|
|
- this.next();
|
|
|
|
|
|
|
+ const kw = this.next();
|
|
|
const m = this.expectIdent();
|
|
const m = this.expectIdent();
|
|
|
if (!m || !this.eatKw('IMPORT')) { this.syncDecl(); continue; }
|
|
if (!m || !this.eatKw('IMPORT')) { this.syncDecl(); continue; }
|
|
|
const names = this.parseIdentList();
|
|
const names = this.parseIdentList();
|
|
|
this.eatSym(';');
|
|
this.eatSym(';');
|
|
|
- if (m) unit.imports.push({ module: m.text, names, all: false });
|
|
|
|
|
|
|
+ if (m) unit.imports.push({ module: m.text, names, all: false, range: this.span(kw, this.prevTok()) });
|
|
|
} else if (this.atKw('IMPORT')) {
|
|
} else if (this.atKw('IMPORT')) {
|
|
|
- this.next();
|
|
|
|
|
|
|
+ const kw = this.next();
|
|
|
const names = this.parseIdentList();
|
|
const names = this.parseIdentList();
|
|
|
this.eatSym(';');
|
|
this.eatSym(';');
|
|
|
- for (const n of names) unit.imports.push({ module: n, names: [], all: true });
|
|
|
|
|
|
|
+ for (const n of names) unit.imports.push({ module: n, names: [], all: true, range: this.span(kw, this.prevTok()) });
|
|
|
} else {
|
|
} else {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private span(a: Token, b: Token): M2Range {
|
|
|
|
|
+ return { startLine: a.line, startCh: a.ch, endLine: b.endLine, endCh: b.endCh };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private parseIdentList(): string[] {
|
|
private parseIdentList(): string[] {
|
|
|
const names: string[] = [];
|
|
const names: string[] = [];
|
|
|
for (;;) {
|
|
for (;;) {
|
|
@@ -230,12 +236,21 @@ class Parser {
|
|
|
while (this.peek().kind === 'ident') {
|
|
while (this.peek().kind === 'ident') {
|
|
|
const nm = this.next();
|
|
const nm = this.next();
|
|
|
if (this.eatSym('=')) {
|
|
if (this.eatSym('=')) {
|
|
|
- const { text, fields } = this.captureType();
|
|
|
|
|
|
|
+ const { text, fields, enumLiterals } = this.captureType();
|
|
|
this.skipAlignment();
|
|
this.skipAlignment();
|
|
|
- parent.children.push({
|
|
|
|
|
|
|
+ const typeSym: M2Symbol = {
|
|
|
name: nm.text, kind: 'type', detail: `${nm.text} = ${text}`,
|
|
name: nm.text, kind: 'type', detail: `${nm.text} = ${text}`,
|
|
|
nameRange: rangeOf(nm), extent: rangeOf(nm), children: fields,
|
|
nameRange: rangeOf(nm), extent: rangeOf(nm), children: fields,
|
|
|
- });
|
|
|
|
|
|
|
+ };
|
|
|
|
|
+ // Enumeration literals live in the type's scope: expose them so
|
|
|
|
|
+ // completion/definition/references and the analyser can see them.
|
|
|
|
|
+ for (const lit of enumLiterals) {
|
|
|
|
|
+ typeSym.children.push({
|
|
|
|
|
+ name: lit.text, kind: 'constant', detail: `${nm.text}.${lit.text}`,
|
|
|
|
|
+ nameRange: rangeOf(lit), extent: rangeOf(lit), children: [],
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ parent.children.push(typeSym);
|
|
|
}
|
|
}
|
|
|
if (!this.eatSym(';')) this.syncDecl();
|
|
if (!this.eatSym(';')) this.syncDecl();
|
|
|
}
|
|
}
|
|
@@ -268,26 +283,37 @@ class Parser {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** Capture a type expression up to ';' at depth 0; collect RECORD fields. */
|
|
|
|
|
- private captureType(): { text: string; fields: M2Symbol[] } {
|
|
|
|
|
|
|
+ /** Capture a type expression up to ';' at depth 0; collect RECORD fields.
|
|
|
|
|
+ * A type consisting solely of `(A, B, ...)` additionally yields the
|
|
|
|
|
+ * enumeration literals so they can be treated as declared constants. */
|
|
|
|
|
+ private captureType(): { text: string; fields: M2Symbol[]; enumLiterals: Token[] } {
|
|
|
const startIdx = this.pos;
|
|
const startIdx = this.pos;
|
|
|
const fields: M2Symbol[] = [];
|
|
const fields: M2Symbol[] = [];
|
|
|
let depth = 0; // ( [ {
|
|
let depth = 0; // ( [ {
|
|
|
let recDepth = 0; // RECORD..END nesting
|
|
let recDepth = 0; // RECORD..END nesting
|
|
|
|
|
+ let roundDepth = 0; // ( ) only, for enumeration detection
|
|
|
let prevSignificant: Token | null = null;
|
|
let prevSignificant: Token | null = null;
|
|
|
const fieldCands: Token[] = [];
|
|
const fieldCands: Token[] = [];
|
|
|
|
|
+ const topIdents: Token[] = [];
|
|
|
|
|
+ let enumOk = true;
|
|
|
|
|
+ let firstTok: Token | null = null;
|
|
|
|
|
+ let lastTok: Token | null = null;
|
|
|
while (!this.atEof()) {
|
|
while (!this.atEof()) {
|
|
|
const t = this.peek();
|
|
const t = this.peek();
|
|
|
if (t.kind === 'symbol') {
|
|
if (t.kind === 'symbol') {
|
|
|
if (t.text === '(' || t.text === '[' || t.text === '{') depth++;
|
|
if (t.text === '(' || t.text === '[' || t.text === '{') depth++;
|
|
|
else if (t.text === ')' || t.text === ']' || t.text === '}') depth = Math.max(0, depth - 1);
|
|
else if (t.text === ')' || t.text === ']' || t.text === '}') depth = Math.max(0, depth - 1);
|
|
|
else if (t.text === ';' && depth === 0 && recDepth === 0) break;
|
|
else if (t.text === ';' && depth === 0 && recDepth === 0) break;
|
|
|
|
|
+ if (t.text === '(') roundDepth++;
|
|
|
|
|
+ else if (t.text === ')') roundDepth = Math.max(0, roundDepth - 1);
|
|
|
|
|
+ if (t.text !== '(' && t.text !== ')' && t.text !== ',') enumOk = false;
|
|
|
} else if (t.kind === 'keyword') {
|
|
} else if (t.kind === 'keyword') {
|
|
|
if (t.text === 'RECORD' && depth === 0) recDepth++;
|
|
if (t.text === 'RECORD' && depth === 0) recDepth++;
|
|
|
else if (t.text === 'END' && depth === 0 && recDepth > 0) recDepth--;
|
|
else if (t.text === 'END' && depth === 0 && recDepth > 0) recDepth--;
|
|
|
else if ((t.text === 'CONST' || t.text === 'TYPE' || t.text === 'VAR' ||
|
|
else if ((t.text === 'CONST' || t.text === 'TYPE' || t.text === 'VAR' ||
|
|
|
t.text === 'PROCEDURE' || t.text === 'MODULE' || t.text === 'BEGIN' ||
|
|
t.text === 'PROCEDURE' || t.text === 'MODULE' || t.text === 'BEGIN' ||
|
|
|
t.text === 'FINALLY') && depth === 0 && recDepth === 0) break;
|
|
t.text === 'FINALLY') && depth === 0 && recDepth === 0) break;
|
|
|
|
|
+ enumOk = false;
|
|
|
} else if (t.kind === 'ident' && recDepth > 0 && depth === 0) {
|
|
} else if (t.kind === 'ident' && recDepth > 0 && depth === 0) {
|
|
|
// Candidate record field: ident followed by ',' or ':' at field level.
|
|
// Candidate record field: ident followed by ',' or ':' at field level.
|
|
|
const nx = this.peek(1);
|
|
const nx = this.peek(1);
|
|
@@ -296,7 +322,15 @@ class Parser {
|
|
|
if (!afterCase && nx.kind === 'symbol' && (nx.text === ',' || nx.text === ':')) {
|
|
if (!afterCase && nx.kind === 'symbol' && (nx.text === ',' || nx.text === ':')) {
|
|
|
fieldCands.push(t);
|
|
fieldCands.push(t);
|
|
|
}
|
|
}
|
|
|
|
|
+ } else if (t.kind === 'ident') {
|
|
|
|
|
+ if (roundDepth === 1 && recDepth === 0 && depth <= 1) topIdents.push(t);
|
|
|
|
|
+ else enumOk = false;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Numbers, strings and anything else disqualify enumerations.
|
|
|
|
|
+ enumOk = false;
|
|
|
}
|
|
}
|
|
|
|
|
+ if (!firstTok) firstTok = t;
|
|
|
|
|
+ lastTok = t;
|
|
|
if (t.kind !== 'eof') prevSignificant = t;
|
|
if (t.kind !== 'eof') prevSignificant = t;
|
|
|
this.next();
|
|
this.next();
|
|
|
}
|
|
}
|
|
@@ -311,7 +345,8 @@ class Parser {
|
|
|
const endTok = this.peek();
|
|
const endTok = this.peek();
|
|
|
const text = this.rawRange(startIdx, this.pos).trim().replace(/\s+/g, ' ');
|
|
const text = this.rawRange(startIdx, this.pos).trim().replace(/\s+/g, ' ');
|
|
|
void endTok;
|
|
void endTok;
|
|
|
- return { text, fields };
|
|
|
|
|
|
|
+ const isEnum = enumOk && topIdents.length > 0 && firstTok?.text === '(' && lastTok?.text === ')';
|
|
|
|
|
+ return { text, fields, enumLiterals: isEnum ? topIdents : [] };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private skipAlignment(): void {
|
|
private skipAlignment(): void {
|