@domainlang/language 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/out/domain-lang-module.d.ts +2 -0
- package/out/domain-lang-module.js +23 -2
- package/out/domain-lang-module.js.map +1 -1
- package/out/lsp/domain-lang-completion.d.ts +142 -1
- package/out/lsp/domain-lang-completion.js +620 -22
- package/out/lsp/domain-lang-completion.js.map +1 -1
- package/out/lsp/domain-lang-document-symbol-provider.d.ts +79 -0
- package/out/lsp/domain-lang-document-symbol-provider.js +210 -0
- package/out/lsp/domain-lang-document-symbol-provider.js.map +1 -0
- package/out/lsp/domain-lang-index-manager.d.ts +98 -1
- package/out/lsp/domain-lang-index-manager.js +214 -7
- package/out/lsp/domain-lang-index-manager.js.map +1 -1
- package/out/lsp/domain-lang-node-kind-provider.d.ts +27 -0
- package/out/lsp/domain-lang-node-kind-provider.js +87 -0
- package/out/lsp/domain-lang-node-kind-provider.js.map +1 -0
- package/out/lsp/domain-lang-scope-provider.d.ts +100 -0
- package/out/lsp/domain-lang-scope-provider.js +170 -0
- package/out/lsp/domain-lang-scope-provider.js.map +1 -0
- package/out/lsp/domain-lang-workspace-manager.d.ts +46 -0
- package/out/lsp/domain-lang-workspace-manager.js +148 -4
- package/out/lsp/domain-lang-workspace-manager.js.map +1 -1
- package/out/lsp/hover/domain-lang-hover.d.ts +16 -6
- package/out/lsp/hover/domain-lang-hover.js +160 -134
- package/out/lsp/hover/domain-lang-hover.js.map +1 -1
- package/out/lsp/hover/hover-builders.d.ts +57 -0
- package/out/lsp/hover/hover-builders.js +171 -0
- package/out/lsp/hover/hover-builders.js.map +1 -0
- package/out/main.js +116 -20
- package/out/main.js.map +1 -1
- package/out/sdk/index.d.ts +2 -1
- package/out/sdk/index.js +1 -1
- package/out/sdk/index.js.map +1 -1
- package/out/sdk/loader-node.js +1 -1
- package/out/sdk/loader-node.js.map +1 -1
- package/out/sdk/loader.d.ts +55 -2
- package/out/sdk/loader.js +87 -28
- package/out/sdk/loader.js.map +1 -1
- package/out/sdk/query.js +14 -11
- package/out/sdk/query.js.map +1 -1
- package/out/services/import-resolver.d.ts +29 -6
- package/out/services/import-resolver.js +48 -9
- package/out/services/import-resolver.js.map +1 -1
- package/out/services/package-boundary-detector.d.ts +101 -0
- package/out/services/package-boundary-detector.js +211 -0
- package/out/services/package-boundary-detector.js.map +1 -0
- package/out/services/performance-optimizer.js +6 -2
- package/out/services/performance-optimizer.js.map +1 -1
- package/out/services/types.d.ts +24 -0
- package/out/services/types.js.map +1 -1
- package/out/services/workspace-manager.d.ts +73 -6
- package/out/services/workspace-manager.js +210 -57
- package/out/services/workspace-manager.js.map +1 -1
- package/out/utils/import-utils.d.ts +9 -6
- package/out/utils/import-utils.js +26 -15
- package/out/utils/import-utils.js.map +1 -1
- package/out/validation/constants.d.ts +20 -0
- package/out/validation/constants.js +39 -3
- package/out/validation/constants.js.map +1 -1
- package/out/validation/import.d.ts +22 -1
- package/out/validation/import.js +104 -16
- package/out/validation/import.js.map +1 -1
- package/out/validation/maps.js +101 -3
- package/out/validation/maps.js.map +1 -1
- package/package.json +5 -5
- package/src/domain-lang-module.ts +26 -3
- package/src/lsp/domain-lang-completion.ts +736 -27
- package/src/lsp/domain-lang-document-symbol-provider.ts +254 -0
- package/src/lsp/domain-lang-index-manager.ts +250 -7
- package/src/lsp/domain-lang-node-kind-provider.ts +119 -0
- package/src/lsp/domain-lang-scope-provider.ts +250 -0
- package/src/lsp/domain-lang-workspace-manager.ts +187 -4
- package/src/lsp/hover/domain-lang-hover.ts +189 -131
- package/src/lsp/hover/hover-builders.ts +208 -0
- package/src/main.ts +156 -23
- package/src/sdk/index.ts +2 -1
- package/src/sdk/loader-node.ts +2 -1
- package/src/sdk/loader.ts +125 -34
- package/src/sdk/query.ts +15 -11
- package/src/services/import-resolver.ts +60 -9
- package/src/services/package-boundary-detector.ts +238 -0
- package/src/services/performance-optimizer.ts +6 -2
- package/src/services/types.ts +25 -0
- package/src/services/workspace-manager.ts +259 -62
- package/src/utils/import-utils.ts +27 -15
- package/src/validation/constants.ts +47 -6
- package/src/validation/import.ts +124 -16
- package/src/validation/maps.ts +118 -4
package/src/sdk/query.ts
CHANGED
|
@@ -137,9 +137,7 @@ class QueryImpl implements Query {
|
|
|
137
137
|
* Lazily builds and caches indexes on first access.
|
|
138
138
|
*/
|
|
139
139
|
private getIndexes(): ModelIndexes {
|
|
140
|
-
|
|
141
|
-
this.indexes = buildIndexes(this.model);
|
|
142
|
-
}
|
|
140
|
+
this.indexes ??= buildIndexes(this.model);
|
|
143
141
|
return this.indexes;
|
|
144
142
|
}
|
|
145
143
|
|
|
@@ -202,7 +200,12 @@ class QueryImpl implements Query {
|
|
|
202
200
|
|
|
203
201
|
/** @internal Generator for relationship iteration */
|
|
204
202
|
private *iterateRelationships(): Generator<RelationshipView> {
|
|
205
|
-
|
|
203
|
+
yield* this.collectBoundedContextRelationships();
|
|
204
|
+
yield* this.collectContextMapRelationships();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** @internal Collects relationships from bounded contexts */
|
|
208
|
+
private *collectBoundedContextRelationships(): Generator<RelationshipView> {
|
|
206
209
|
for (const node of AstUtils.streamAllContents(this.model)) {
|
|
207
210
|
if (isBoundedContext(node)) {
|
|
208
211
|
for (const rel of node.relationships) {
|
|
@@ -213,8 +216,10 @@ class QueryImpl implements Query {
|
|
|
213
216
|
}
|
|
214
217
|
}
|
|
215
218
|
}
|
|
219
|
+
}
|
|
216
220
|
|
|
217
|
-
|
|
221
|
+
/** @internal Collects relationships from context maps */
|
|
222
|
+
private *collectContextMapRelationships(): Generator<RelationshipView> {
|
|
218
223
|
for (const node of AstUtils.streamAllContents(this.model)) {
|
|
219
224
|
if (isContextMap(node)) {
|
|
220
225
|
for (const rel of node.relationships) {
|
|
@@ -442,10 +447,9 @@ class QueryBuilderImpl<T> implements QueryBuilder<T> {
|
|
|
442
447
|
}
|
|
443
448
|
|
|
444
449
|
first(): T | undefined {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
return undefined;
|
|
450
|
+
const iterator = this[Symbol.iterator]();
|
|
451
|
+
const result = iterator.next();
|
|
452
|
+
return result.done ? undefined : result.value;
|
|
449
453
|
}
|
|
450
454
|
|
|
451
455
|
toArray(): T[] {
|
|
@@ -554,7 +558,7 @@ class BcQueryBuilderImpl extends QueryBuilderImpl<BoundedContext> implements BcQ
|
|
|
554
558
|
* Escapes special regex characters in a string.
|
|
555
559
|
*/
|
|
556
560
|
function escapeRegex(str: string): string {
|
|
557
|
-
return str.
|
|
561
|
+
return str.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
|
|
558
562
|
}
|
|
559
563
|
|
|
560
564
|
/**
|
|
@@ -780,7 +784,7 @@ function augmentModelInternal(model: Model): void {
|
|
|
780
784
|
} else if (isContextMap(node)) {
|
|
781
785
|
// Augment relationships in context maps (no containing BC)
|
|
782
786
|
for (const rel of node.relationships) {
|
|
783
|
-
augmentRelationship(rel
|
|
787
|
+
augmentRelationship(rel);
|
|
784
788
|
}
|
|
785
789
|
}
|
|
786
790
|
}
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { URI, type LangiumDocument } from 'langium';
|
|
3
|
+
import { DocumentState, SimpleCache, WorkspaceCache, URI, type LangiumDocument, type LangiumSharedCoreServices } from 'langium';
|
|
4
4
|
import { WorkspaceManager } from './workspace-manager.js';
|
|
5
5
|
import type { DomainLangServices } from '../domain-lang-module.js';
|
|
6
6
|
import type { LockFile } from './types.js';
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Cache interface for import resolution.
|
|
10
|
+
* Uses WorkspaceCache in LSP mode (clears on ANY document change) or SimpleCache in standalone mode.
|
|
11
|
+
*/
|
|
12
|
+
type ResolverCache = WorkspaceCache<string, URI> | SimpleCache<string, URI>;
|
|
13
|
+
|
|
8
14
|
/**
|
|
9
15
|
* ImportResolver resolves import statements using manifest-centric rules (PRS-010).
|
|
10
16
|
*
|
|
@@ -17,21 +23,66 @@ import type { LockFile } from './types.js';
|
|
|
17
23
|
* - ./types → ./types/index.dlang → ./types.dlang
|
|
18
24
|
* - Module entry defaults to index.dlang (no model.yaml required)
|
|
19
25
|
*
|
|
20
|
-
*
|
|
21
|
-
* -
|
|
22
|
-
*
|
|
26
|
+
* Caching Strategy (uses Langium standard infrastructure):
|
|
27
|
+
* - LSP mode: Uses `WorkspaceCache` - clears on ANY document change in workspace
|
|
28
|
+
* This is necessary because file moves/deletes affect resolution of OTHER documents
|
|
29
|
+
* - Standalone mode: Uses `SimpleCache` - manual invalidation via clearCache()
|
|
30
|
+
*
|
|
31
|
+
* Why WorkspaceCache (not DocumentCache)?
|
|
32
|
+
* - DocumentCache only invalidates when the KEYED document changes
|
|
33
|
+
* - But import resolution can break when IMPORTED files are moved/deleted
|
|
34
|
+
* - Example: index.dlang imports @domains → domains/index.dlang
|
|
35
|
+
* If domains/index.dlang is moved, index.dlang's cache entry must be cleared
|
|
36
|
+
* DocumentCache wouldn't clear it (index.dlang didn't change)
|
|
37
|
+
* WorkspaceCache clears on ANY change, ensuring correct re-resolution
|
|
38
|
+
*
|
|
39
|
+
* @see https://langium.org/docs/recipes/caching/ for Langium caching patterns
|
|
23
40
|
*/
|
|
24
41
|
export class ImportResolver {
|
|
25
42
|
private readonly workspaceManager: WorkspaceManager;
|
|
26
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Workspace-level cache for resolved import URIs.
|
|
45
|
+
* In LSP mode: WorkspaceCache - clears when ANY document changes (correct for imports)
|
|
46
|
+
* In standalone mode: SimpleCache - manual invalidation via clearCache()
|
|
47
|
+
*/
|
|
48
|
+
private readonly resolverCache: ResolverCache;
|
|
27
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Creates an ImportResolver.
|
|
52
|
+
*
|
|
53
|
+
* @param services - DomainLang services. If `services.shared` is present, uses WorkspaceCache
|
|
54
|
+
* for automatic invalidation. Otherwise uses SimpleCache for standalone mode.
|
|
55
|
+
*/
|
|
28
56
|
constructor(services: DomainLangServices) {
|
|
29
57
|
this.workspaceManager = services.imports.WorkspaceManager;
|
|
58
|
+
|
|
59
|
+
// Use Langium's WorkspaceCache when shared services are available (LSP mode)
|
|
60
|
+
// Fall back to SimpleCache for standalone utilities (SDK, CLI)
|
|
61
|
+
const shared = (services as DomainLangServices & { shared?: LangiumSharedCoreServices }).shared;
|
|
62
|
+
if (shared) {
|
|
63
|
+
// LSP mode: WorkspaceCache with DocumentState.Linked
|
|
64
|
+
//
|
|
65
|
+
// This follows the standard pattern used by TypeScript, rust-analyzer, gopls:
|
|
66
|
+
// - Cache is valid for a "workspace snapshot"
|
|
67
|
+
// - Invalidates after a batch of changes completes linking (debounced ~300ms)
|
|
68
|
+
// - Invalidates immediately on file deletion
|
|
69
|
+
// - Does NOT invalidate during typing (would be too expensive)
|
|
70
|
+
//
|
|
71
|
+
// DocumentState.Linked is the right phase because:
|
|
72
|
+
// - Import resolution is needed during linking
|
|
73
|
+
// - By the time linking completes, we know which files exist
|
|
74
|
+
// - File renames appear as delete+create, triggering immediate invalidation
|
|
75
|
+
this.resolverCache = new WorkspaceCache(shared, DocumentState.Linked);
|
|
76
|
+
} else {
|
|
77
|
+
// Standalone mode: simple key-value cache, manual invalidation
|
|
78
|
+
this.resolverCache = new SimpleCache<string, URI>();
|
|
79
|
+
}
|
|
30
80
|
}
|
|
31
81
|
|
|
32
82
|
/**
|
|
33
|
-
* Clears the import resolution cache.
|
|
34
|
-
*
|
|
83
|
+
* Clears the entire import resolution cache.
|
|
84
|
+
* In LSP mode, this is also triggered automatically by WorkspaceCache on any document change.
|
|
85
|
+
* Call explicitly when model.yaml or model.lock changes.
|
|
35
86
|
*/
|
|
36
87
|
clearCache(): void {
|
|
37
88
|
this.resolverCache.clear();
|
|
@@ -39,10 +90,10 @@ export class ImportResolver {
|
|
|
39
90
|
|
|
40
91
|
/**
|
|
41
92
|
* Resolve an import specifier relative to a Langium document.
|
|
42
|
-
* Results are cached
|
|
93
|
+
* Results are cached using WorkspaceCache (clears on any workspace change).
|
|
43
94
|
*/
|
|
44
95
|
async resolveForDocument(document: LangiumDocument, specifier: string): Promise<URI> {
|
|
45
|
-
//
|
|
96
|
+
// Cache key combines document URI + specifier for uniqueness
|
|
46
97
|
const cacheKey = `${document.uri.toString()}|${specifier}`;
|
|
47
98
|
const cached = this.resolverCache.get(cacheKey);
|
|
48
99
|
if (cached) {
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package Boundary Detector
|
|
3
|
+
*
|
|
4
|
+
* Determines package boundaries for import scoping.
|
|
5
|
+
* Per ADR-003, package boundaries are defined by:
|
|
6
|
+
* - External packages: Files within .dlang/packages/ sharing the same model.yaml
|
|
7
|
+
* - Local files: Each file is its own boundary (non-transitive)
|
|
8
|
+
*
|
|
9
|
+
* Used by DomainLangScopeProvider to enable transitive imports within
|
|
10
|
+
* package boundaries while keeping local file imports non-transitive.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as path from 'node:path';
|
|
14
|
+
import * as fs from 'node:fs/promises';
|
|
15
|
+
import { URI } from 'langium';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Detects and caches package boundaries for efficient scope resolution.
|
|
19
|
+
*/
|
|
20
|
+
export class PackageBoundaryDetector {
|
|
21
|
+
/**
|
|
22
|
+
* Cache mapping document URI to its package root path.
|
|
23
|
+
* - External packages: path to directory containing model.yaml
|
|
24
|
+
* - Local files: null (no package boundary)
|
|
25
|
+
*/
|
|
26
|
+
private readonly packageRootCache = new Map<string, string | null>();
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Determines if a document is part of an external package.
|
|
30
|
+
*
|
|
31
|
+
* External packages are stored in .dlang/packages/owner/repo/commit/
|
|
32
|
+
*
|
|
33
|
+
* @param documentUri - The URI of the document to check
|
|
34
|
+
* @returns true if document is in an external package
|
|
35
|
+
*/
|
|
36
|
+
isExternalPackage(documentUri: URI | string): boolean {
|
|
37
|
+
const fsPath = this.toFsPath(documentUri);
|
|
38
|
+
const normalized = fsPath.split(path.sep);
|
|
39
|
+
|
|
40
|
+
// Check if path contains .dlang/packages/
|
|
41
|
+
const dlangIndex = normalized.indexOf('.dlang');
|
|
42
|
+
if (dlangIndex === -1) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return dlangIndex + 1 < normalized.length &&
|
|
47
|
+
normalized[dlangIndex + 1] === 'packages';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Gets the package root for a document.
|
|
52
|
+
*
|
|
53
|
+
* For external packages (.dlang/packages/), walks up from the document
|
|
54
|
+
* to find the nearest model.yaml file within the package structure.
|
|
55
|
+
*
|
|
56
|
+
* For local files, returns null (no package boundary).
|
|
57
|
+
*
|
|
58
|
+
* @param documentUri - The URI of the document
|
|
59
|
+
* @returns Absolute path to package root, or null if not in a package
|
|
60
|
+
*/
|
|
61
|
+
async getPackageRoot(documentUri: URI | string): Promise<string | null> {
|
|
62
|
+
const uriString = documentUri.toString();
|
|
63
|
+
|
|
64
|
+
// Check cache first
|
|
65
|
+
if (this.packageRootCache.has(uriString)) {
|
|
66
|
+
return this.packageRootCache.get(uriString) ?? null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// If not an external package, it has no package boundary
|
|
70
|
+
if (!this.isExternalPackage(documentUri)) {
|
|
71
|
+
this.packageRootCache.set(uriString, null);
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const fsPath = this.toFsPath(documentUri);
|
|
76
|
+
const packageRoot = await this.findPackageRootForExternal(fsPath);
|
|
77
|
+
this.packageRootCache.set(uriString, packageRoot);
|
|
78
|
+
return packageRoot;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Checks if two documents are in the same package (synchronous heuristic).
|
|
83
|
+
*
|
|
84
|
+
* This is a fast, synchronous check that compares package commit directories
|
|
85
|
+
* without filesystem access. Documents are in the same package if:
|
|
86
|
+
* - Both are in .dlang/packages/ AND
|
|
87
|
+
* - They share the same owner/repo/commit path
|
|
88
|
+
*
|
|
89
|
+
* This is used by the scope provider which needs synchronous access.
|
|
90
|
+
*
|
|
91
|
+
* Structure: .dlang/packages/owner/repo/commit/...
|
|
92
|
+
*
|
|
93
|
+
* @param doc1Uri - URI of first document
|
|
94
|
+
* @param doc2Uri - URI of second document
|
|
95
|
+
* @returns true if both are in the same package commit directory
|
|
96
|
+
*/
|
|
97
|
+
areInSamePackageSync(doc1Uri: URI | string, doc2Uri: URI | string): boolean {
|
|
98
|
+
// Both must be external packages
|
|
99
|
+
if (!this.isExternalPackage(doc1Uri) || !this.isExternalPackage(doc2Uri)) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const path1 = this.toFsPath(doc1Uri);
|
|
104
|
+
const path2 = this.toFsPath(doc2Uri);
|
|
105
|
+
|
|
106
|
+
const root1 = this.getPackageCommitDirectory(path1);
|
|
107
|
+
const root2 = this.getPackageCommitDirectory(path2);
|
|
108
|
+
|
|
109
|
+
return root1 !== null && root1 === root2;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Gets the package commit directory (owner/repo/commit) from a path.
|
|
114
|
+
*
|
|
115
|
+
* @param fsPath - Filesystem path
|
|
116
|
+
* @returns Commit directory path or null
|
|
117
|
+
*/
|
|
118
|
+
private getPackageCommitDirectory(fsPath: string): string | null {
|
|
119
|
+
const normalized = fsPath.split(path.sep);
|
|
120
|
+
const dlangIndex = normalized.indexOf('.dlang');
|
|
121
|
+
|
|
122
|
+
if (dlangIndex === -1) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const packagesIndex = dlangIndex + 1;
|
|
127
|
+
if (packagesIndex >= normalized.length || normalized[packagesIndex] !== 'packages') {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Commit directory is at: .dlang/packages/owner/repo/commit
|
|
132
|
+
const commitIndex = packagesIndex + 3;
|
|
133
|
+
if (commitIndex >= normalized.length) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Return the path up to and including the commit directory
|
|
138
|
+
return normalized.slice(0, commitIndex + 1).join(path.sep);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Checks if two documents are in the same package.
|
|
143
|
+
*
|
|
144
|
+
* Documents are in the same package if:
|
|
145
|
+
* - Both are external packages AND
|
|
146
|
+
* - They share the same package root (model.yaml location)
|
|
147
|
+
*
|
|
148
|
+
* Local files are never in the same package (each is isolated).
|
|
149
|
+
*
|
|
150
|
+
* @param doc1Uri - URI of first document
|
|
151
|
+
* @param doc2Uri - URI of second document
|
|
152
|
+
* @returns true if both documents are in the same package
|
|
153
|
+
*/
|
|
154
|
+
async areInSamePackage(doc1Uri: URI | string, doc2Uri: URI | string): Promise<boolean> {
|
|
155
|
+
const root1 = await this.getPackageRoot(doc1Uri);
|
|
156
|
+
const root2 = await this.getPackageRoot(doc2Uri);
|
|
157
|
+
|
|
158
|
+
// If either is not in a package, they can't be in the same package
|
|
159
|
+
if (!root1 || !root2) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return root1 === root2;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Finds the package root for an external package by walking up to find model.yaml.
|
|
168
|
+
*
|
|
169
|
+
* External packages have structure: .dlang/packages/owner/repo/commit/...
|
|
170
|
+
* The model.yaml should be at the commit level or just below it.
|
|
171
|
+
*
|
|
172
|
+
* @param fsPath - Filesystem path of the document
|
|
173
|
+
* @returns Path to directory containing model.yaml, or null
|
|
174
|
+
*/
|
|
175
|
+
private async findPackageRootForExternal(fsPath: string): Promise<string | null> {
|
|
176
|
+
const normalized = fsPath.split(path.sep);
|
|
177
|
+
const dlangIndex = normalized.indexOf('.dlang');
|
|
178
|
+
|
|
179
|
+
if (dlangIndex === -1) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Find the packages directory
|
|
184
|
+
const packagesIndex = dlangIndex + 1;
|
|
185
|
+
if (packagesIndex >= normalized.length || normalized[packagesIndex] !== 'packages') {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Start from the commit directory level
|
|
190
|
+
// Structure: .dlang/packages/owner/repo/commit/
|
|
191
|
+
const commitIndex = packagesIndex + 3;
|
|
192
|
+
if (commitIndex >= normalized.length) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Walk up from the document path to the commit directory
|
|
197
|
+
let currentPath = path.dirname(fsPath);
|
|
198
|
+
const commitPath = normalized.slice(0, commitIndex + 1).join(path.sep);
|
|
199
|
+
|
|
200
|
+
// Search upward for model.yaml, but don't go above the commit directory
|
|
201
|
+
while (currentPath.length >= commitPath.length) {
|
|
202
|
+
const manifestPath = path.join(currentPath, 'model.yaml');
|
|
203
|
+
try {
|
|
204
|
+
await fs.access(manifestPath);
|
|
205
|
+
return currentPath;
|
|
206
|
+
} catch {
|
|
207
|
+
// model.yaml not found at this level, continue upward
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const parent = path.dirname(currentPath);
|
|
211
|
+
if (parent === currentPath) {
|
|
212
|
+
// Reached filesystem root without finding model.yaml
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
currentPath = parent;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Converts a URI to a filesystem path.
|
|
223
|
+
*/
|
|
224
|
+
private toFsPath(uri: URI | string): string {
|
|
225
|
+
if (typeof uri === 'string') {
|
|
226
|
+
uri = URI.parse(uri);
|
|
227
|
+
}
|
|
228
|
+
return uri.fsPath;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Clears the package root cache.
|
|
233
|
+
* Call this when packages are installed/removed.
|
|
234
|
+
*/
|
|
235
|
+
clearCache(): void {
|
|
236
|
+
this.packageRootCache.clear();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -76,7 +76,8 @@ export class PerformanceOptimizer {
|
|
|
76
76
|
|
|
77
77
|
try {
|
|
78
78
|
const content = await fs.readFile(manifestPath, 'utf-8');
|
|
79
|
-
const
|
|
79
|
+
const { parse } = await import('yaml');
|
|
80
|
+
const manifest: unknown = parse(content);
|
|
80
81
|
|
|
81
82
|
this.manifestCache.set(cacheKey, {
|
|
82
83
|
value: manifest,
|
|
@@ -127,7 +128,10 @@ export class PerformanceOptimizer {
|
|
|
127
128
|
const stat = await fs.stat(lockPath);
|
|
128
129
|
const cached = this.lockFileCache.get(workspaceRoot);
|
|
129
130
|
|
|
130
|
-
|
|
131
|
+
// Floor mtimeMs to integer precision to match Date.now() —
|
|
132
|
+
// some filesystems (e.g. APFS) report sub-millisecond mtime,
|
|
133
|
+
// which can exceed the integer timestamp from Date.now().
|
|
134
|
+
if (cached && Math.floor(stat.mtimeMs) > cached.timestamp) {
|
|
131
135
|
stale.push(workspaceRoot);
|
|
132
136
|
}
|
|
133
137
|
} catch {
|
package/src/services/types.ts
CHANGED
|
@@ -42,6 +42,31 @@
|
|
|
42
42
|
// Core Building Blocks
|
|
43
43
|
// ============================================================================
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Information about an import statement tracked during indexing.
|
|
47
|
+
*
|
|
48
|
+
* Used by IndexManager to track both the import's resolved location
|
|
49
|
+
* and its alias (if any) for scope resolution.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* // import "larsbaunwall/ddd-types" as ddd
|
|
54
|
+
* const info: ImportInfo = {
|
|
55
|
+
* specifier: "larsbaunwall/ddd-types",
|
|
56
|
+
* alias: "ddd",
|
|
57
|
+
* resolvedUri: "file:///.dlang/packages/larsbaunwall/ddd-types/abc123/index.dlang"
|
|
58
|
+
* };
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export interface ImportInfo {
|
|
62
|
+
/** The import specifier as written in source (e.g., "./file.dlang", "owner/repo") */
|
|
63
|
+
readonly specifier: string;
|
|
64
|
+
/** Optional alias from 'as' clause (e.g., "ddd" in 'import "pkg" as ddd') */
|
|
65
|
+
readonly alias?: string;
|
|
66
|
+
/** Resolved absolute URI of the imported document */
|
|
67
|
+
readonly resolvedUri: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
45
70
|
/**
|
|
46
71
|
* Type of git reference for version pinning.
|
|
47
72
|
*
|