@domainlang/language 0.11.0 → 0.12.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 (31) hide show
  1. package/out/diagram/context-map-diagram-generator.d.ts +65 -0
  2. package/out/diagram/context-map-diagram-generator.js +356 -0
  3. package/out/diagram/context-map-diagram-generator.js.map +1 -0
  4. package/out/diagram/context-map-layout-configurator.d.ts +15 -0
  5. package/out/diagram/context-map-layout-configurator.js +39 -0
  6. package/out/diagram/context-map-layout-configurator.js.map +1 -0
  7. package/out/diagram/elk-layout-factory.d.ts +43 -0
  8. package/out/diagram/elk-layout-factory.js +64 -0
  9. package/out/diagram/elk-layout-factory.js.map +1 -0
  10. package/out/domain-lang-module.d.ts +7 -0
  11. package/out/domain-lang-module.js +11 -2
  12. package/out/domain-lang-module.js.map +1 -1
  13. package/out/index.d.ts +3 -0
  14. package/out/index.js +4 -0
  15. package/out/index.js.map +1 -1
  16. package/out/lsp/domain-lang-code-lens-provider.d.ts +8 -0
  17. package/out/lsp/domain-lang-code-lens-provider.js +48 -0
  18. package/out/lsp/domain-lang-code-lens-provider.js.map +1 -0
  19. package/out/lsp/domain-lang-document-symbol-provider.js +5 -5
  20. package/out/lsp/domain-lang-document-symbol-provider.js.map +1 -1
  21. package/out/lsp/tool-handlers.js +63 -57
  22. package/out/lsp/tool-handlers.js.map +1 -1
  23. package/package.json +5 -2
  24. package/src/diagram/context-map-diagram-generator.ts +451 -0
  25. package/src/diagram/context-map-layout-configurator.ts +43 -0
  26. package/src/diagram/elk-layout-factory.ts +83 -0
  27. package/src/domain-lang-module.ts +19 -2
  28. package/src/index.ts +5 -0
  29. package/src/lsp/domain-lang-code-lens-provider.ts +54 -0
  30. package/src/lsp/domain-lang-document-symbol-provider.ts +5 -5
  31. package/src/lsp/tool-handlers.ts +61 -47
