@domainlang/language 0.7.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.
Files changed (83) hide show
  1. package/out/domain-lang-module.d.ts +2 -0
  2. package/out/domain-lang-module.js +21 -2
  3. package/out/domain-lang-module.js.map +1 -1
  4. package/out/lsp/domain-lang-completion.d.ts +142 -1
  5. package/out/lsp/domain-lang-completion.js +620 -22
  6. package/out/lsp/domain-lang-completion.js.map +1 -1
  7. package/out/lsp/domain-lang-document-symbol-provider.d.ts +79 -0
  8. package/out/lsp/domain-lang-document-symbol-provider.js +210 -0
  9. package/out/lsp/domain-lang-document-symbol-provider.js.map +1 -0
  10. package/out/lsp/domain-lang-index-manager.d.ts +34 -5
  11. package/out/lsp/domain-lang-index-manager.js +66 -27
  12. package/out/lsp/domain-lang-index-manager.js.map +1 -1
  13. package/out/lsp/domain-lang-node-kind-provider.d.ts +27 -0
  14. package/out/lsp/domain-lang-node-kind-provider.js +87 -0
  15. package/out/lsp/domain-lang-node-kind-provider.js.map +1 -0
  16. package/out/lsp/domain-lang-scope-provider.d.ts +53 -20
  17. package/out/lsp/domain-lang-scope-provider.js +119 -44
  18. package/out/lsp/domain-lang-scope-provider.js.map +1 -1
  19. package/out/lsp/domain-lang-workspace-manager.d.ts +23 -2
  20. package/out/lsp/domain-lang-workspace-manager.js +51 -6
  21. package/out/lsp/domain-lang-workspace-manager.js.map +1 -1
  22. package/out/lsp/hover/domain-lang-hover.d.ts +16 -6
  23. package/out/lsp/hover/domain-lang-hover.js +160 -134
  24. package/out/lsp/hover/domain-lang-hover.js.map +1 -1
  25. package/out/lsp/hover/hover-builders.d.ts +57 -0
  26. package/out/lsp/hover/hover-builders.js +171 -0
  27. package/out/lsp/hover/hover-builders.js.map +1 -0
  28. package/out/main.js +2 -1
  29. package/out/main.js.map +1 -1
  30. package/out/sdk/index.d.ts +2 -1
  31. package/out/sdk/index.js +1 -1
  32. package/out/sdk/index.js.map +1 -1
  33. package/out/sdk/loader-node.js +1 -1
  34. package/out/sdk/loader-node.js.map +1 -1
  35. package/out/sdk/loader.d.ts +55 -2
  36. package/out/sdk/loader.js +87 -28
  37. package/out/sdk/loader.js.map +1 -1
  38. package/out/sdk/query.js +14 -11
  39. package/out/sdk/query.js.map +1 -1
  40. package/out/services/package-boundary-detector.d.ts +101 -0
  41. package/out/services/package-boundary-detector.js +211 -0
  42. package/out/services/package-boundary-detector.js.map +1 -0
  43. package/out/services/performance-optimizer.js +6 -2
  44. package/out/services/performance-optimizer.js.map +1 -1
  45. package/out/services/types.d.ts +24 -0
  46. package/out/services/types.js.map +1 -1
  47. package/out/services/workspace-manager.d.ts +73 -6
  48. package/out/services/workspace-manager.js +210 -57
  49. package/out/services/workspace-manager.js.map +1 -1
  50. package/out/utils/import-utils.d.ts +9 -6
  51. package/out/utils/import-utils.js +26 -15
  52. package/out/utils/import-utils.js.map +1 -1
  53. package/out/validation/constants.d.ts +7 -0
  54. package/out/validation/constants.js +21 -3
  55. package/out/validation/constants.js.map +1 -1
  56. package/out/validation/import.d.ts +11 -1
  57. package/out/validation/import.js +42 -14
  58. package/out/validation/import.js.map +1 -1
  59. package/out/validation/maps.js +50 -1
  60. package/out/validation/maps.js.map +1 -1
  61. package/package.json +5 -5
  62. package/src/domain-lang-module.ts +24 -3
  63. package/src/lsp/domain-lang-completion.ts +736 -27
  64. package/src/lsp/domain-lang-document-symbol-provider.ts +254 -0
  65. package/src/lsp/domain-lang-index-manager.ts +79 -27
  66. package/src/lsp/domain-lang-node-kind-provider.ts +119 -0
  67. package/src/lsp/domain-lang-scope-provider.ts +171 -55
  68. package/src/lsp/domain-lang-workspace-manager.ts +64 -6
  69. package/src/lsp/hover/domain-lang-hover.ts +189 -131
  70. package/src/lsp/hover/hover-builders.ts +208 -0
  71. package/src/main.ts +3 -1
  72. package/src/sdk/index.ts +2 -1
  73. package/src/sdk/loader-node.ts +2 -1
  74. package/src/sdk/loader.ts +125 -34
  75. package/src/sdk/query.ts +15 -11
  76. package/src/services/package-boundary-detector.ts +238 -0
  77. package/src/services/performance-optimizer.ts +6 -2
  78. package/src/services/types.ts +25 -0
  79. package/src/services/workspace-manager.ts +259 -62
  80. package/src/utils/import-utils.ts +27 -15
  81. package/src/validation/constants.ts +23 -6
  82. package/src/validation/import.ts +49 -14
  83. package/src/validation/maps.ts +59 -2
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * DomainLang Scope Provider
3
3
  *
