@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.
Files changed (88) hide show
  1. package/README.md +1 -1
  2. package/out/domain-lang-module.d.ts +2 -0
  3. package/out/domain-lang-module.js +23 -2
  4. package/out/domain-lang-module.js.map +1 -1
  5. package/out/lsp/domain-lang-completion.d.ts +142 -1
  6. package/out/lsp/domain-lang-completion.js +620 -22
  7. package/out/lsp/domain-lang-completion.js.map +1 -1
  8. package/out/lsp/domain-lang-document-symbol-provider.d.ts +79 -0
  9. package/out/lsp/domain-lang-document-symbol-provider.js +210 -0
  10. package/out/lsp/domain-lang-document-symbol-provider.js.map +1 -0
  11. package/out/lsp/domain-lang-index-manager.d.ts +98 -1
  12. package/out/lsp/domain-lang-index-manager.js +214 -7
  13. package/out/lsp/domain-lang-index-manager.js.map +1 -1
  14. package/out/lsp/domain-lang-node-kind-provider.d.ts +27 -0
  15. package/out/lsp/domain-lang-node-kind-provider.js +87 -0
  16. package/out/lsp/domain-lang-node-kind-provider.js.map +1 -0
  17. package/out/lsp/domain-lang-scope-provider.d.ts +100 -0
  18. package/out/lsp/domain-lang-scope-provider.js +170 -0
  19. package/out/lsp/domain-lang-scope-provider.js.map +1 -0
  20. package/out/lsp/domain-lang-workspace-manager.d.ts +46 -0
  21. package/out/lsp/domain-lang-workspace-manager.js +148 -4
  22. package/out/lsp/domain-lang-workspace-manager.js.map +1 -1
  23. package/out/lsp/hover/domain-lang-hover.d.ts +16 -6
  24. package/out/lsp/hover/domain-lang-hover.js +160 -134
  25. package/out/lsp/hover/domain-lang-hover.js.map +1 -1
  26. package/out/lsp/hover/hover-builders.d.ts +57 -0
  27. package/out/lsp/hover/hover-builders.js +171 -0
  28. package/out/lsp/hover/hover-builders.js.map +1 -0
  29. package/out/main.js +116 -20
  30. package/out/main.js.map +1 -1
  31. package/out/sdk/index.d.ts +2 -1
  32. package/out/sdk/index.js +1 -1
  33. package/out/sdk/index.js.map +1 -1
  34. package/out/sdk/loader-node.js +1 -1
  35. package/out/sdk/loader-node.js.map +1 -1
  36. package/out/sdk/loader.d.ts +55 -2
  37. package/out/sdk/loader.js +87 -28
  38. package/out/sdk/loader.js.map +1 -1
  39. package/out/sdk/query.js +14 -11
  40. package/out/sdk/query.js.map +1 -1
  41. package/out/services/import-resolver.d.ts +29 -6
  42. package/out/services/import-resolver.js +48 -9
  43. package/out/services/import-resolver.js.map +1 -1
  44. package/out/services/package-boundary-detector.d.ts +101 -0
  45. package/out/services/package-boundary-detector.js +211 -0
  46. package/out/services/package-boundary-detector.js.map +1 -0
  47. package/out/services/performance-optimizer.js +6 -2
  48. package/out/services/performance-optimizer.js.map +1 -1
  49. package/out/services/types.d.ts +24 -0
  50. package/out/services/types.js.map +1 -1
  51. package/out/services/workspace-manager.d.ts +73 -6
  52. package/out/services/workspace-manager.js +210 -57
  53. package/out/services/workspace-manager.js.map +1 -1
  54. package/out/utils/import-utils.d.ts +9 -6
  55. package/out/utils/import-utils.js +26 -15
  56. package/out/utils/import-utils.js.map +1 -1
  57. package/out/validation/constants.d.ts +20 -0
  58. package/out/validation/constants.js +39 -3
  59. package/out/validation/constants.js.map +1 -1
  60. package/out/validation/import.d.ts +22 -1
  61. package/out/validation/import.js +104 -16
  62. package/out/validation/import.js.map +1 -1
  63. package/out/validation/maps.js +101 -3
  64. package/out/validation/maps.js.map +1 -1
  65. package/package.json +5 -5
  66. package/src/domain-lang-module.ts +26 -3
  67. package/src/lsp/domain-lang-completion.ts +736 -27
  68. package/src/lsp/domain-lang-document-symbol-provider.ts +254 -0
  69. package/src/lsp/domain-lang-index-manager.ts +250 -7
  70. package/src/lsp/domain-lang-node-kind-provider.ts +119 -0
  71. package/src/lsp/domain-lang-scope-provider.ts +250 -0
  72. package/src/lsp/domain-lang-workspace-manager.ts +187 -4
  73. package/src/lsp/hover/domain-lang-hover.ts +189 -131
  74. package/src/lsp/hover/hover-builders.ts +208 -0
  75. package/src/main.ts +156 -23
  76. package/src/sdk/index.ts +2 -1
  77. package/src/sdk/loader-node.ts +2 -1
  78. package/src/sdk/loader.ts +125 -34
  79. package/src/sdk/query.ts +15 -11
  80. package/src/services/import-resolver.ts +60 -9
  81. package/src/services/package-boundary-detector.ts +238 -0
  82. package/src/services/performance-optimizer.ts +6 -2
  83. package/src/services/types.ts +25 -0
  84. package/src/services/workspace-manager.ts +259 -62
  85. package/src/utils/import-utils.ts +27 -15
  86. package/src/validation/constants.ts +47 -6
  87. package/src/validation/import.ts +124 -16
  88. package/src/validation/maps.ts +118 -4
