|
@@ -0,0 +1,127 @@
|
|
|
|
|
+/** Semantic tokens for Modula-2 editors (LSP `textDocument/semanticTokens`).
|
|
|
|
|
+ *
|
|
|
|
|
+ * Declarations come from the symbol tree; identifier *uses* are resolved
|
|
|
|
|
+ * through the same machinery as hover/definition, so variables, parameters,
|
|
|
|
|
+ * fields, enum members and cross-file imports all highlight by meaning,
|
|
|
|
|
+ * not just by spelling. Anything unresolvable is skipped, so broken code
|
|
|
|
|
+ * under editing degrades to TextMate highlighting instead of errors.
|
|
|
|
|
+ */
|
|
|
|
|
+
|
|
|
|
|
+import { M2Symbol, M2Unit, parseUnitText } from './parser';
|
|
|
|
|
+import { flattenUnit, identifierOccurrences, resolveName } from './resolve';
|
|
|
|
|
+
|
|
|
|
|
+export const SEMANTIC_TOKEN_TYPES = [
|
|
|
|
|
+ 'namespace', // modules
|
|
|
|
|
+ 'type', // declared types
|
|
|
|
|
+ 'function', // procedures
|
|
|
|
|
+ 'variable', // variables and constants
|
|
|
|
|
+ 'parameter', // parameters
|
|
|
|
|
+ 'property', // record fields
|
|
|
|
|
+ 'enumMember', // enumeration literals
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+export const SEMANTIC_TOKEN_MODIFIERS = ['readonly'];
|
|
|
|
|
+
|
|
|
|
|
+export function semanticTokensLegend(): { tokenTypes: string[]; tokenModifiers: string[] } {
|
|
|
|
|
+ return { tokenTypes: [...SEMANTIC_TOKEN_TYPES], tokenModifiers: [...SEMANTIC_TOKEN_MODIFIERS] };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface RawToken {
|
|
|
|
|
+ line: number;
|
|
|
|
|
+ ch: number;
|
|
|
|
|
+ length: number;
|
|
|
|
|
+ type: number;
|
|
|
|
|
+ modifiers: number;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** LSP delta-encoded token data for a document. Never throws on broken input. */
|
|
|
|
|
+export function computeSemanticTokens(text: string, filePath: string, docDir: string): number[] {
|
|
|
|
|
+ const unit = parseUnitText(text, filePath);
|
|
|
|
|
+ const out: RawToken[] = [];
|
|
|
|
|
+ const declared = new Set<string>();
|
|
|
|
|
+ for (const { sym, chain } of flattenUnit(unit)) {
|
|
|
|
|
+ if (!sym.name) continue;
|
|
|
|
|
+ // The file's own module symbol is the root scope, not a declaration.
|
|
|
|
|
+ if (sym.kind === 'module' && chain.length === 0) continue;
|
|
|
|
|
+ declared.add(`${sym.nameRange.startLine}:${sym.nameRange.startCh}`);
|
|
|
|
|
+ const mapped = symbolToken(sym, chain);
|
|
|
|
|
+ if (!mapped) continue;
|
|
|
|
|
+ out.push({
|
|
|
|
|
+ line: sym.nameRange.startLine,
|
|
|
|
|
+ ch: sym.nameRange.startCh,
|
|
|
|
|
+ length: Math.max(1, sym.nameRange.endCh - sym.nameRange.startCh),
|
|
|
|
|
+ type: mapped.type,
|
|
|
|
|
+ modifiers: mapped.modifiers,
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ for (const occ of identifierOccurrences(text)) {
|
|
|
|
|
+ if (declared.has(`${occ.line}:${occ.ch}`)) continue;
|
|
|
|
|
+ const resolved = resolveName(unit, docDir, occ.line, occ.ch, occ.qualifier, occ.name);
|
|
|
|
|
+ if (!resolved) continue;
|
|
|
|
|
+ const kind = useTokenKind(resolved.sym);
|
|
|
|
|
+ if (kind === null) continue;
|
|
|
|
|
+ out.push({
|
|
|
|
|
+ line: occ.line,
|
|
|
|
|
+ ch: occ.ch,
|
|
|
|
|
+ length: Math.max(1, occ.endCh - occ.ch),
|
|
|
|
|
+ type: kind,
|
|
|
|
|
+ modifiers: 0,
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ out.sort((a, b) => a.line - b.line || a.ch - b.ch);
|
|
|
|
|
+ const data: number[] = [];
|
|
|
|
|
+ let prevLine = 0;
|
|
|
|
|
+ let prevCh = 0;
|
|
|
|
|
+ for (const t of out) {
|
|
|
|
|
+ data.push(
|
|
|
|
|
+ t.line - prevLine,
|
|
|
|
|
+ t.line === prevLine ? t.ch - prevCh : t.ch,
|
|
|
|
|
+ t.length, t.type, t.modifiers,
|
|
|
|
|
+ );
|
|
|
|
|
+ prevLine = t.line;
|
|
|
|
|
+ prevCh = t.ch;
|
|
|
|
|
+ }
|
|
|
|
|
+ return data;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function typeIndex(name: string): number {
|
|
|
|
|
+ const i = SEMANTIC_TOKEN_TYPES.indexOf(name);
|
|
|
|
|
+ return i >= 0 ? i : 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function symbolToken(
|
|
|
|
|
+ sym: M2Symbol, chain: M2Symbol[],
|
|
|
|
|
+): { type: number; modifiers: number } | null {
|
|
|
|
|
+ switch (sym.kind) {
|
|
|
|
|
+ case 'module': return { type: typeIndex('namespace'), modifiers: 0 };
|
|
|
|
|
+ case 'procedure': return { type: typeIndex('function'), modifiers: 0 };
|
|
|
|
|
+ case 'variable': return { type: typeIndex('variable'), modifiers: 0 };
|
|
|
|
|
+ case 'parameter': return { type: typeIndex('parameter'), modifiers: 0 };
|
|
|
|
|
+ case 'field': return { type: typeIndex('property'), modifiers: 0 };
|
|
|
|
|
+ case 'constant': {
|
|
|
|
|
+ // Enumeration literals live under their type; other constants read-only.
|
|
|
|
|
+ const underType = chain.length > 0 && chain[chain.length - 1].kind === 'type';
|
|
|
|
|
+ return underType
|
|
|
|
|
+ ? { type: typeIndex('enumMember'), modifiers: 0 }
|
|
|
|
|
+ : { type: typeIndex('variable'), modifiers: 1 };
|
|
|
|
|
+ }
|
|
|
|
|
+ case 'type': return { type: typeIndex('type'), modifiers: 0 };
|
|
|
|
|
+ default: return null;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** Token type for a resolved identifier use (null = no token). */
|
|
|
|
|
+function useTokenKind(sym: M2Symbol): number | null {
|
|
|
|
|
+ switch (sym.kind) {
|
|
|
|
|
+ case 'module': return typeIndex('namespace');
|
|
|
|
|
+ case 'procedure': return typeIndex('function');
|
|
|
|
|
+ case 'variable': return typeIndex('variable');
|
|
|
|
|
+ case 'parameter': return typeIndex('parameter');
|
|
|
|
|
+ case 'field': return typeIndex('property');
|
|
|
|
|
+ case 'constant':
|
|
|
|
|
+ // Enumeration literals are recorded as `Type.Literal`.
|
|
|
|
|
+ return /^\w+\.\w+$/.test(sym.detail) ? typeIndex('enumMember') : typeIndex('variable');
|
|
|
|
|
+ case 'type': return typeIndex('type');
|
|
|
|
|
+ default: return null;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|