/** * Shared semantic reference logic used by Find All References, the CodeLens * "N references" command and (indirectly) the unreferenced-assets report. * * Unlike the old text-search implementation, every result here comes from * the reverse reference index built during indexing, so the count shown on a * top-level asset always matches the references peek opened by clicking it. */ import * as vscode from "vscode"; import { findElementAt, parseXml, textContentTokenAt, } from "../language/xmlParser"; import { resolveElementType } from "../language/typeContext"; import { attributesOfType, typeInfo } from "../model/schemaModel"; import { filterAndScoreDefs, isReferenceAttributeOfType, isReferenceContentType, mergeLocalAndGlobalDefs, } from "../indexer/refs"; import { referenceSitesForDef, scheduleRebuildIfRecordsDesync, } from "../indexer/referenceIndex"; import type { AssetDef, ModIndex, ReferenceSite } from "../indexer/types"; import type { ModWorkspace } from "../workspace"; export interface ReferenceContext { id: string; /** XSD refType when the cursor is on a typed reference; null otherwise. */ refType: string | null; /** Element type for inheritFrom filtering; null otherwise. */ selfType: string | null; } /** * Extracts the referenced id and its XSD context from the cursor position. * Works on reference attribute values/names, simple-content text and on an * asset's own `id` definition (where every same-id definition is a target). */ export function referenceContextAt( document: vscode.TextDocument, offset: number, ): ReferenceContext | null { const text = document.getText(); const doc = parseXml(text); const el = findElementAt(doc, offset); if (!el) return null; const elType = resolveElementType(el); const attr = el.attrs.find( (a) => (a.hasValue && offset >= a.valueStart && offset <= a.valueEnd) || (offset >= a.nameStart && offset <= a.nameEnd), ); if (attr?.hasValue) { const value = attr.value; if (!value || value.startsWith("$") || value.startsWith("=")) return null; const nameLower = attr.name.toLowerCase(); if (nameLower === "id") { return { id: value, refType: null, selfType: null }; } if (!isReferenceAttributeOfType(elType, attr.name)) return null; if (nameLower === "inheritfrom") { return { id: value, refType: null, selfType: elType }; } const attrInfo = elType ? attributesOfType(elType).find((a) => a.name === attr.name) : undefined; return { id: value, refType: attrInfo?.refType ?? null, selfType: null }; } if (elType && isReferenceContentType(elType)) { const token = textContentTokenAt(text, el, offset); if (token && !token.value.startsWith("$") && !token.value.startsWith("=")) { const info = typeInfo(elType); return { id: token.value, refType: info?.kind === "simple" ? info.refType : null, selfType: null, }; } } return null; } /** Definitions matching a reference context (strict type filtering). */ export function definitionsForReference( idx: ModIndex, ctx: ReferenceContext, ): AssetDef[] { const defs = mergeLocalAndGlobalDefs( idx.local?.assetsById.get(ctx.id.toLowerCase()), idx.assetsById.get(ctx.id.toLowerCase()), ); return filterAndScoreDefs(defs, ctx.refType, ctx.selfType).map((t) => t.def); } /** Union of reference sites for a set of definitions, de-duplicated. */ export function collectReferenceSites( idx: ModIndex, defs: readonly AssetDef[], ): ReferenceSite[] { const sites: ReferenceSite[] = []; const seen = new Set(); for (const def of defs) { for (const site of referenceSitesForDef(idx, def)) { const key = `${site.file}\u0000${site.start}\u0000${site.end}\u0000${site.kind}`; if (seen.has(key)) continue; seen.add(key); sites.push(site); } } return sites; } /** Converts stored offsets to precise editor locations (fallback: line). */ export async function sitesToLocations( ws: ModWorkspace, sites: readonly ReferenceSite[], ): Promise { const byFile = new Map(); for (const site of sites) { let list = byFile.get(site.file); if (!list) { list = []; byFile.set(site.file, list); } list.push(site); } const locations: vscode.Location[] = []; for (const [file, fileSites] of byFile) { const parsed = await (ws.indexerForFile(file) ?? ws.activeIndexer())?.readDom( file, ); const lineMap = parsed?.lineMap ?? null; for (const site of fileSites) { if (lineMap) { const start = lineMap.positionAt(site.start); const end = lineMap.positionAt(site.end); locations.push( new vscode.Location( vscode.Uri.file(file), new vscode.Range( new vscode.Position(start.line, start.character), new vscode.Position(end.line, end.character), ), ), ); } else { const line = Math.max(0, site.line - 1); locations.push( new vscode.Location( vscode.Uri.file(file), new vscode.Range( new vscode.Position(line, 0), new vscode.Position(line, 1), ), ), ); } } } return locations; } /** * Semantic Find All References (used by the reference provider). * * Only real reference sites are returned — the asset's own `id` definition is * never included, regardless of VS Code's `includeDeclaration` flag, so the * result set matches the CodeLens reference count exactly. */ export async function findReferenceLocations( ws: ModWorkspace, document: vscode.TextDocument, position: vscode.Position, ): Promise { if (!ws.isRa3Workspace()) return null; scheduleRebuildIfRecordsDesync(ws.recordsSyncSurfaceFor(document), document); const scope = await ws.getScope(document); const idx = scope.merged; if (!idx) return null; const ctx = referenceContextAt(document, document.offsetAt(position)); if (!ctx) return null; const defs = definitionsForReference(idx, ctx); if (!defs.length) return null; const locations = await sitesToLocations(ws, collectReferenceSites(idx, defs)); return locations.length ? locations : null; } /** Arguments passed from the CodeLens to the showReferences command. */ export interface ShowReferencesArgs { uri: vscode.Uri; position: vscode.Position; id: string; type: string; file: string; line: number; } /** Opens the references peek for one specific asset definition. */ export async function showReferencesForDef( ws: ModWorkspace, args: ShowReferencesArgs, ): Promise { const doc = vscode.workspace.textDocuments.find( (d) => d.uri.toString() === args.uri.toString(), ); if (!doc) return; let idx: ModIndex | null = null; try { idx = (await ws.getCodeLensScope(doc)).merged; } catch { return; } if (!idx) return; // Same definition union as the lens count / Find All References. const defs = definitionsForReference(idx, { id: args.id, refType: null, selfType: null, }); const sites = collectReferenceSites(idx, defs); const locations = await sitesToLocations(ws, sites); await vscode.commands.executeCommand( "editor.action.showReferences", args.uri, args.position, locations, ); }