@@ -6,21 +6,29 @@ import type {
6
6
  MaybePromise,
7
7
  Reference
8
8
  } from 'langium';
9
- import { CstUtils, isReference } from 'langium';
9
+ import { AstUtils, CstUtils, isReference } from 'langium';
10
10
  import type { LangiumServices } from 'langium/lsp';
11
11
  import { AstNodeHoverProvider } from 'langium/lsp';
12
12
  import type { Hover, HoverParams } from 'vscode-languageserver';
13
13
  import * as ast from '../../generated/ast.js';
14
14
  import type { DomainLangServices } from '../../domain-lang-module.js';
15
15
  import { QualifiedNameProvider } from '../domain-lang-naming.js';
16
+ import type { DomainLangIndexManager } from '../domain-lang-index-manager.js';
16
17
  import { keywordExplanations } from './domain-lang-keywords.js';
17
- import { effectiveClassification, effectiveTeam } from '../../sdk/resolution.js';
18
+ import {
19
+ buildDomainFields,
20
+ buildBcFields,
21
+ formatHoverContent,
22
+ } from './hover-builders.js';
18
23
 
19
24
  /**
20
25
  * Type-specific hover content generator.
21
26
  * Returns undefined if the generator doesn't handle this node type.
27
+ * @param node - The AST node to generate hover content for
28
+ * @param commentBlock - Documentation comment block
29
+ * @param importAlias - Optional import alias if the node is from an imported document
22
30
  */
23
- type HoverContentGenerator = (node: AstNode, commentBlock: string) => string | undefined;
31
+ type HoverContentGenerator = (node: AstNode, commentBlock: string, importAlias?: string) => string | undefined;
24
32
 