4
- * Implements import-based scoping for the DomainLang DSL.
4
+ * Implements import-based scoping with alias support and package-boundary transitive imports.
5
5
  *
6
- * **Key Concept:**
7
- * Unlike languages with global namespaces, DomainLang enforces strict import-based scoping:
8
- * - Elements are only visible if they are defined in the current document OR explicitly imported
9
- * - The global scope is restricted to imported documents only
10
- * - Transitive imports do NOT provide scope (only direct imports)
6
+ * **Key Concepts (per ADR-003):**
7
+ * - Elements are only visible if defined in current document OR explicitly imported
8
+ * - Import aliases control visibility: `import "pkg" as ddd` makes types visible as `ddd.*` only
9
+ * - Package-boundary transitive imports: External packages (.dlang/packages/) can re-export
10
+ * - Local file imports remain non-transitive (explicit dependencies only)
11
11
  *
12
12
  * **Why this matters:**
13
13
  * Without this, Langium's DefaultScopeProvider would make ALL indexed documents visible
@@ -17,6 +17,7 @@
17
17
  * 3. Create confusion about dependencies between files
18
18
  *
19
19
  * @see https://langium.org/docs/recipes/scoping/ for Langium scoping patterns
20
+ * @see ADR-003 for alias and package-boundary design decisions
20
21
  */
21
22
 