@@ -0,0 +1,451 @@
1
+ import type { LangiumDocument } from 'langium';
2
+ import type { SGraph, SModelElement, SModelRoot, SEdge, SNode } from 'sprotty-protocol';
3
+ import { LangiumDiagramGenerator, type GeneratorContext } from 'langium-sprotty';
4
+ import { fromDocument } from '../sdk/query.js';
5
+ import type { Query } from '../sdk/types.js';
6
+ import type { BoundedContext, ContextMap, Model, Relationship } from '../generated/ast.js';
7
+
8
+ /** Ellipse sizing for bounded context nodes — sized for long names like "CustomerManagementContext" */
9
+ const NODE_WIDTH = 280;
10
+ const NODE_HEIGHT = 100;
11
+
12
+ /**
13
+ * Maps long-form DomainLang integration pattern keywords to their standard
14
+ * DDD abbreviations for display in U/D badges.
15
+ */
16
+ const PATTERN_ABBREVIATIONS: Readonly<Record<string, string>> = {
17
+ OpenHostService: 'OHS',
18
+ PublishedLanguage: 'PL',
19
+ AntiCorruptionLayer: 'ACL',
20
+ Conformist: 'CF',
21
+ Partnership: 'P',
22
+ SharedKernel: 'SK',
23
+ BigBallOfMud: 'BBoM',
24
+ };
25
+
26
+ /**
27
+ * Returns the abbreviated form of an integration pattern keyword.
28
+ *
29
+ * Short forms (e.g. `OHS`, `ACL`) are returned unchanged. Long forms (e.g.
30
+ * `OpenHostService`, `AntiCorruptionLayer`) are mapped to their abbreviation.
31
+ */
32
+ function normalizePattern(pattern: string): string {
33
+ return PATTERN_ABBREVIATIONS[pattern] ?? pattern;
34
+ }
35
+
36
+ /**
37
+ * Returns `true` when the pattern identifies a Big Ball of Mud participant.
38
+ *
39
+ * BBoM is surfaced as a cloud node shape on the bounded context itself, not as
40
+ * a text annotation in the edge badge, so it should be excluded from badge text.
41
+ */
42
+ function isBBoMPattern(pattern: string): boolean {
43
+ return pattern === 'BBoM' || pattern === 'BigBallOfMud';
44
+ }
45
+
46
+ interface DiagramSelection {
47
+ selectedContextMapFqn?: string;
48
+ selectedContextMapName?: string;
49
+ }
50
+
51
+ interface RelationshipEdgeParams {
52
+ leftNode: SNode;
53
+ rightNode: SNode;
54
+ arrow: string;
55
+ leftPatterns: readonly string[];
56
+ rightPatterns: readonly string[];
57
+ type: string | undefined;
58
+ astNode: Relationship;
59
+ }
60
+
61
+ /**
62
+ * Generates context map diagrams in the **DDD community notation** style.
63
+ *
64
+ * Bounded contexts are rendered as ellipses. Relationships are rendered as edges
65
+ * with U/D (upstream/downstream) annotations and integration pattern labels at
66
+ * each end, matching the notation used in Eric Evans' "Domain-Driven Design" and
67
+ * Vaughn Vernon's "Implementing Domain-Driven Design".
68
+ *
69
+ * Edge label convention:
70
+ * - Position 0.1 (near source): `U [OHS, PL]` or `D [ACL]`
71
+ * - Position 0.9 (near target): `D [CF]` or `U [OHS]`
72
+ * - Position 0.5 (center): relationship type (e.g., "Customer/Supplier")
73
+ */
74
+ export class DomainLangContextMapDiagramGenerator extends LangiumDiagramGenerator {
75
+ protected override generateRoot(args: GeneratorContext): SModelRoot {
76
+ const document = args.document as LangiumDocument<Model>;
77
+ const query = fromDocument(document);
78
+ const selection = this.getSelection(args);
79
+ const contextMaps = query.contextMaps().toArray();
80
+
81
+ const selectedMap = this.selectContextMap(contextMaps, query, selection);
82
+ if (!selectedMap) {
83
+ return {
84
+ type: 'graph',
85
+ id: args.idCache.uniqueId('context-map:empty'),
86
+ children: [],
87
+ } satisfies SGraph;
88
+ }
89
+
90
+ // Fetch relationships first so the BBoM pre-pass can identify which BCs
91
+ // need the cloud node shape before any nodes are created.
92
+ const relationships = query.relationships()
93
+ .where((relationship) => relationship.source === 'ContextMap' && relationship.astNode.$container === selectedMap)
94
+ .toArray();
95
+
96
+ // BBoM pre-pass: collect the node keys of every bounded context that
97
+ // appears on a side annotated with the BigBallOfMud pattern. These get
98
+ // the `node:bbom` Sprotty type so the webview renders them as clouds.
99
+ const bboMNodeKeys = new Set<string>();
100
+ for (const rel of relationships) {
101
+ if (rel.leftPatterns.some(isBBoMPattern)) {
102
+ bboMNodeKeys.add(this.getNodeKey(query, rel.left));
103
+ }
104
+ if (rel.rightPatterns.some(isBBoMPattern)) {
105
+ bboMNodeKeys.add(this.getNodeKey(query, rel.right));
106
+ }
107
+ }
108
+
109
+ const nodeMap = new Map<string, SNode>();
110
+ this.collectContextMapNodes(selectedMap, query, nodeMap, bboMNodeKeys, args);
111
+
112
+ for (const relationship of relationships) {
113
+ this.ensureNode(nodeMap, query, relationship.left, bboMNodeKeys, args);
114
+ this.ensureNode(nodeMap, query, relationship.right, bboMNodeKeys, args);
115
+ }
116
+
117
+ const edges = relationships.flatMap((relationship) => {
118
+ const leftKey = this.getNodeKey(query, relationship.left);
119
+ const rightKey = this.getNodeKey(query, relationship.right);
120
+ const leftNode = nodeMap.get(leftKey);
121
+ const rightNode = nodeMap.get(rightKey);
122
+
123
+ if (!leftNode || !rightNode) {
124
+ return [];
125
+ }
126
+
127
+ return this.createRelationshipEdge({
128
+ leftNode,
129
+ rightNode,
130
+ arrow: relationship.arrow,
131
+ leftPatterns: relationship.leftPatterns,
132
+ rightPatterns: relationship.rightPatterns,
133
+ type: relationship.type,
134
+ astNode: relationship.astNode,
135
+ }, args);
136
+ });
137
+
138
+ return {
139
+ type: 'graph',
140
+ id: args.idCache.uniqueId(`context-map:${selectedMap.name}`, selectedMap),
141
+ children: [...nodeMap.values(), ...edges],
142
+ } satisfies SGraph;
143
+ }
144
+
145
+ // ── Relationship edges (DDD community notation) ──
146
+
147
+ /**
148
+ * Creates an edge with DDD community notation labels.
149
+ *
150
+ * For `->`: left = Upstream (U), right = Downstream (D)
151
+ * For `<-`: left = Downstream (D), right = Upstream (U)
152
+ * For `<->`: Partnership (bidirectional)
153
+ * For `><`: Separate Ways
154
+ *
155
+ * U/D labels are rendered as DDD notation badges with optional
156
+ * pattern boxes (e.g., `U [OHS, PL]`).
157
+ */
158
+ private createRelationshipEdge(
159
+ params: RelationshipEdgeParams,
160
+ args: GeneratorContext
161
+ ): SEdge[] {
162
+ const { leftNode, rightNode, arrow, leftPatterns, rightPatterns, type, astNode } = params;
163
+ // Determine source/target based on arrow direction
164
+ const sourceId = arrow === '<-' ? rightNode.id : leftNode.id;
165
+ const targetId = arrow === '<-' ? leftNode.id : rightNode.id;
166
+
167
+ const edgeId = args.idCache.uniqueId(
168
+ `edge:${sourceId}:${targetId}`,
169
+ astNode
170
+ );
171
+
172
+ const children: SModelElement[] = [];
173
+
174
+ if (arrow === '<->') {
175
+ // Partnership / bidirectional — no U/D roles
176
+ this.addUDBadge(children, edgeId, 'source', leftPatterns, undefined, args);
177
+ this.addUDBadge(children, edgeId, 'target', rightPatterns, undefined, args);
178
+ } else if (arrow === '><') {
179
+ // Separate Ways — no U/D, no patterns
180
+ } else {
181
+ // Unidirectional: -> or <-
182
+ const sourcePatterns = arrow === '<-' ? rightPatterns : leftPatterns;
183
+ const targetPatterns = arrow === '<-' ? leftPatterns : rightPatterns;
184
+ this.addUDBadge(children, edgeId, 'source', sourcePatterns, 'U', args);
185
+ this.addUDBadge(children, edgeId, 'target', targetPatterns, 'D', args);
186
+ }
187
+
188
+ // Center label: relationship type
189
+ const centerLabel = this.formatRelationshipType(type, arrow);
190
+ if (centerLabel) {
191
+ children.push({
192
+ type: 'label:edge',
193
+ id: args.idCache.uniqueId(`${edgeId}:type`),
194
+ text: centerLabel,
195
+ edgePlacement: {
196
+ side: 'on',
197
+ position: 0.5,
198
+ rotate: false,
199
+ offset: 10,
200
+ },
201
+ } as unknown as SModelElement);
202
+ }
203
+
204
+ let edgeCssClasses: string[] | undefined;
205
+ if (arrow === '><') {
206
+ edgeCssClasses = ['separate-ways'];
207
+ } else if (arrow === '<->') {
208
+ edgeCssClasses = ['partnership'];
209
+ }
210
+
211
+ const edge: SEdge = {
212
+ type: 'edge',
213
+ id: edgeId,
214
+ sourceId,
215
+ targetId,
216
+ cssClasses: edgeCssClasses,
217
+ children: children.length > 0 ? children : undefined,
218
+ };
219
+
220
+ this.traceProvider.trace(edge as SModelElement, astNode as unknown as import('langium').AstNode);
221
+ return [edge];
222
+ }
223
+
224
+ /**
225
+ * Adds a U/D badge label at the source or target end of an edge.
226
+ *
227
+ * Patterns are normalised to their short abbreviations (e.g. `OpenHostService` →
228
+ * `OHS`) and `BBoM`/`BigBallOfMud` is excluded — BBoM is surfaced visually as a
229
+ * cloud node shape rather than a text annotation.
230
+ *
231
+ * Badge text format: `ROLE|PATTERNS` (e.g. `U|OHS, PL` or `D|ACL`).
232
+ * The webview `UDBadgeLabelView` renders this as a bordered box.
233
+ */
234
+ private addUDBadge(
235
+ children: SModelElement[],
236
+ edgeId: string,
237
+ placement: 'source' | 'target',
238
+ patterns: readonly string[],
239
+ role: 'U' | 'D' | undefined,
240
+ args: GeneratorContext
241
+ ): void {
242
+ // Normalise pattern names and strip BBoM (shown on node, not in badge)
243
+ const badgePatterns = patterns
244
+ .filter((p) => !isBBoMPattern(p))
245
+ .map(normalizePattern);
246
+
247
+ if (!role && badgePatterns.length === 0) {
248
+ return;
249
+ }
250
+
251
+ // Encode as ROLE|PATTERNS for UDBadgeLabelView parsing
252
+ const rolePart = role ?? '';
253
+ const patternPart = badgePatterns.length > 0 ? badgePatterns.join(', ') : '';
254
+ const badgeText = `${rolePart}|${patternPart}`;
255
+
256
+ children.push({
257
+ type: 'label:ud-badge',
258
+ id: args.idCache.uniqueId(`${edgeId}:${placement}`),
259
+ text: badgeText,
260
+ edgePlacement: {
261
+ side: 'on',
262
+ position: placement === 'source' ? 0.1 : 0.9,
263
+ rotate: false,
264
+ offset: 0,
265
+ },
266
+ } as unknown as SModelElement);
267
+ }
268
+
269
+ /**
270
+ * Formats the relationship type for the center edge label.
271
+ *
272
+ * Maps DomainLang keywords to DDD community notation display names:
273
+ * CustomerSupplier → Customer/Supplier
274
+ * SharedKernel → Shared Kernel
275
+ * UpstreamDownstream → Upstream/Downstream
276
+ * Partnership → Partnership
277
+ *
278
+ * For `<->` without explicit type, defaults to "Partnership".
279
+ * For `><`, defaults to "Separate Ways".
280
+ */
281
+ private formatRelationshipType(type: string | undefined, arrow: string): string | undefined {
282
+ if (type) {
283
+ return this.displayRelationshipType(type);
284
+ }
285
+ if (arrow === '<->') {
286
+ return 'Partnership';
287
+ }
288
+ if (arrow === '><') {
289
+ return 'Separate Ways';
290
+ }
291
+ return undefined;
292
+ }
293
+
294
+ private displayRelationshipType(type: string): string {
295
+ switch (type) {
296
+ case 'CustomerSupplier': return 'Customer/Supplier';
297
+ case 'SharedKernel': return 'Shared Kernel';
298
+ case 'UpstreamDownstream': return 'Upstream/Downstream';
299
+ default: return type;
300
+ }
301
+ }
302
+
303
+ // ── Node generation ──
304
+
305
+ private collectContextMapNodes(
306
+ selectedMap: ContextMap,
307
+ query: Query,
308
+ nodeMap: Map<string, SNode>,
309
+ bboMNodeKeys: ReadonlySet<string>,
310
+ args: GeneratorContext
311
+ ): void {
312
+ for (const boundedContextRef of selectedMap.boundedContexts) {
313
+ for (const item of boundedContextRef.items) {
314
+ this.ensureNodeForContextMapItem(item, query, nodeMap, bboMNodeKeys, args);
315
+ }
316
+
317
+ if (boundedContextRef.items.length === 0 && boundedContextRef.$refText) {
318
+ this.ensureUnresolvedNode(nodeMap, boundedContextRef.$refText, args);
319
+ }
320
+ }
321
+ }
322
+
323
+ private ensureNodeForContextMapItem(
324
+ item: { ref?: BoundedContext; $refText?: string },
325
+ query: Query,
326
+ nodeMap: Map<string, SNode>,
327
+ bboMNodeKeys: ReadonlySet<string>,
328
+ args: GeneratorContext
329
+ ): void {
330
+ const boundedContext = item.ref;
331
+ if (!boundedContext) {
332
+ if (item.$refText) {
333
+ this.ensureUnresolvedNode(nodeMap, item.$refText, args);
334
+ }
335
+ return;
336
+ }
337
+
338
+ const nodeKey = this.getNodeKey(query, boundedContext);
339
+ if (nodeMap.has(nodeKey)) {
340
+ return;
341
+ }
342
+
343
+ const nodeId = args.idCache.uniqueId(`node:${nodeKey}`, boundedContext);
344
+ const node = this.createNode(nodeId, boundedContext.name, bboMNodeKeys.has(nodeKey));
345
+ this.traceProvider.trace(node as SModelElement, boundedContext);
346
+ nodeMap.set(nodeKey, node);
347
+ }
348
+
349
+ private createNode(id: string, label: string, isBBoM = false): SNode {
350
+ return {
351
+ type: isBBoM ? 'node:bbom' : 'node',
352
+ id,
353
+ position: { x: 0, y: 0 },
354
+ size: {
355
+ width: NODE_WIDTH,
356
+ height: NODE_HEIGHT,
357
+ },
358
+ layout: 'vbox',
359
+ layoutOptions: {
360
+ hAlign: 'center',
361
+ vAlign: 'center',
362
+ resizeContainer: false,
363
+ paddingTop: 10,
364
+ paddingBottom: 10,
365
+ paddingLeft: 20,
366
+ paddingRight: 20,
367
+ },
368
+ children: [this.createNodeLabel(id, label)],
369
+ };
370
+ }
371
+
372
+ private createNodeLabel(nodeId: string, label: string): SModelElement {
373
+ return {
374
+ type: 'label',
375
+ id: `${nodeId}:label`,
376
+ text: label,
377
+ } as unknown as SModelElement;
378
+ }
379
+
380
+ private ensureNode(
381
+ nodeMap: Map<string, SNode>,
382
+ query: Query,
383
+ boundedContext: BoundedContext,
384
+ bboMNodeKeys: ReadonlySet<string>,
385
+ args: GeneratorContext
386
+ ): void {
387
+ const nodeKey = this.getNodeKey(query, boundedContext);
388
+ if (nodeMap.has(nodeKey)) {
389
+ return;
390
+ }
391
+
392
+ const nodeId = args.idCache.uniqueId(`node:${nodeKey}`, boundedContext);
393
+ const node = this.createNode(nodeId, boundedContext.name, bboMNodeKeys.has(nodeKey));
394
+ this.traceProvider.trace(node as SModelElement, boundedContext);
395
+ nodeMap.set(nodeKey, node);
396
+ }
397
+
398
+ private ensureUnresolvedNode(nodeMap: Map<string, SNode>, label: string, args: GeneratorContext): void {
399
+ const key = `unresolved:${label}`;
400
+ if (nodeMap.has(key)) {
401
+ return;
402
+ }
403
+
404
+ const nodeId = args.idCache.uniqueId(`node:${key}`);
405
+ nodeMap.set(key, this.createNode(nodeId, label));
406
+ }
407
+
408
+ private getNodeKey(query: Query, boundedContext: BoundedContext): string {
409
+ const fqn = query.fqn(boundedContext);
410
+ return fqn ?? boundedContext.name;
411
+ }
412
+
413
+ // ── Selection helpers ──
414
+
415
+ private getSelection(args: GeneratorContext): DiagramSelection {
416
+ const options = args.options;
417
+ if (typeof options !== 'object' || options === null) {
418
+ return {};
419
+ }
420
+
421
+ const selectedContextMapFqn = this.getStringOption(options, 'selectedContextMapFqn');
422
+ const selectedContextMapName = this.getStringOption(options, 'selectedContextMapName');
423
+ return {
424
+ selectedContextMapFqn,
425
+ selectedContextMapName,
426
+ };
427
+ }
428
+
429
+ private getStringOption(options: object, key: string): string | undefined {
430
+ const value = Reflect.get(options, key);
431
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
432
+ }
433
+
434
+ private selectContextMap(contextMaps: readonly ContextMap[], query: Query, selection: DiagramSelection): ContextMap | undefined {
435
+ if (selection.selectedContextMapFqn) {
436
+ const byFqn = contextMaps.find((contextMap) => query.fqn(contextMap) === selection.selectedContextMapFqn);
437
+ if (byFqn) {
438
+ return byFqn;
439
+ }
440
+ }
441
+
442
+ if (selection.selectedContextMapName) {
443
+ const byName = contextMaps.find((contextMap) => contextMap.name === selection.selectedContextMapName);
444
+ if (byName) {
445
+ return byName;
446
+ }
447
+ }
448
+
449
+ return contextMaps[0];
450
+ }
451
+ }
@@ -0,0 +1,43 @@
1
+ import type { LayoutOptions } from 'elkjs/lib/elk-api.js';
2
+ import { DefaultLayoutConfigurator } from 'sprotty-elk/lib/elk-layout.js';
3
+ import type { SGraph, SNode, SModelIndex, SEdge } from 'sprotty-protocol';
4
+
5
+ /**
6
+ * ELK layout configurator for DomainLang context map diagrams.
7
+ *
8
+ * Uses the `layered` algorithm with **DOWN** direction (top-to-bottom) so
9
+ * upstream contexts appear above downstream contexts, matching the conventional
10
+ * DDD Context Map layout direction.
11
+ */
12
+ export class ContextMapLayoutConfigurator extends DefaultLayoutConfigurator {
13
+ protected override graphOptions(_graph: SGraph, _index: SModelIndex): LayoutOptions {
14
+ return {
15
+ 'elk.algorithm': 'layered',
16
+ 'elk.direction': 'DOWN',
17
+ 'elk.layered.nodePlacement.strategy': 'BRANDES_KOEPF',
18
+ 'elk.spacing.nodeNode': '100',
19
+ 'elk.layered.spacing.nodeNodeBetweenLayers': '140',
20
+ 'elk.spacing.edgeNode': '60',
21
+ 'elk.spacing.edgeEdge': '40',
22
+ // Edge routing mode is irrelevant — the webview SmoothBezierEdgeView
23
+ // ignores ELK routing points and computes dynamic bezier curves
24
+ // anchored directly to node ellipses with obstacle avoidance.
25
+ 'elk.edgeRouting': 'POLYLINE',
26
+ 'elk.layered.mergeEdges': 'false',
27
+ 'elk.layered.crossingMinimization.strategy': 'LAYER_SWEEP',
28
+ 'elk.layered.thoroughness': '7',
29
+ };
30
+ }
31
+
32
+ protected override nodeOptions(_node: SNode, _index: SModelIndex): LayoutOptions {
33
+ return {
34
+ 'elk.layered.nodePlacement.bk.fixedAlignment': 'BALANCED',
35
+ };
36
+ }
37
+
38
+ protected override edgeOptions(_edge: SEdge, _index: SModelIndex): LayoutOptions {
39
+ return {
40
+ 'elk.layered.priority.direction': '1',
41
+ };
42
+ }
43
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * ELK layout engine factory for DomainLang diagrams.
3
+ *
4
+ * Encapsulates the ELK construction, factory wiring, and Sprotty-ELK layout
5
+ * engine instantiation so that both the VS Code extension (via the LSP server
6
+ * DI module) and the CLI (for standalone diagram export) can reuse the same
7
+ * layout logic without embedding the setup inline.
8
+ *
9
+ * @module diagram/elk-layout-factory
10
+ */
11
+ import * as ElkBundled from 'elkjs/lib/elk.bundled.js';
12
+ import type { ELK } from 'elkjs/lib/elk-api.js';
13
+ import {
14
+ DefaultElementFilter,
15
+ ElkLayoutEngine,
16
+ type ElkFactory,
17
+ type IElementFilter,
18
+ type ILayoutConfigurator,
19
+ } from 'sprotty-elk/lib/elk-layout.js';
20
+ import type { IModelLayoutEngine } from 'sprotty-protocol';
21
+ import { ContextMapLayoutConfigurator } from './context-map-layout-configurator.js';
22
+
23
+ /** @internal Handles the dual ESM/CJS export shape of the bundled ELK module. */
24
+ type ElkConstructor = new (args?: { algorithms?: string[] }) => ELK;
25
+ const _elkConstructor: ElkConstructor = (
26
+ (ElkBundled as unknown as { default?: ElkConstructor }).default
27
+ ?? (ElkBundled as unknown as ElkConstructor)
28
+ );
29
+
30
+ /**
31
+ * Creates an {@link ElkFactory} that produces a new bundled ELK instance
32
+ * pre-configured for the `layered` algorithm.
33
+ *
34
+ * Use this in Langium's DI module (`layout.ElkFactory`) or directly in the
35
+ * CLI when running the layout engine standalone.
36
+ */
37
+ export function createElkFactory(): ElkFactory {
38
+ return () => new _elkConstructor({ algorithms: ['layered'] });
39
+ }
40
+
41
+ /**
42
+ * Creates the full set of objects required to run the ELK layout engine for
43
+ * DomainLang context-map diagrams.
44
+ *
45
+ * Returns the three collaborators that together constitute a complete
46
+ * `IModelLayoutEngine`:
47
+ * - `elkFactory` — produces the ELK worker instance
48
+ * - `elementFilter` — controls which model elements are laid out
49
+ * - `layoutConfigurator` — supplies algorithm-specific ELK options
50
+ *
51
+ * Callers that use Langium DI should register these individually via the
52
+ * `layout` service group. Callers that operate outside of DI (e.g. the CLI)
53
+ * can call {@link createElkLayoutEngine} for a fully assembled engine.
54
+ */
55
+ export function createElkLayoutComponents(): {
56
+ elkFactory: ElkFactory;
57
+ elementFilter: IElementFilter;
58
+ layoutConfigurator: ILayoutConfigurator;
59
+ } {
60
+ return {
61
+ elkFactory: createElkFactory(),
62
+ elementFilter: new DefaultElementFilter(),
63
+ layoutConfigurator: new ContextMapLayoutConfigurator(),
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Creates a fully assembled {@link IModelLayoutEngine} for DomainLang
69
+ * context-map diagrams using the bundled ELK engine.
70
+ *
71
+ * Intended for use outside of Langium's DI container — for example, in the
72
+ * CLI when generating standalone SVG or image exports.
73
+ *
74
+ * @example
75
+ * ```typescript
76
+ * const layoutEngine = createElkLayoutEngine();
77
+ * const laidOutModel = await layoutEngine.layout(rootModel);
78
+ * ```
79
+ */
80
+ export function createElkLayoutEngine(): IModelLayoutEngine {
81
+ const { elkFactory, elementFilter, layoutConfigurator } = createElkLayoutComponents();
82
+ return new ElkLayoutEngine(elkFactory, elementFilter, layoutConfigurator) as IModelLayoutEngine;
83
+ }
@@ -16,13 +16,18 @@ import { DomainLangFormatter } from './lsp/domain-lang-formatter.js';
16
16
  import { DomainLangHoverProvider } from './lsp/hover/domain-lang-hover.js';
17
17
  import { DomainLangCompletionProvider } from './lsp/domain-lang-completion.js';
18
18
  import { DomainLangCodeActionProvider } from './lsp/domain-lang-code-actions.js';
19
+ import { DomainLangCodeLensProvider } from './lsp/domain-lang-code-lens-provider.js';
19
20
  import { DomainLangNodeKindProvider } from './lsp/domain-lang-node-kind-provider.js';
20
21
  import { DomainLangDocumentSymbolProvider } from './lsp/domain-lang-document-symbol-provider.js';
22
+ import { SprottyDefaultModule, SprottySharedModule } from 'langium-sprotty';
23
+ import type { IDiagramGenerator, IModelLayoutEngine } from 'sprotty-protocol';
21
24
  import { ImportResolver } from './services/import-resolver.js';
22
25
  import { ManifestManager } from './services/workspace-manager.js';
23
26
  import { PackageBoundaryDetector } from './services/package-boundary-detector.js';
24
27
  import { DomainLangWorkspaceManager } from './lsp/domain-lang-workspace-manager.js';
25
28
  import { DomainLangIndexManager } from './lsp/domain-lang-index-manager.js';
29
+ import { DomainLangContextMapDiagramGenerator } from './diagram/context-map-diagram-generator.js';
30
+ import { createElkLayoutEngine } from './diagram/elk-layout-factory.js';
26
31
 
27
32
  /**
28
33
  * Declaration of custom services - add your own service classes here.
@@ -40,8 +45,13 @@ export type DomainLangAddedServices = {
40
45
  Formatter: DomainLangFormatter,
41
46
  HoverProvider: DomainLangHoverProvider,
42
47
  CompletionProvider: DomainLangCompletionProvider,
48
+ CodeLensProvider: DomainLangCodeLensProvider,
43
49
  CodeActionProvider: DomainLangCodeActionProvider
44
- }
50
+ },
51
+ diagram: {
52
+ DiagramGenerator: IDiagramGenerator,
53
+ ModelLayoutEngine: IModelLayoutEngine
54
+ },
45
55
  }
46
56
 
47
57
  /**
@@ -80,9 +90,14 @@ export const DomainLangModule: Module<DomainLangServices, PartialLangiumServices
80
90
  Formatter: () => new DomainLangFormatter(),
81
91
  HoverProvider: (services) => new DomainLangHoverProvider(services),
82
92
  CompletionProvider: (services) => new DomainLangCompletionProvider(services),
93
+ CodeLensProvider: () => new DomainLangCodeLensProvider(),
83
94
  CodeActionProvider: () => new DomainLangCodeActionProvider(),
84
95
  DocumentSymbolProvider: (services) => new DomainLangDocumentSymbolProvider(services)
85
96
  },
97
+ diagram: {
98
+ DiagramGenerator: (services) => new DomainLangContextMapDiagramGenerator(services as never),
99
+ ModelLayoutEngine: () => createElkLayoutEngine(),
100
+ },
86
101
  };
87
102
 
88
103
  /**
@@ -107,11 +122,13 @@ export function createDomainLangServices(context: DefaultSharedModuleContext): {
107
122
  const shared = inject(
108
123
  createDefaultSharedModule(context),
109
124
  DomainLangGeneratedSharedModule,
110
- DomainLangSharedModule
125
+ DomainLangSharedModule,
126
+ SprottySharedModule as never
111
127
  );
112
128
  const DomainLang = inject(
113
129
  createDefaultModule({ shared }),
114
130
  DomainLangGeneratedModule,
131
+ SprottyDefaultModule as never,
115
132
  DomainLangModule
116
133
  );
117
134
  shared.ServiceRegistry.register(DomainLang);
package/src/index.ts CHANGED
@@ -32,3 +32,8 @@ export * from './lsp/manifest-diagnostics.js';
32
32
  export { DomainLangIndexManager } from './lsp/domain-lang-index-manager.js';
33
33
  export { registerDomainLangRefresh, processWatchedFileChanges } from './lsp/domain-lang-refresh.js';
34
34
  export { registerToolHandlers } from './lsp/tool-handlers.js';
35
+
36
+ // Export diagram services (reusable by CLI and extension)
37
+ export { DomainLangContextMapDiagramGenerator } from './diagram/context-map-diagram-generator.js';
38
+ export { ContextMapLayoutConfigurator } from './diagram/context-map-layout-configurator.js';
39
+ export { createElkLayoutEngine, createElkFactory } from './diagram/elk-layout-factory.js';