25
33
  /**
26
34
  * Provides hover information for DomainLang elements.
@@ -32,6 +40,7 @@ export class DomainLangHoverProvider extends AstNodeHoverProvider {
32
40
  protected readonly documentationProvider: DocumentationProvider;
33
41
  protected readonly commentProvider: CommentProvider;
34
42
  protected readonly qualifiedNameProvider: QualifiedNameProvider;
43
+ protected readonly indexManager: DomainLangIndexManager;
35
44
 
36
45
  /**
37
46
  * Registry of type-specific hover content generators.
@@ -45,24 +54,25 @@ export class DomainLangHoverProvider extends AstNodeHoverProvider {
45
54
  this.commentProvider = services.documentation.CommentProvider;
46
55
  const domainServices = services as DomainLangServices;
47
56
  this.qualifiedNameProvider = domainServices.references.QualifiedNameProvider;
57
+ this.indexManager = services.shared.workspace.IndexManager as DomainLangIndexManager;
48
58
 
49
59
  // Register type-specific generators
50
60
  this.hoverGenerators = [
51
- (node, comment) => this.getDomainHover(node, comment),
52
- (node, comment) => this.getThisRefHover(node, comment),
53
- (node, comment) => this.getBoundedContextHover(node, comment),
54
- (node, comment) => this.getNamespaceHover(node, comment),
55
- (node, comment) => this.getContextMapHover(node, comment),
56
- (node, comment) => this.getDomainMapHover(node, comment),
57
- (node, comment) => this.getDecisionHover(node, comment),
58
- (node, comment) => this.getPolicyHover(node, comment),
59
- (node, comment) => this.getBusinessRuleHover(node, comment),
60
- (node, comment) => this.getDomainTermHover(node, comment),
61
- (node, comment) => this.getTeamHover(node, comment),
62
- (node, comment) => this.getClassificationHover(node, comment),
63
- (node, comment) => this.getMetadataHover(node, comment),
64
- (node, comment) => this.getRelationshipHover(node, comment),
65
- (node, comment) => this.getImportHover(node, comment),
61
+ (node, comment, alias) => this.getDomainHover(node, comment, alias),
62
+ (node, comment, alias) => this.getThisRefHover(node, comment, alias),
63
+ (node, comment, alias) => this.getBoundedContextHover(node, comment, alias),
64
+ (node, comment, alias) => this.getNamespaceHover(node, comment, alias),
65
+ (node, comment, alias) => this.getContextMapHover(node, comment, alias),
66
+ (node, comment, alias) => this.getDomainMapHover(node, comment, alias),
67
+ (node, comment, alias) => this.getDecisionHover(node, comment, alias),
68
+ (node, comment, alias) => this.getPolicyHover(node, comment, alias),
69
+ (node, comment, alias) => this.getBusinessRuleHover(node, comment, alias),
70
+ (node, comment, alias) => this.getDomainTermHover(node, comment, alias),
71
+ (node, comment, alias) => this.getTeamHover(node, comment, alias),
72
+ (node, comment, alias) => this.getClassificationHover(node, comment, alias),
73
+ (node, comment, alias) => this.getMetadataHover(node, comment, alias),
74
+ (node, comment, alias) => this.getRelationshipHover(node, comment, alias),
75
+ (node, comment, alias) => this.getImportHover(node, comment, alias),
66
76
  ];
67
77
  }
68
78
 
@@ -80,13 +90,13 @@ export class DomainLangHoverProvider extends AstNodeHoverProvider {
80
90
  }
81
91
 
82
92
  // Try declaration hover first
83
- const declarationHover = await this.tryGetDeclarationHover(cstNode);
93
+ const declarationHover = await this.tryGetDeclarationHover(cstNode, document);
84
94
  if (declarationHover) {
85
95
  return declarationHover;
86
96
  }
87
97
 
88
98
  // Then try keyword hover
89
- return await this.tryGetKeywordHover(cstNode);
99
+ return this.tryGetKeywordHover(cstNode);
90
100
  } catch (error) {
91
101
  console.error('Error in getHoverContent:', error);
92
102
  return undefined;
@@ -95,21 +105,46 @@ export class DomainLangHoverProvider extends AstNodeHoverProvider {
95
105
 
96
106
  /**
97
107
  * Try to get hover for a declaration node (AST node).
108
+ * For qualified name references, only shows hover when hovering over the last segment.
109
+ * For qualified name declarations (like namespace names), shows hover on any segment.
98
110
  */