22
23
  import type {
@@ -30,10 +31,13 @@ import {
30
31
  AstUtils,
31
32
  DefaultScopeProvider,
32
33
  EMPTY_SCOPE,
33
- MapScope
34
+ MapScope,
35
+ stream
34
36
  } from 'langium';
35
37
  import type { DomainLangServices } from '../domain-lang-module.js';
36
38
  import type { DomainLangIndexManager } from './domain-lang-index-manager.js';
39
+ import type { PackageBoundaryDetector } from '../services/package-boundary-detector.js';
40
+ import type { ImportInfo } from '../services/types.js';
37
41
 
38
42
  /**
39
43
  * Custom scope provider that restricts cross-file references to imported documents only.
@@ -42,93 +46,205 @@ import type { DomainLangIndexManager } from './domain-lang-index-manager.js';
42
46
  */
43
47
  export class DomainLangScopeProvider extends DefaultScopeProvider {
44
48
  /**
45
- * Reference to IndexManager for getting resolved imports.
49
+ * Reference to IndexManager for getting resolved imports with aliases.
46
50
  */
47
51
  private readonly domainLangIndexManager: DomainLangIndexManager;
48
52
 
53
+ /**
54
+ * Detects package boundaries for transitive import resolution.
55
+ */
56
+ private readonly packageBoundaryDetector: PackageBoundaryDetector;
57
+
49
58
  constructor(services: DomainLangServices) {
50
59
  super(services);
51
60
  this.domainLangIndexManager = services.shared.workspace.IndexManager as DomainLangIndexManager;
61
+ this.packageBoundaryDetector = services.imports.PackageBoundaryDetector;
52
62
  }
53
63
 
54
64
  /**
55
- * Override getGlobalScope to restrict it to imported documents only.
65
+ * Override getGlobalScope to implement alias-scoped and package-boundary transitive imports.
56
66
  *
57
67
  * The default Langium behavior includes ALL documents in the workspace.
58
- * We restrict this to:
68
+ * We restrict and transform scope to:
59
69
  * 1. The current document's own exported symbols
60
- * 2. Exported symbols from directly imported documents
70
+ * 2. Symbols from directly imported documents (with alias prefixing)
71
+ * 3. Symbols from package-boundary transitive imports (external packages only)
61
72
  *
62
73
  * @param referenceType - The AST type being referenced
63
74
  * @param context - Information about the reference
64
75
  * @returns A scope containing only visible elements
65
76
  */
66
77
  protected override getGlobalScope(referenceType: string, context: ReferenceInfo): Scope {
67
- const document = AstUtils.getDocument(context.container);
68
- if (!document) {
78
+ try {
79
+ const document = AstUtils.getDocument(context.container);
80
+ if (!document) {
81
+ return EMPTY_SCOPE;
82
+ }
83
+
84
+ const descriptions = this.computeVisibleDescriptions(referenceType, document);
85
+ return new MapScope(descriptions);
86
+ } catch (error) {
87
+ console.error('Error in getGlobalScope:', error);
69
88
  return EMPTY_SCOPE;
70
89
  }
71
-
72
- // Get the set of URIs that are in scope for this document
73
- const importedUris = this.getImportedDocumentUris(document);
74
-
75
- // Filter the global index to only include descriptions from imported documents
76
- const filteredDescriptions = this.filterDescriptionsByImports(
77
- referenceType,
78
- document,
79
- importedUris
80
- );
81
-
82
- // Create a scope from the filtered descriptions
83
- return new MapScope(filteredDescriptions);
84
90
  }
85
91
 
86
92
  /**
87
- * Gets the set of document URIs that are directly imported by the given document.
88
- *
89
- * Uses the resolved imports tracked by DomainLangIndexManager during indexing.
90
- * This ensures accurate resolution including path aliases.
93
+ * Computes all visible descriptions for a document, including:
94
+ * - Current document's own symbols
95
+ * - Direct imports (with alias prefixing)
96
+ * - Package-boundary transitive imports
91
97
  *
92
- * @param document - The document to get imports for
93
- * @returns Set of imported document URIs (as strings)
98
+ * @param referenceType - The AST type being referenced
99
+ * @param document - The document making the reference
100
+ * @returns Stream of visible descriptions
94
101
  */
95
- private getImportedDocumentUris(document: LangiumDocument): Set<string> {
102
+ private computeVisibleDescriptions(
103
+ referenceType: string,
104
+ document: LangiumDocument
105
+ ): Stream<AstNodeDescription> {
96
106
  const docUri = document.uri.toString();
107
+ const allVisibleDescriptions: AstNodeDescription[] = [];
108
+
109
+ // 1. Always include current document's own symbols
110
+ const ownDescriptions = this.indexManager.allElements(referenceType)
111
+ .filter(desc => desc.documentUri.toString() === docUri);
112
+ allVisibleDescriptions.push(...ownDescriptions.toArray());
113
+
114
+ // 2. Get import info (with aliases)
115
+ const importInfo = this.domainLangIndexManager.getImportInfo(docUri);
97
116
 
98
- // Get resolved imports from the index manager (tracked during indexing)
99
- const resolvedImports = this.domainLangIndexManager.getResolvedImports(docUri);
117
+ // Track which documents we've already included to avoid duplicates
118
+ const processedUris = new Set<string>([docUri]);
100
119
 
101
- // Always include the current document itself
102
- const importedUris = new Set<string>([docUri]);
120
+ // 3. Process each direct import
121
+ for (const imp of importInfo) {
122
+ if (!imp.resolvedUri || processedUris.has(imp.resolvedUri)) {
123
+ continue;
124
+ }
103
125
 
104
- // Add all resolved import URIs
105
- for (const resolvedUri of resolvedImports) {
106
- importedUris.add(resolvedUri);
126
+ // Add descriptions from the directly imported document
127
+ this.addDescriptionsFromImport(
128
+ imp,
129
+ referenceType,
130
+ processedUris,
131
+ allVisibleDescriptions
132
+ );
133
+
134
+ // 4. Check for package-boundary transitive imports
135
+ this.addPackageBoundaryTransitiveImports(
136
+ imp,
137
+ referenceType,
138
+ document,
139
+ processedUris,
140
+ allVisibleDescriptions
141
+ );
107
142
  }
108
143
 
109
- return importedUris;
144
+ return stream(allVisibleDescriptions);
110
145
  }
111
146
 
112
147
  /**
113
- * Filters the global index to only include descriptions from imported documents.
148
+ * Adds descriptions from a single import, applying alias prefixing if needed.
114
149
  *
150
+ * @param imp - Import information (specifier, alias, resolved URI)
151
+ * @param referenceType - The AST type being referenced
152
+ * @param processedUris - Set of already-processed URIs to avoid duplicates
153
+ * @param output - Array to append visible descriptions to
154
+ */
155
+ private addDescriptionsFromImport(
156
+ imp: ImportInfo,
157
+ referenceType: string,
158
+ processedUris: Set<string>,
159
+ output: AstNodeDescription[]
160
+ ): void {
161
+ const descriptions = this.indexManager.allElements(referenceType)
162
+ .filter(desc => desc.documentUri.toString() === imp.resolvedUri);
163
+
164
+ if (imp.alias) {
165
+ // With alias: prefix all names with alias
166
+ // Example: CoreDomain → ddd.CoreDomain
167
+ for (const desc of descriptions) {
168
+ output.push(this.createAliasedDescription(desc, imp.alias));
169
+ }
170
+ } else {
171
+ // Without alias: use original names
172
+ output.push(...descriptions.toArray());
173
+ }
174
+
175
+ processedUris.add(imp.resolvedUri);
176
+ }
177
+
178
+ /**
179
+ * Adds package-boundary transitive imports for external packages.
180
+ *
181
+ * When document A imports package document B (e.g., index.dlang),
182
+ * and B imports internal package files C, D, etc. (same package root),
183
+ * then A can see types from C, D, etc. (package re-exports).
184
+ *
185
+ * Local file imports remain non-transitive.
186
+ *
187
+ * @param imp - Import information for the direct import
115
188
  * @param referenceType - The AST type being referenced
116
189
  * @param currentDocument - The document making the reference
117
- * @param importedUris - Set of URIs that are in scope
118
- * @returns Stream of filtered descriptions
190
+ * @param processedUris - Set of already-processed URIs to avoid duplicates
191
+ * @param output - Array to append visible descriptions to
119
192
  */
120
- private filterDescriptionsByImports(
193
+ private addPackageBoundaryTransitiveImports(
194
+ imp: ImportInfo,
121
195
  referenceType: string,
122
196
  currentDocument: LangiumDocument,
123
- importedUris: Set<string>
124
- ): Stream<AstNodeDescription> {
125
- // Get all descriptions of the reference type from the index
126
- const allDescriptions = this.indexManager.allElements(referenceType);
127
-
128
- // Filter to only those from imported documents
129
- return allDescriptions.filter(desc => {
130
- const descDocUri = desc.documentUri.toString();
131
- return importedUris.has(descDocUri);
132
- });
197
+ processedUris: Set<string>,
198
+ output: AstNodeDescription[]
199
+ ): void {
200
+ // Get the imports of the imported document (B's imports)
201
+ const transitiveImports = this.domainLangIndexManager.getImportInfo(imp.resolvedUri);
202
+
203
+ for (const transitiveImp of transitiveImports) {
204
+ if (!transitiveImp.resolvedUri || processedUris.has(transitiveImp.resolvedUri)) {
205
+ continue;
206
+ }
207
+
208
+ // Check if both documents are in the same external package
209
+ // (package boundary = same commit directory within .dlang/packages/)
210
+ const samePackage = this.packageBoundaryDetector.areInSamePackageSync(
211
+ imp.resolvedUri,
212
+ transitiveImp.resolvedUri
213
+ );
214
+
215
+ if (samePackage) {
216
+ // Within package boundary: include transitive imports
217
+ // Apply the top-level import's alias (if any)
218
+ this.addDescriptionsFromImport(
219
+ {
220
+ specifier: transitiveImp.specifier,
221
+ alias: imp.alias, // Use the top-level import's alias
222
+ resolvedUri: transitiveImp.resolvedUri
223
+ },
224
+ referenceType,
225
+ processedUris,
226
+ output
227
+ );
228
+ }
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Creates an alias-prefixed version of a description.
234
+ *
235
+ * Example: CoreDomain with alias "ddd" → ddd.CoreDomain
236
+ *
237
+ * @param original - Original description
238
+ * @param alias - Import alias to prefix with
239
+ * @returns New description with prefixed name
240
+ */
241
+ private createAliasedDescription(
242
+ original: AstNodeDescription,
243
+ alias: string
244
+ ): AstNodeDescription {
245
+ return {
246
+ ...original,
247
+ name: `${alias}.${original.name}`
248
+ };
133
249
  }
134
250
  }
@@ -4,6 +4,8 @@ import { DefaultWorkspaceManager, URI, UriUtils, type FileSystemNode, type Langi
4
4
  import type { CancellationToken } from 'vscode-languageserver-protocol';
5
5
  import { ensureImportGraphFromDocument } from '../utils/import-utils.js';
6
6
  import { findManifestsInDirectories } from '../utils/manifest-utils.js';
7
+ import type { ImportResolver } from '../services/import-resolver.js';
8
+ import type { DomainLangServices } from '../domain-lang-module.js';
7
9
 
8
10
  /**
9
11
  * Langium WorkspaceManager override implementing manifest-centric import loading per PRS-010.
@@ -54,11 +56,26 @@ import { findManifestsInDirectories } from '../utils/manifest-utils.js';
54
56
  export class DomainLangWorkspaceManager extends DefaultWorkspaceManager {
55
57
  private readonly sharedServices: LangiumSharedCoreServices;
56
58
 
59
+ /**
60
+ * DI-injected import resolver. Set via late-binding because
61
+ * WorkspaceManager (shared module) is created before ImportResolver (language module).
62
+ * Falls back to standalone ensureImportGraphFromDocument when not set.
63
+ */
64
+ private importResolver: ImportResolver | undefined;
65
+
57
66
  constructor(services: LangiumSharedCoreServices) {
58
67
  super(services);
59
68
  this.sharedServices = services;
60
69
  }
61
70
 
71
+ /**
72
+ * Late-binds the language-specific services after DI initialization.
73
+ * Called from `createDomainLangServices()` after the language module is created.
74
+ */
75
+ setLanguageServices(services: DomainLangServices): void {
76
+ this.importResolver = services.imports.ImportResolver;
77
+ }
78
+
62
79
  override shouldIncludeEntry(entry: FileSystemNode): boolean {
63
80
  // Prevent auto-including .dlang files; we'll load via entry/import graph
64
81
  const name = UriUtils.basename(entry.uri);
@@ -93,7 +110,7 @@ export class DomainLangWorkspaceManager extends DefaultWorkspaceManager {
93
110
  validation: true
94
111
  });
95
112
 
96
- const uris = await ensureImportGraphFromDocument(entryDoc, this.langiumDocuments);
113
+ const uris = await this.loadImportGraph(entryDoc);
97
114
  const importedDocs: LangiumDocument[] = [];
98
115
  for (const uriString of uris) {
99
116
  const uri = URI.parse(uriString);
@@ -125,7 +142,7 @@ export class DomainLangWorkspaceManager extends DefaultWorkspaceManager {
125
142
  *
126
143
  * Skips:
127
144
  * - Module directories (directories with model.yaml) - loaded via import graph
128
- * - `.dlang/cache` directory - package cache managed by CLI
145
+ * - `.dlang/packages` directory - package cache managed by CLI
129
146
  *
130
147
  * @param folders - Workspace folders to scan
131
148
  * @param moduleDirectories - Set of directories containing model.yaml (to skip)
@@ -154,7 +171,7 @@ export class DomainLangWorkspaceManager extends DefaultWorkspaceManager {
154
171
 
155
172
  /**
156
173
  * Recursively loads .dlang files from a directory.
157
- * Skips module directories and the .dlang/cache package cache.
174
+ * Skips module directories and the .dlang/packages cache.
158
175
  */
159
176
  private async loadDlangFilesRecursively(
160
177
  dirPath: string,
@@ -166,13 +183,13 @@ export class DomainLangWorkspaceManager extends DefaultWorkspaceManager {
166
183
  return [];
167
184
  }
168
185
 
169
- // Skip .dlang/cache - package cache managed by CLI
186
+ // Skip .dlang/packages - package cache managed by CLI
170
187
  const baseName = path.basename(dirPath);
171
188
  const parentName = path.basename(path.dirname(dirPath));
172
- if (baseName === 'cache' && parentName === '.dlang') {
189
+ if (baseName === 'packages' && parentName === '.dlang') {
173
190
  return [];
174
191
  }
175
- // Also skip the .dlang directory itself if it contains cache
192
+ // Also skip the .dlang directory itself (contains packages cache)
176
193
  if (baseName === '.dlang') {
177
194
  return [];
178
195
  }
@@ -249,4 +266,45 @@ export class DomainLangWorkspaceManager extends DefaultWorkspaceManager {
249
266
  const directories = folders.map(f => URI.parse(f.uri).fsPath);
250
267
  return findManifestsInDirectories(directories);
251
268
  }
269
+
270
+ /**
271
+ * Recursively builds the import graph from a document.
272
+ * Uses the DI-injected ImportResolver when available,
273
+ * falling back to the standalone utility.
274
+ *
275
+ * @param document - The starting document
276
+ * @returns Set of URIs (as strings) for all documents in the import graph
277
+ */
278
+ private async loadImportGraph(document: LangiumDocument): Promise<Set<string>> {
279
+ if (!this.importResolver) {
280
+ // Fallback to standalone utility when DI isn't wired
281
+ return ensureImportGraphFromDocument(document, this.langiumDocuments);
282
+ }
283
+
284
+ const resolver = this.importResolver;
285
+ const langiumDocuments = this.langiumDocuments;
286
+ const visited = new Set<string>();
287
+
288
+ async function visit(doc: LangiumDocument): Promise<void> {
289
+ const uriString = doc.uri.toString();
290
+ if (visited.has(uriString)) return;
291
+ visited.add(uriString);
292
+
293
+ const model = doc.parseResult.value as { imports?: Array<{ uri?: string }> };
294
+ for (const imp of model.imports ?? []) {
295
+ if (!imp.uri) continue;
296
+
297
+ try {
298
+ const resolvedUri = await resolver.resolveForDocument(doc, imp.uri);
299
+ const childDoc = await langiumDocuments.getOrCreateDocument(resolvedUri);
300
+ await visit(childDoc);
301
+ } catch {
302
+ // Import resolution failed — validation will report the error
303
+ }
304
+ }
305
+ }
306
+
307
+ await visit(document);
308
+ return visited;
309
+ }
252
310
  }