import type { LangiumDocument, LangiumSharedCoreServices, URI } from 'langium'; import { DefaultIndexManager } from 'langium'; import { CancellationToken } from 'vscode-jsonrpc'; import type { DomainLangServices } from '../domain-lang-module.js'; import type { ImportInfo, ImportCycleDetector } from '../services/types.js'; /** * Custom IndexManager that extends Langium's default to: * 1. Automatically load imported documents during indexing * 2. Track import dependencies for cross-file revalidation * 3. Export-signature diffing to prevent unnecessary cascading (PRS-017 R2) * 4. Import cycle detection with diagnostics (PRS-017 R3) * 5. Targeted ImportResolver cache invalidation (PRS-017 R1) * * **Why this exists:** * Langium's `DefaultIndexManager.isAffected()` only checks cross-references * (elements declared with `[Type]` grammar syntax). DomainLang's imports use * string literals (`import "path"`), which are not cross-references. * * **How it works:** * - When a document is indexed, we ensure all its imports are also loaded * - Maintains a reverse dependency graph: importedUri → Set * - Also tracks import specifiers to detect when file moves affect resolution * - Overrides `isAffected()` to also check this graph * - This integrates with Langium's native `DocumentBuilder.update()` flow * * **Integration with Langium:** * This approach is idiomatic because: * 1. `updateContent()` is called for EVERY document during build * 2. We load imports during indexing, BEFORE linking/validation * 3. `DocumentBuilder.shouldRelink()` calls `IndexManager.isAffected()` * 4. No need for separate lifecycle service - this IS the central place */ export declare class DomainLangIndexManager extends DefaultIndexManager implements ImportCycleDetector { /** * Reverse dependency graph: maps a document URI to all documents that import it. * Key: imported document URI (string) * Value: Set of URIs of documents that import the key document */ private readonly importDependencies; /** * Maps document URI to its import information (specifier, alias, resolved URI). * Used for scope resolution with aliases and detecting when file moves affect imports. * Key: importing document URI * Value: Array of ImportInfo objects */ private readonly documentImportInfo; /** * Tracks documents that have had their imports loaded to avoid redundant work. * Cleared on workspace config changes. */ private readonly importsLoaded; /** * Per-cycle cache for the transitive affected set computation. * Uses `changedUris` Set identity as cache key — Langium creates a fresh Set * for each `DocumentBuilder.update()` cycle, so reference equality naturally * invalidates the cache between cycles. */ private transitiveAffectedCache; /** * Export snapshot cache (PRS-017 R2): maps document URI to its exported symbol * signatures. Used to detect whether a document's public interface actually * changed, preventing cascading revalidation for implementation-only changes. * Signature = "nodeType:qualifiedName" for each exported symbol. */ private readonly exportSnapshots; /** * Tracks which URIs had their exports actually change during the current * update cycle. Reset before each updateContent() call. Used by isAffected() * to skip transitive invalidation when exports are unchanged. */ private readonly changedExports; /** * Detected import cycles (PRS-017 R3): maps document URI to the cycle path. * Populated during trackImportDependencies(). Consumed by ImportValidator. */ private readonly detectedCycles; /** * Reference to shared services for accessing LangiumDocuments. */ private readonly sharedServices; /** * DI-injected import resolver. Set via late-binding because * IndexManager (shared module) is created before ImportResolver (language module). * Always set before any document indexing begins via `setLanguageServices()`. */ private importResolver; constructor(services: LangiumSharedCoreServices); /** * Late-binds the language-specific services after DI initialization. * Called from `createDomainLangServices()` after the language module is created. * * This is necessary because the IndexManager lives in the shared module, * which is created before the language module that provides ImportResolver. */ setLanguageServices(services: DomainLangServices): void; /** * Resolves an import path using the DI-injected ImportResolver. */ private resolveImport; /** * Extends the default content update to: * 1. Capture export snapshot before update (PRS-017 R2) * 2. Ensure all imported documents are loaded * 3. Track import dependencies for change propagation * 4. Compare export snapshot to detect interface changes (PRS-017 R2) * 5. Detect import cycles (PRS-017 R3) * 6. Trigger targeted ImportResolver cache invalidation (PRS-017 R1) * * Called by Langium during the IndexedContent build phase. * This is BEFORE linking/validation, so imports are available for resolution. */ updateContent(document: LangiumDocument, cancelToken?: CancellationToken): Promise; /** * Extends the default remove to also clean up import dependencies. */ remove(uri: URI): void; /** * Extends the default content removal to also clean up import dependencies. */ removeContent(uri: URI): void; /** * Extends `isAffected` to check import dependencies — direct, transitive, * and specifier-sensitive. * * A document is affected if: * 1. It has cross-references to any changed document (default Langium behavior) * 2. It directly or transitively imports any changed document whose exports * actually changed (PRS-017 R2 — export-signature diffing) * 3. Its import specifiers match changed file paths (handles renames/moves) * * The transitive affected set is computed once per `update()` cycle and cached * using `changedUris` Set identity (Langium creates a fresh Set per cycle). * This avoids redundant BFS walks when `isAffected()` is called for every * loaded document in the workspace. */ isAffected(document: LangiumDocument, changedUris: Set): boolean; /** * Computes the full set of document URIs affected by changes. * Cached per `changedUris` identity to avoid recomputation across multiple * `isAffected()` calls within the same `DocumentBuilder.update()` cycle. * * Combines two dependency strategies: * 1. **Reverse graph walk** — direct and transitive importers via `importDependencies` * 2. **Specifier matching** — documents whose import specifiers match changed file * paths (handles file renames/moves that change how imports resolve) */ private computeAffectedSet; /** * BFS through the reverse dependency graph to find all transitive importers. * If C changes and B imports C and A imports B, both A and B are added. */ private addTransitiveDependents; /** * Finds documents whose import specifiers fuzzy-match changed file paths. * Handles file renames/moves where the resolved URI hasn't been updated yet. */ private addSpecifierMatches; /** * Tracks import dependencies for a document. * For each import in the document, records: * 1. That the imported URI is depended upon (for direct change detection) * 2. The import specifier and alias (for scope resolution) */ private trackImportDependencies; /** * Resolves a single import and registers it in the reverse dependency graph. * Falls back to searching loaded documents when the filesystem resolver fails. */ private resolveAndTrackImport; /** * Adds an edge to the reverse dependency graph: importedUri → importingUri. */ private addToDependencyGraph; /** * Ensures all imported documents are loaded and available. * This is called during indexing, BEFORE linking/validation, * so that cross-file references can be resolved. * * Works for both workspace files and standalone files. */ private ensureImportsLoaded; /** * Removes a document from the import dependencies graph entirely. * Called when a document is deleted. */ private removeImportDependencies; /** * Removes a document from all dependency sets. * Called when a document's imports change or it's deleted. * * Uses the forward graph (documentImportInfo) to find only the reverse-graph sets * that contain this document, avoiding an O(N) scan over all imported documents. */ private removeDocumentFromDependencies; /** * Clears all import-related caches. * Call this when workspace configuration changes. */ clearImportDependencies(): void; /** * Fallback for import resolution: searches loaded documents for one whose * URI path matches the import specifier. Used when the filesystem-based * resolver fails (e.g., unsaved files, EmptyFileSystem in tests). */ private findLoadedDocumentByPath; /** * Marks a document as needing import re-loading. * Called when a document's content changes. */ markForReprocessing(uri: string): void; /** * Gets all documents that import the given URI. * Used to find documents that need rebuilding when a file changes. * * @param uri - The URI of the changed/deleted file * @returns Set of URIs (as strings) of documents that import this file */ getDependentDocuments(uri: string): Set; /** * Gets the resolved import URIs for a document. * Returns only URIs where import resolution succeeded (non-empty resolved URI). * * @param documentUri - The URI of the document * @returns Set of resolved import URIs, or empty set if none */ getResolvedImports(documentUri: string): Set; /** * Gets the full import information (including aliases) for a document. * Used by the scope provider to implement alias-prefixed name resolution. * * @param documentUri - The URI of the document * @returns Array of ImportInfo objects, or empty array if none */ getImportInfo(documentUri: string): ImportInfo[]; /** * Gets all documents that would be affected by changes to the given URIs. * This includes direct dependents and transitive dependents. * * @param changedUris - URIs of changed/deleted files * @returns Set of all affected document URIs */ getAllAffectedDocuments(changedUris: Iterable): Set; /** * Gets documents that have import specifiers which might be affected by file moves. * * When a file is moved/renamed, import specifiers that previously resolved to it * (or could now resolve to it) need to be re-evaluated. This method finds documents * whose imports might resolve differently after the file system change. * * @param changedUris - URIs of changed/deleted/created files * @returns Set of document URIs that should be rebuilt */ getDocumentsWithPotentiallyAffectedImports(changedUris: Iterable): Set; /** * Extracts path segments from URIs for fuzzy matching. */ private extractPathSegments; /** * Adds path segments from a single URI to the set. */ private addPathSegmentsFromUri; /** * Finds documents with import specifiers matching any of the given paths. */ private findDocumentsMatchingPaths; /** * Checks if any specifier OR its resolved URI matches the changed paths (PRS-017 R4). * * Uses exact filename matching instead of substring matching to prevent * false positives (e.g., changing `sales.dlang` should NOT trigger * revalidation of a file importing `pre-sales.dlang`). * * This handles both regular imports and path aliases: * - Regular: `./domains/sales.dlang` matches path `sales.dlang` * - Aliased: `@domains/sales.dlang` resolves to `/full/path/domains/sales.dlang` * When the file moves, the resolved URI matches but the specifier doesn't * * We check both to ensure moves of aliased imports trigger revalidation. */ private hasMatchingSpecifierOrResolvedUri; /** * Checks if a single import info matches any of the changed paths. * Extracted to reduce cognitive complexity of hasMatchingSpecifierOrResolvedUri. */ private matchesAnyChangedPath; /** * Checks if a single import info matches a single changed path. */ private matchesChangedPath; /** * Checks if a resolved URI matches a changed path by exact filename comparison. */ private matchesResolvedUri; /** * Checks if an import specifier matches a changed path by exact filename comparison. */ private matchesSpecifier; /** * Extracts the filename (without extension) from a path or URI string. */ private extractFileName; /** * Checks if longPath ends with shortPath, comparing path segments. * Prevents substring false positives (e.g., "pre-sales" matching "sales"). */ private pathEndsWith; /** * Captures a snapshot of exported symbol signatures for a document. * Signature = "nodeType:qualifiedName" for each exported symbol. * Used to detect whether a document's public interface actually changed. */ private captureExportSnapshot; /** * Checks if two sets of strings are equal (same size and same elements). */ private setsEqual; /** * Returns true if any of the changed URIs had their exports actually change. * Used by isAffected() to skip transitive invalidation when only * implementation details changed (e.g., editing a vision string). */ private anyExportsChanged; /** * Detects import cycles starting from a given document URI. * Uses DFS with a recursion stack to find back-edges in the import graph. * Stores detected cycles for reporting by ImportValidator. */ private detectAndStoreCycles; /** * DFS to find a cycle in the forward import graph starting from startUri. * Returns the cycle path (e.g., [A, B, C, A]) if found, undefined otherwise. */ private findCycle; /** * Gets the detected import cycle for a document, if any. * Returns the cycle path as an array of URIs, or undefined if no cycle. * Used by ImportValidator to report cycle diagnostics (PRS-017 R3). */ getCycleForDocument(uri: string): string[] | undefined; /** * Invalidates the ImportResolver cache for the changed document and its dependents. * This provides surgical cache invalidation instead of clearing the entire cache. */ private invalidateImportResolverCache; }