99
- private async tryGetDeclarationHover(cstNode: ReturnType<typeof CstUtils.findDeclarationNodeAtOffset>): Promise<Hover | undefined> {
111
+ private async tryGetDeclarationHover(
112
+ cstNode: ReturnType<typeof CstUtils.findDeclarationNodeAtOffset>,
113
+ document: LangiumDocument
114
+ ): Promise<Hover | undefined> {
100
115
  if (!cstNode) return undefined;
101
116
 
117
+ // For qualified name REFERENCES (e.g., "Core.Baunwalls.Jannie" in a BC classification),
118
+ // only show hover on the last segment. But for qualified name DECLARATIONS (like namespace names),
119
+ // show hover on any segment.
120
+ const isNamespaceDeclaration = ast.isNamespaceDeclaration(cstNode.astNode);
121
+
122
+ if (!isNamespaceDeclaration) {
123
+ // This is a reference context - check if there's a dot immediately after this node
124
+ const fullText = document.textDocument.getText();
125
+ const nodeEndOffset = cstNode.offset + cstNode.length;
126
+
127
+ // If the next non-whitespace character is a dot, this is not the last segment
128
+ if (nodeEndOffset < fullText.length) {
129
+ const nextChar = fullText.charAt(nodeEndOffset);
130
+ if (nextChar === '.') {
131
+ // This node is followed by a dot, so it's not the last segment
132
+ return undefined;
133
+ }
134
+ }
135
+ }
136
+
102
137
  const targetNodes = this.references.findDeclarations(cstNode);
103
138
  const targetNode = targetNodes?.[0];
104
139
  if (targetNode) {
105
- const content = await this.getAstNodeHoverContent(targetNode);
140
+ const content = await this.getAstNodeHoverContent(targetNode, document);
106
141
  if (content) {
107
142
  return { contents: { kind: 'markdown', value: content } };
108
143
  }
109
144
  }
110
145
 
111
146
  if (cstNode.astNode && ast.isThisRef(cstNode.astNode)) {
112
- const content = await this.getAstNodeHoverContent(cstNode.astNode);
147
+ const content = await this.getAstNodeHoverContent(cstNode.astNode, document);
113
148
  if (content) {
114
149
  return { contents: { kind: 'markdown', value: content } };
115
150
  }
@@ -122,7 +157,7 @@ export class DomainLangHoverProvider extends AstNodeHoverProvider {
122
157
  * Try to get hover for a keyword node.
123
158
  * Uses the keyword dictionary for all keywords.
124
159
  */
125
- private async tryGetKeywordHover(cstNode: ReturnType<typeof CstUtils.findDeclarationNodeAtOffset>): Promise<Hover | undefined> {
160
+ private tryGetKeywordHover(cstNode: ReturnType<typeof CstUtils.findDeclarationNodeAtOffset>): Hover | undefined {
126
161
  if (!cstNode || cstNode.grammarSource?.$type !== 'Keyword') {
127
162
  return undefined;
128
163
  }
@@ -136,54 +171,76 @@ export class DomainLangHoverProvider extends AstNodeHoverProvider {
136
171
  return undefined;
137
172
  }
138
173
 
139
- protected getAstNodeHoverContent(node: AstNode): MaybePromise<string | undefined> {
174
+ protected getAstNodeHoverContent(node: AstNode, currentDocument?: LangiumDocument): MaybePromise<string | undefined> {
140
175
  try {
141
176
  const content = this.documentationProvider.getDocumentation(node);
142
177
  const commentBlock = content ? `*${content}*\n\n` : '';
143
178
 
179
+ // Get import alias if the node is from an imported document
180
+ const importAlias = currentDocument ? this.getImportAliasForNode(node, currentDocument) : undefined;
181
+
144
182
  // Try each type-specific generator
145
183
  for (const generator of this.hoverGenerators) {
146
- const result = generator(node, commentBlock);
184
+ const result = generator(node, commentBlock, importAlias);
147
185
  if (result !== undefined) {
148
186
  return result;
149
187
  }
150
188
  }
151
189
 
152
190
  // Default fallback for unknown types
153
- return this.getDefaultHover(node, commentBlock);
191
+ return this.getDefaultHover(node, commentBlock, importAlias);
154
192
  } catch (error) {
155
193
  console.error('Error in getAstNodeHoverContent:', error);
156
194
  return undefined;
157
195
  }
158
196
  }
159
197
 
198
+ /**
199
+ * Gets the import alias for a node if it's from an imported document.
200
+ * Returns undefined if the node is in the same document or not imported with an alias.
201
+ */
202
+ private getImportAliasForNode(targetNode: AstNode, currentDocument: LangiumDocument): string | undefined {
203
+ try {
204
+ const targetDoc = AstUtils.getDocument(targetNode);
205
+ const currentDocUri = currentDocument.uri.toString();
206
+ const targetDocUri = targetDoc.uri.toString();
207
+
208
+ // If same document, no alias needed
209
+ if (currentDocUri === targetDocUri) {
210
+ return undefined;
211
+ }
212
+
213
+ // Look up import info for the current document
214
+ const importInfo = this.indexManager.getImportInfo(currentDocUri);
215
+
216
+ // Find the import that brings in the target document
217
+ for (const imp of importInfo) {
218
+ if (imp.resolvedUri === targetDocUri && imp.alias) {
219
+ return imp.alias;
220
+ }
221
+ }
222
+
223
+ return undefined;
224
+ } catch (error) {
225
+ console.error('Error getting import alias for node:', error);
226
+ return undefined;
227
+ }
228
+ }
229
+
160
230
  // ============================================================
161
231
  // Type-specific hover generators
162
232
  // ============================================================
163
233
 
164
- private getDomainHover(node: AstNode, commentBlock: string): string | undefined {
234
+ private getDomainHover(node: AstNode, commentBlock: string, importAlias?: string): string | undefined {
165
235
  if (!ast.isDomain(node)) return undefined;
166
236
 
167
- const description = node.description ?? '';
168
- const vision = node.vision ?? '';
169
- const typeRef = node.type?.ref;
170
- const type = this.getRefName(typeRef);
171
-
172
- const signatureParts = ['Domain', node.name];
173
- if (node.parent?.ref?.name) signatureParts.push('in', node.parent.ref.name);
174
- const signature = this.codeBlock(signatureParts.join(' '));
175
-
176
- const fields: string[] = [signature];
177
- if (description) fields.push(description);
178
- if (vision || type || node.parent) fields.push('---');
179
- if (vision) fields.push(`**Vision:** ${vision}`);
180
- if (type) fields.push(`**Type:** ${this.refLink(typeRef, type)}`);
181
- if (node.parent) fields.push(`**Parent:** ${this.refLink(node.parent)}`);
182
-
183
- return this.formatHover(commentBlock, '📁', 'domain', node.name, fields);
237
+ const displayName = this.getDisplayNameForHover(node, importAlias);
238
+ const refLink = (ref: ast.Type | undefined, label?: string): string => this.refLink(ref, label);
239
+ const fields = buildDomainFields(node, refLink);
240
+ return formatHoverContent(commentBlock, '📁', 'domain', displayName, fields);
184
241
  }
185
242
 
186
- private getThisRefHover(node: AstNode, _commentBlock: string): string | undefined {
243
+ private getThisRefHover(node: AstNode, _commentBlock: string, _importAlias?: string): string | undefined {
187
244
  if (!ast.isThisRef(node)) return undefined;
188
245
 
189
246
  let parent = node.$container;
@@ -196,7 +253,17 @@ export class DomainLangHoverProvider extends AstNodeHoverProvider {
196
253
  ast.isDomainMap(parent) ||
197
254
  ast.isModel(parent)
198
255
  ) {
199
- return this.getAstNodeHoverContent(parent) as string | undefined;
256
+ const result = this.getAstNodeHoverContent(parent);
257
+ // getAstNodeHoverContent returns MaybePromise<string | undefined>.
258
+ // All registered hover generators are synchronous, so the result
259
+ // should always be a plain string. Guard defensively in case a
260
+ // future generator becomes async.
261
+ if (typeof result === 'string' || result === undefined) {
262
+ return result;
263
+ }
264
+ // If somehow a Promise is returned, we cannot await in a sync
265
+ // context - fall through to the default message.
266
+ return undefined;
200
267
  }
201
268
  parent = parent.$container;
202
269
  }
@@ -204,58 +271,27 @@ export class DomainLangHoverProvider extends AstNodeHoverProvider {
204
271
  return '*this* refers to the current context';
205
272
  }
206
273
 
207
- private getBoundedContextHover(node: AstNode, commentBlock: string): string | undefined {
274
+ private getBoundedContextHover(node: AstNode, commentBlock: string, importAlias?: string): string | undefined {
208
275
  if (!ast.isBoundedContext(node)) return undefined;
209
276
 
210
- const description = node.description ?? '';
211
- const classification = effectiveClassification(node);
212
- const team = effectiveTeam(node);
213
- const businessModel = node.businessModel?.ref;
214
- const evolution = node.evolution?.ref;
215
- const relationships = node.relationships ?? [];
216
- const terminology = node.terminology ?? [];
217
- const decisions = node.decisions ?? [];
218
- const classificationName = classification?.name;
219
- const teamName = team?.name;
220
-
221
- const signatureParts = ['boundedcontext', node.name];
222
- if (node.domain?.ref?.name) signatureParts.push('for', node.domain.ref.name);
223
- if (classificationName) signatureParts.push('as', classificationName);
224
- if (teamName) signatureParts.push('by', teamName);
225
- const signature = this.codeBlock(signatureParts.join(' '));
226
-
227
- const fields: string[] = [signature];
228
- if (description) fields.push(description);
229
- if (classification || team || businessModel || evolution) fields.push('---');
230
- if (classification) fields.push(`🔖 **Classification:** ${this.refLink(classification)}`);
231
- if (team) fields.push(`👥 **Team:** ${this.refLink(team)}`);
232
- if (businessModel) fields.push(`💼 **Business Model:** ${this.refLink(businessModel)}`);
233
- if (evolution) fields.push(`🔄 **Evolution:** ${this.refLink(evolution)}`);
234
-
235
- if (relationships.length > 0) {
236
- const lines = relationships.map(rel => this.formatRelationshipLine(rel));
237
- fields.push(`**Relationships:**\n${lines.join('\n')}`);
238
- }
239
- if (terminology.length > 0) {
240
- const lines = terminology.map(t => `- \`${t.name}\`: ${t.meaning ?? ''}`);
241
- fields.push(`**Terminology:**\n${lines.join('\n')}`);
242
- }
243
- if (decisions.length > 0) {
244
- const lines = decisions.map(d => `- \`${d.name}\`: ${d.value ?? ''}`);
245
- fields.push(`**Decisions:**\n${lines.join('\n')}`);
246
- }
247
-
248
- return this.formatHover(commentBlock, '📕', 'boundedcontext', node.name, fields);
277
+ const displayName = this.getDisplayNameForHover(node, importAlias);
278
+ const refLink = (ref: ast.Type | undefined, label?: string): string => this.refLink(ref, label);
279
+ const fields = buildBcFields(
280
+ node,
281
+ refLink,
282
+ (rel) => this.formatRelationshipLine(rel)
283
+ );
284
+ return formatHoverContent(commentBlock, '🎯', 'bounded context', displayName, fields);
249
285
  }
250
286
 
251
- private getNamespaceHover(node: AstNode, commentBlock: string): string | undefined {
287
+ private getNamespaceHover(node: AstNode, commentBlock: string, _importAlias?: string): string | undefined {
252
288
  if (!ast.isNamespaceDeclaration(node)) return undefined;
253
289
 
254
290
  const fields: string[] = [`Contains ${node.children.length} elements.`];
255
291
  return this.formatHover(commentBlock, '🧭', 'namespace', node.name, fields);
256
292
  }
257
293
 
258
- private getContextMapHover(node: AstNode, commentBlock: string): string | undefined {
294
+ private getContextMapHover(node: AstNode, commentBlock: string, _importAlias?: string): string | undefined {
259
295
  if (!ast.isContextMap(node)) return undefined;
260
296
 
261
297
  const fields: string[] = [];
@@ -274,7 +310,7 @@ export class DomainLangHoverProvider extends AstNodeHoverProvider {
274
310
  return this.formatHover(commentBlock, '🗺️', 'contextmap', node.name, fields);
275
311
  }
276
312
 
277
- private getDomainMapHover(node: AstNode, commentBlock: string): string | undefined {
313
+ private getDomainMapHover(node: AstNode, commentBlock: string, _importAlias?: string): string | undefined {
278
314
  if (!ast.isDomainMap(node)) return undefined;
279
315
 
280
316
  const fields: string[] = [];
@@ -288,7 +324,7 @@ export class DomainLangHoverProvider extends AstNodeHoverProvider {
288
324
  return this.formatHover(commentBlock, '🗺️', 'domainmap', node.name, fields);
289
325
  }
290
326
 
291
- private getDecisionHover(node: AstNode, commentBlock: string): string | undefined {
327
+ private getDecisionHover(node: AstNode, commentBlock: string, _importAlias?: string): string | undefined {
292
328
  if (!ast.isDecision(node)) return undefined;
293
329
 
294
330
  const fields: string[] = [];
@@ -298,7 +334,7 @@ export class DomainLangHoverProvider extends AstNodeHoverProvider {
298
334
  return this.formatHover(commentBlock, '⚖️', 'decision', node.name, fields);
299
335
  }
300
336
 
301
- private getPolicyHover(node: AstNode, commentBlock: string): string | undefined {
337
+ private getPolicyHover(node: AstNode, commentBlock: string, _importAlias?: string): string | undefined {
302
338
  if (!ast.isPolicy(node)) return undefined;
303
339
 
304
340
  const fields: string[] = [];
@@ -308,7 +344,7 @@ export class DomainLangHoverProvider extends AstNodeHoverProvider {
308
344
  return this.formatHover(commentBlock, '📜', 'policy', node.name, fields);
309
345
  }
310
346
 
311
- private getBusinessRuleHover(node: AstNode, commentBlock: string): string | undefined {
347
+ private getBusinessRuleHover(node: AstNode, commentBlock: string, _importAlias?: string): string | undefined {
312
348
  if (!ast.isBusinessRule(node)) return undefined;
313
349
 
314
350
  const fields: string[] = [];
@@ -318,7 +354,7 @@ export class DomainLangHoverProvider extends AstNodeHoverProvider {
318
354
  return this.formatHover(commentBlock, '⚖️', 'rule', node.name, fields);
319
355
  }
320
356
 
321
- private getDomainTermHover(node: AstNode, commentBlock: string): string | undefined {
357
+ private getDomainTermHover(node: AstNode, commentBlock: string, _importAlias?: string): string | undefined {
322
358
  if (!ast.isDomainTerm(node)) return undefined;
323
359
 
324
360
  const fields: string[] = [];
@@ -328,22 +364,25 @@ export class DomainLangHoverProvider extends AstNodeHoverProvider {
328
364
  return this.formatHover(commentBlock, '🗝️', 'term', node.name, fields);
329
365
  }
330
366
 
331
- private getTeamHover(node: AstNode, commentBlock: string): string | undefined {
367
+ private getTeamHover(node: AstNode, commentBlock: string, importAlias?: string): string | undefined {
332
368
  if (!ast.isTeam(node)) return undefined;
333
- return this.formatHover(commentBlock, '👥', 'team', node.name, []);
369
+ const displayName = this.getDisplayNameForHover(node, importAlias);
370
+ return this.formatHover(commentBlock, '👥', 'team', displayName, []);
334
371
  }
335
372
 
336
- private getClassificationHover(node: AstNode, commentBlock: string): string | undefined {
373
+ private getClassificationHover(node: AstNode, commentBlock: string, importAlias?: string): string | undefined {
337
374
  if (!ast.isClassification(node)) return undefined;
338
- return this.formatHover(commentBlock, '🏷️', 'classification', node.name, []);
375
+ const displayName = this.getDisplayNameForHover(node, importAlias);
376
+ return this.formatHover(commentBlock, '🏷️', 'classification', displayName, []);
339
377
  }
340
378
 
341
- private getMetadataHover(node: AstNode, commentBlock: string): string | undefined {
379
+ private getMetadataHover(node: AstNode, commentBlock: string, importAlias?: string): string | undefined {
342
380
  if (!ast.isMetadata(node)) return undefined;
343
- return this.formatHover(commentBlock, '🔖', 'metadata', node.name, []);
381
+ const displayName = this.getDisplayNameForHover(node, importAlias);
382
+ return this.formatHover(commentBlock, '🔖', 'metadata', displayName, []);
344
383
  }
345
384
 
346
- private getRelationshipHover(node: AstNode, commentBlock: string): string | undefined {
385
+ private getRelationshipHover(node: AstNode, commentBlock: string, _importAlias?: string): string | undefined {
347
386
  if (!ast.isRelationship(node)) return undefined;
348
387
 
349
388
  const leftPatterns = node.leftPatterns.join(', ');
@@ -358,7 +397,7 @@ export class DomainLangHoverProvider extends AstNodeHoverProvider {
358
397
  return this.formatHover(commentBlock, '🔗', 'relationship', undefined, fields);
359
398
  }
360
399
 
361
- private getImportHover(node: AstNode, commentBlock: string): string | undefined {
400
+ private getImportHover(node: AstNode, commentBlock: string, _importAlias?: string): string | undefined {
362
401
  if (!ast.isImportStatement(node)) return undefined;
363
402
 
364
403
  const fields: string[] = [`**URI:** \`${node.uri}\``];
@@ -366,7 +405,7 @@ export class DomainLangHoverProvider extends AstNodeHoverProvider {
366
405
  return this.formatHover(commentBlock, '📦', 'import', undefined, fields);
367
406
  }
368
407
 
369
- private getDefaultHover(node: AstNode, commentBlock: string): string {
408
+ private getDefaultHover(node: AstNode, commentBlock: string, _importAlias?: string): string {
370
409
  const title = ast.isType(node) ? node.name : node.$type.toLowerCase();
371
410
  const typeName = node.$type.toLowerCase();
372
411
  const name = ast.isType(node) ? ` ${title}` : '';
@@ -379,6 +418,21 @@ export class DomainLangHoverProvider extends AstNodeHoverProvider {
379
418
  // Helper methods
380
419
  // ============================================================
381
420
 
421
+ /**
422
+ * Computes the display name for a node in hover, considering import aliases.
423
+ * If node is imported with an alias, shows `alias.name`, otherwise shows full qualified name.
424
+ */
425
+ private getDisplayNameForHover(node: ast.Type, importAlias?: string): string {
426
+ if (importAlias) {
427
+ return `${importAlias}.${node.name}`;
428
+ }
429
+ try {
430
+ return this.qualifiedNameProvider.getQualifiedName(node.$container, node.name);
431
+ } catch {
432
+ return node.name;
433
+ }
434
+ }
435
+
382
436
  /**
383
437
  * Formats a relationship line for hover display.
384
438
  */
@@ -389,15 +443,9 @@ export class DomainLangHoverProvider extends AstNodeHoverProvider {
389
443
  return `- ${left} ${rel.arrow} ${right}${type}`;
390
444
  }
391
445
 
392
- /**
393
- * Wraps text in a domain-lang code block.
394
- */
395
- private codeBlock(text: string): string {
396
- return `\`\`\`domain-lang\n${text}\n\`\`\``;
397
- }
398
-
399
446
  /**
400
447
  * Formats the final hover content with consistent structure.
448
+ * Delegates to the shared hover-builders utility.
401
449
  */
402
450
  private formatHover(
403
451
  commentBlock: string,
@@ -406,38 +454,48 @@ export class DomainLangHoverProvider extends AstNodeHoverProvider {
406
454
  name: string | undefined,
407
455
  fields: string[]
408
456
  ): string {
409
- const separator = commentBlock ? `${commentBlock}\n\n---\n\n` : '';
410
- const nameDisplay = name ? ` ${name}` : '';
411
- const header = `${emoji} **\`(${typeName})\`${nameDisplay}**`;
412
- const body = fields.length > 0 ? `\n\n${fields.join('\n\n')}` : '';
413
- return `${separator}${header}${body}`;
457
+ return formatHoverContent(commentBlock, emoji, typeName, name, fields);
414
458
  }
415
459
 
416
- private getRefName(ref: ast.Type | Reference<ast.Type> | undefined): string {
460
+ private refLink(ref: Reference<ast.Type> | ast.Type | undefined, label?: string): string {
417
461
  const node = isReference(ref) ? ref.ref : ref;
418
- if (node && ast.isType(node)) {
419
- return node.name;
420
- }
421
- return '';
422
- }
423
462
 
424
- private refLink(ref: Reference<ast.Type> | ast.Type | undefined, label?: string): string {
425
- if (label) {
426
- return `[${label}](#${encodeURIComponent(label)})`;
463
+ if (!node || !ast.isType(node)) {
464
+ return label ? `\`${label}\`` : '';
427
465
  }
428
466
 
429
- const node = isReference(ref) ? ref.ref : ref;
430
-
431
- if (node && ast.isType(node)) {
432
- let linkLabel = node.name;
467
+ // Get display name (use label if provided, otherwise qualified name)
468
+ let linkLabel: string;
469
+ if (label) {
470
+ linkLabel = label;
471
+ } else {
433
472
  try {
434
473
  linkLabel = this.qualifiedNameProvider.getQualifiedName(node.$container, node.name);
435
474
  } catch {
436
- // fallback to name
475
+ linkLabel = node.name;
437
476
  }
438
- return `[${linkLabel}](#${encodeURIComponent(linkLabel)})`;
439
477
  }
440
- return '';
478
+
479
+ // Try to create clickable go-to-definition link
480
+ try {
481
+ const doc = AstUtils.getDocument(node);
482
+ const cstNode = node.$cstNode;
483
+
484
+ if (doc && cstNode) {
485
+ const range = CstUtils.toDocumentSegment(cstNode).range;
486
+ const line = range.start.line + 1; // LSP lines are 0-indexed, file links use 1-indexed
487
+ const col = range.start.character + 1;
488
+ const uri = doc.uri.toString();
489
+
490
+ // VS Code recognizes file:// URIs with #Lline,col for go-to-definition
491
+ return `[${linkLabel}](${uri}#L${line},${col})`;
492
+ }
493
+ } catch (error) {
494
+ console.error('Error creating hover link:', error);
495
+ }
496
+
497
+ // Fallback to plain text if we can't create a link
498
+ return `\`${linkLabel}\``;
441
499
  }
442
500
 
443
501
  }