@cratis/screenplay-language 4.52.0 → 4.54.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 (191) hide show
  1. package/README.md +15 -0
  2. package/dist/TypeReferenceSymbol.d.ts +8 -0
  3. package/dist/TypeReferenceSymbol.d.ts.map +1 -0
  4. package/dist/bundles/chunk-GJX4NJYC.js +4836 -0
  5. package/dist/bundles/chunk-GJX4NJYC.js.map +7 -0
  6. package/dist/bundles/index.js +1417 -0
  7. package/dist/bundles/index.js.map +7 -0
  8. package/dist/bundles/sub-languages/capture/index.js +677 -0
  9. package/dist/bundles/sub-languages/capture/index.js.map +7 -0
  10. package/dist/bundles/sub-languages/projection/index.js +1647 -0
  11. package/dist/bundles/sub-languages/projection/index.js.map +7 -0
  12. package/dist/code-actions.d.ts +3 -0
  13. package/dist/code-actions.d.ts.map +1 -0
  14. package/dist/completion-items.d.ts +1 -0
  15. package/dist/completion-items.d.ts.map +1 -1
  16. package/dist/completion-planner.d.ts.map +1 -1
  17. package/dist/completions.d.ts.map +1 -1
  18. package/dist/diagnostic-codes.d.ts +3 -0
  19. package/dist/diagnostic-codes.d.ts.map +1 -1
  20. package/dist/diagnostics.d.ts.map +1 -1
  21. package/dist/hover-content.d.ts.map +1 -1
  22. package/dist/index.d.ts +3 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/keyword-docs.d.ts.map +1 -1
  25. package/dist/language.d.ts +1 -0
  26. package/dist/language.d.ts.map +1 -1
  27. package/dist/production-destinations.d.ts.map +1 -1
  28. package/dist/symbols.d.ts +4 -0
  29. package/dist/symbols.d.ts.map +1 -1
  30. package/dist/tokens.d.ts.map +1 -1
  31. package/dist/validation.d.ts +1 -0
  32. package/dist/validation.d.ts.map +1 -1
  33. package/package.json +13 -9
  34. package/DestinationHint.ts +0 -8
  35. package/ProductionSymbol.ts +0 -11
  36. package/completion-items.ts +0 -261
  37. package/completion-planner.ts +0 -167
  38. package/completions.ts +0 -156
  39. package/diagnostic-codes.ts +0 -43
  40. package/diagnostics.ts +0 -55
  41. package/dist/DestinationHint.js +0 -4
  42. package/dist/DestinationHint.js.map +0 -1
  43. package/dist/ProductionSymbol.js +0 -4
  44. package/dist/ProductionSymbol.js.map +0 -1
  45. package/dist/completion-items.js +0 -228
  46. package/dist/completion-items.js.map +0 -1
  47. package/dist/completion-planner.js +0 -143
  48. package/dist/completion-planner.js.map +0 -1
  49. package/dist/completions.js +0 -101
  50. package/dist/completions.js.map +0 -1
  51. package/dist/diagnostic-codes.js +0 -41
  52. package/dist/diagnostic-codes.js.map +0 -1
  53. package/dist/diagnostics.js +0 -50
  54. package/dist/diagnostics.js.map +0 -1
  55. package/dist/document-context.js +0 -85
  56. package/dist/document-context.js.map +0 -1
  57. package/dist/event-analysis-source.js +0 -19
  58. package/dist/event-analysis-source.js.map +0 -1
  59. package/dist/event-context-catalog.js +0 -116
  60. package/dist/event-context-catalog.js.map +0 -1
  61. package/dist/event-context.js +0 -88
  62. package/dist/event-context.js.map +0 -1
  63. package/dist/file-imports.js +0 -58
  64. package/dist/file-imports.js.map +0 -1
  65. package/dist/file-references.js +0 -70
  66. package/dist/file-references.js.map +0 -1
  67. package/dist/hover-content.js +0 -98
  68. package/dist/hover-content.js.map +0 -1
  69. package/dist/hover.js +0 -15
  70. package/dist/hover.js.map +0 -1
  71. package/dist/index.js +0 -61
  72. package/dist/index.js.map +0 -1
  73. package/dist/inlay-hints.js +0 -15
  74. package/dist/inlay-hints.js.map +0 -1
  75. package/dist/inline-event-validation.js +0 -131
  76. package/dist/inline-event-validation.js.map +0 -1
  77. package/dist/keyword-docs.js +0 -148
  78. package/dist/keyword-docs.js.map +0 -1
  79. package/dist/language.js +0 -196
  80. package/dist/language.js.map +0 -1
  81. package/dist/production-destinations.js +0 -32
  82. package/dist/production-destinations.js.map +0 -1
  83. package/dist/sub-language-registry.js +0 -18
  84. package/dist/sub-language-registry.js.map +0 -1
  85. package/dist/sub-languages/capture/CompletionProvider.js +0 -332
  86. package/dist/sub-languages/capture/CompletionProvider.js.map +0 -1
  87. package/dist/sub-languages/capture/HoverProvider.js +0 -87
  88. package/dist/sub-languages/capture/HoverProvider.js.map +0 -1
  89. package/dist/sub-languages/capture/Validator.js +0 -72
  90. package/dist/sub-languages/capture/Validator.js.map +0 -1
  91. package/dist/sub-languages/capture/index.js +0 -58
  92. package/dist/sub-languages/capture/index.js.map +0 -1
  93. package/dist/sub-languages/capture/language.js +0 -160
  94. package/dist/sub-languages/capture/language.js.map +0 -1
  95. package/dist/sub-languages/cdl.js +0 -74
  96. package/dist/sub-languages/cdl.js.map +0 -1
  97. package/dist/sub-languages/pdl.js +0 -73
  98. package/dist/sub-languages/pdl.js.map +0 -1
  99. package/dist/sub-languages/projection/CodeActionProvider.js +0 -93
  100. package/dist/sub-languages/projection/CodeActionProvider.js.map +0 -1
  101. package/dist/sub-languages/projection/CompletionProvider.js +0 -595
  102. package/dist/sub-languages/projection/CompletionProvider.js.map +0 -1
  103. package/dist/sub-languages/projection/HoverProvider.js +0 -333
  104. package/dist/sub-languages/projection/HoverProvider.js.map +0 -1
  105. package/dist/sub-languages/projection/Validator.js +0 -388
  106. package/dist/sub-languages/projection/Validator.js.map +0 -1
  107. package/dist/sub-languages/projection/index.js +0 -160
  108. package/dist/sub-languages/projection/index.js.map +0 -1
  109. package/dist/sub-languages/projection/language.js +0 -171
  110. package/dist/sub-languages/projection/language.js.map +0 -1
  111. package/dist/symbols.js +0 -275
  112. package/dist/symbols.js.map +0 -1
  113. package/dist/themes/screenplay-dark.js +0 -30
  114. package/dist/themes/screenplay-dark.js.map +0 -1
  115. package/dist/themes/screenplay-light.js +0 -30
  116. package/dist/themes/screenplay-light.js.map +0 -1
  117. package/dist/tokens.js +0 -147
  118. package/dist/tokens.js.map +0 -1
  119. package/dist/validation.js +0 -248
  120. package/dist/validation.js.map +0 -1
  121. package/document-context.ts +0 -93
  122. package/event-analysis-source.ts +0 -18
  123. package/event-context-catalog.ts +0 -135
  124. package/event-context.ts +0 -119
  125. package/file-imports.ts +0 -70
  126. package/file-references.ts +0 -87
  127. package/for_documentContext/when_stripping_comments.ts +0 -32
  128. package/for_eventContext/when_resolving_generated_catalog_vectors.ts +0 -15
  129. package/for_eventGenerations/when_editing_event_generations.ts +0 -57
  130. package/for_fileImports/when_finding_file_imports.ts +0 -38
  131. package/for_fileReferences/when_recognizing_an_absolute_path/with_paths_of_every_shape.ts +0 -26
  132. package/for_fileReferences/when_scanning_a_document/inside_a_code_fence.ts +0 -26
  133. package/for_fileReferences/when_scanning_a_document/with_a_bare_file_keyword.ts +0 -19
  134. package/for_fileReferences/when_scanning_a_document/with_a_file_directive.ts +0 -30
  135. package/for_fileReferences/when_scanning_a_document/with_a_property_named_file.ts +0 -25
  136. package/for_fileReferences/when_scanning_a_document/with_a_trailing_comment.ts +0 -25
  137. package/for_fileReferences/when_scanning_a_document/with_directives_on_several_constructs.ts +0 -41
  138. package/for_importablePaths/when_listing_what_a_document_can_import.ts +0 -34
  139. package/for_inlineEvents/when_analyzing_many_inline_events.ts +0 -59
  140. package/for_inlineEvents/when_authoring_inline_events.ts +0 -149
  141. package/for_inlineEvents/when_completing_many_events.ts +0 -49
  142. package/for_inlineEvents/when_indexing_command_production_ownership.ts +0 -79
  143. package/for_planCompletions/when_completing_a_file_import.ts +0 -26
  144. package/for_planCompletions/when_completing_a_specification_query.ts +0 -24
  145. package/for_planCompletions/when_completing_a_specification_step.ts +0 -42
  146. package/for_planCompletions/when_completing_an_absent_read_model.ts +0 -14
  147. package/for_planCompletions/when_completing_reaction_reads.ts +0 -36
  148. package/for_projectionCompletionProvider/when_completing_an_event_context_path.ts +0 -102
  149. package/for_projectionHoverProvider/when_hovering_an_event_context_path.ts +0 -44
  150. package/for_projectionValidator/when_validating_a_projection_with_variants.ts +0 -128
  151. package/for_projectionValidator/when_validating_event_context_paths.ts +0 -31
  152. package/for_scanDocument/when_scanning_a_query/with_a_dotted_return_type.ts +0 -34
  153. package/for_scanDocument/when_scanning_aliased_reads.ts +0 -33
  154. package/for_scanDocument/when_scanning_many_commands.ts +0 -47
  155. package/for_tokens/when_highlighting_a_file_import.ts +0 -18
  156. package/for_tokens/when_highlighting_event_metadata.ts +0 -63
  157. package/for_tokens/when_highlighting_specification_steps.ts +0 -28
  158. package/for_validateLines/when_validating_a_document/with_a_problem_of_every_coded_kind.ts +0 -77
  159. package/for_validateLines/when_validating_a_document/with_an_unknown_policy.ts +0 -33
  160. package/for_validateLines/when_validating_a_file_placed_by_an_import/with_names_declared_in_other_files.ts +0 -43
  161. package/for_validateLines/when_validating_event_context_paths/in_a_projection.ts +0 -97
  162. package/for_validateLines/when_validating_file_imports/at_every_level.ts +0 -36
  163. package/for_validateLines/when_validating_production_destinations/with_an_available_identifier.ts +0 -120
  164. package/hover-content.ts +0 -117
  165. package/hover.ts +0 -23
  166. package/index.ts +0 -125
  167. package/inlay-hints.ts +0 -17
  168. package/inline-event-validation.ts +0 -115
  169. package/keyword-docs.ts +0 -152
  170. package/language.ts +0 -212
  171. package/production-destinations.ts +0 -32
  172. package/sub-language-registry.ts +0 -42
  173. package/sub-languages/capture/CompletionProvider.ts +0 -365
  174. package/sub-languages/capture/HoverProvider.ts +0 -99
  175. package/sub-languages/capture/Validator.ts +0 -97
  176. package/sub-languages/capture/index.ts +0 -72
  177. package/sub-languages/capture/language.ts +0 -176
  178. package/sub-languages/cdl.ts +0 -76
  179. package/sub-languages/pdl.ts +0 -75
  180. package/sub-languages/projection/CodeActionProvider.ts +0 -123
  181. package/sub-languages/projection/CompletionProvider.ts +0 -660
  182. package/sub-languages/projection/HoverProvider.ts +0 -377
  183. package/sub-languages/projection/Validator.ts +0 -441
  184. package/sub-languages/projection/index.ts +0 -223
  185. package/sub-languages/projection/language.ts +0 -193
  186. package/symbols.ts +0 -374
  187. package/themes/screenplay-dark.ts +0 -33
  188. package/themes/screenplay-light.ts +0 -33
  189. package/tokens.ts +0 -174
  190. package/validation.ts +0 -443
  191. package/vitest.setup.ts +0 -5
@@ -1,660 +0,0 @@
1
- // Copyright (c) Cratis. All rights reserved.
2
- // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
-
4
- import type { editor, languages, IRange, Position } from 'monaco-editor';
5
- import type { JsonSchema, JsonSchemaProperty, ReadModelInfo } from './index';
6
- import { eventContextMembers, eventContextMembersAfter } from '../../event-context';
7
-
8
- interface CompletionContext {
9
- lineContent: string;
10
- textBeforeCursor: string;
11
- textAfterCursor: string;
12
- currentWord: string;
13
- indentLevel: number;
14
- lineNumber: number;
15
- }
16
-
17
- // The path typed so far after $eventContext. - on its own or as the dynamic key of a mapping target - and the
18
- // partial segment under the cursor.
19
- const EVENT_CONTEXT_PATH = /\$eventContext\.((?:\w+\.)*)(\w*)$/;
20
-
21
- export class CompletionProvider implements languages.CompletionItemProvider {
22
- private readModels: ReadModelInfo[] = [];
23
- private eventSchemas: Record<string, JsonSchema> = {};
24
- private eventSequences: string[] = [];
25
-
26
- setReadModels(readModels: ReadModelInfo[]): void {
27
- this.readModels = readModels || [];
28
- }
29
-
30
- setEventSequences(sequences: string[]): void {
31
- this.eventSequences = sequences || [];
32
- }
33
-
34
- // Keep for backwards compatibility
35
- setReadModelSchemas(schemas: JsonSchema[]): void {
36
- this.readModels = schemas.map(schema => ({
37
- identifier: this.getSchemaName(schema) || '',
38
- displayName: this.getSchemaName(schema) || '',
39
- schema
40
- }));
41
- }
42
-
43
- setEventSchemas(schemas: Record<string, JsonSchema> | JsonSchema[]): void {
44
- if (!schemas) {
45
- this.eventSchemas = {};
46
- return;
47
- }
48
-
49
- if (Array.isArray(schemas)) {
50
- const map: Record<string, JsonSchema> = {};
51
- schemas.forEach((s, i) => {
52
- if (!s) return;
53
- const name = this.getSchemaName(s) || `Event${i + 1}`;
54
- map[name] = s;
55
- });
56
- this.eventSchemas = map;
57
- return;
58
- }
59
-
60
- this.eventSchemas = schemas;
61
- }
62
-
63
- provideCompletionItems(
64
- model: editor.ITextModel,
65
- position: Position
66
- ): languages.ProviderResult<languages.CompletionList> {
67
- const context = this.buildContext(model, position);
68
- const suggestions: languages.CompletionItem[] = [];
69
-
70
- // Get the active read model schema from the projection declaration
71
- const activeSchema = this.getActiveReadModelSchema(model);
72
-
73
- // Determine what kind of completions to provide based on context
74
- if (context.lineNumber === 1) {
75
- this.addProjectionLineCompletions(suggestions, context);
76
- } else if (this.isAfterKeyword(context, 'sequence')) {
77
- this.addEventSequenceCompletions(suggestions, context);
78
- } else if (this.isAfterKeyword(context, 'from') || this.isAfterKeyword(context, 'every')) {
79
- this.addEventTypeCompletions(suggestions, context);
80
- } else if (/\benters\s+on\s+\w*$/.test(context.textBeforeCursor)) {
81
- this.addEventTypeCompletions(suggestions, context);
82
- } else if (/\benters\s+\w*$/.test(context.textBeforeCursor)) {
83
- suggestions.push({
84
- label: 'on',
85
- kind: 14, // Keyword
86
- insertText: 'on ',
87
- documentation: 'Declares the event that activates this variant.',
88
- detail: 'enters on <EventType>',
89
- range: this.getRangeForWord(context),
90
- });
91
- } else if (this.isAfterKeyword(context, 'variant')) {
92
- this.addVariantNameCompletions(suggestions, context);
93
- } else if (this.isAfterKeyword(context, 'key')) {
94
- this.addKeyCompletions(suggestions, context, activeSchema);
95
- } else if (this.isAfterKeyword(context, 'parent')) {
96
- this.addParentCompletions(suggestions, context);
97
- } else if (this.isAfterKeyword(context, 'children')) {
98
- this.addChildrenCompletions(suggestions, context, activeSchema);
99
- } else if (this.isAfterKeyword(context, 'join')) {
100
- this.addJoinCompletions(suggestions, context, activeSchema);
101
- } else if (this.isAfterKeyword(context, 'with')) {
102
- this.addEventTypeCompletions(suggestions, context);
103
- } else if (this.isAfterNumericOperationKeyword(context)) {
104
- this.addNumericPropertyCompletions(suggestions, context, activeSchema);
105
- } else if (this.isAfterAddOrSubtractBy(context)) {
106
- this.addAssignmentValueCompletions(suggestions, context, model, position);
107
- } else if (this.isInAssignment(context)) {
108
- this.addAssignmentValueCompletions(suggestions, context, model, position);
109
- } else if (this.isPropertyDotAccess(context)) {
110
- this.addPropertyMemberCompletions(suggestions, context);
111
- } else if (this.isStartOfLine(context) || this.isAfterIndent(context)) {
112
- this.addStatementCompletions(suggestions, context, activeSchema);
113
- }
114
-
115
- return { suggestions };
116
- }
117
-
118
- private buildContext(model: editor.ITextModel, position: Position): CompletionContext {
119
- const lineContent = model.getLineContent(position.lineNumber);
120
- const textBeforeCursor = lineContent.substring(0, position.column - 1);
121
- const textAfterCursor = lineContent.substring(position.column - 1);
122
- const word = model.getWordAtPosition(position);
123
- const currentWord = word?.word || '';
124
- const indentLevel = this.getIndentLevel(textBeforeCursor);
125
-
126
- return {
127
- lineContent,
128
- textBeforeCursor,
129
- textAfterCursor,
130
- currentWord,
131
- indentLevel,
132
- lineNumber: position.lineNumber,
133
- };
134
- }
135
-
136
- private getIndentLevel(text: string): number {
137
- const match = text.match(/^(\s*)/);
138
- return match ? Math.floor(match[1].length / 2) : 0;
139
- }
140
-
141
- private isStartOfLine(context: CompletionContext): boolean {
142
- return context.textBeforeCursor.trim() === '';
143
- }
144
-
145
- private isAfterIndent(context: CompletionContext): boolean {
146
- return /^\s+$/.test(context.textBeforeCursor);
147
- }
148
-
149
- private isAfterKeyword(context: CompletionContext, keyword: string): boolean {
150
- const pattern = new RegExp(`\\b${keyword}\\s+\\w*$`);
151
- return pattern.test(context.textBeforeCursor);
152
- }
153
-
154
- private isAfterNumericOperationKeyword(context: CompletionContext): boolean {
155
- // Match: count, increment, decrement, add, subtract followed by optional word (but not "by")
156
- // For add/subtract, we only match if "by" hasn't been typed yet
157
- const countIncrDecrPattern = /\b(count|increment|decrement)\s+\w*$/;
158
- const addSubtractPattern = /\b(add|subtract)\s+\w*$/;
159
-
160
- // Check for count/increment/decrement first
161
- if (countIncrDecrPattern.test(context.textBeforeCursor)) {
162
- return true;
163
- }
164
-
165
- // For add/subtract, only match if "by" hasn't been typed yet
166
- if (addSubtractPattern.test(context.textBeforeCursor)) {
167
- // Make sure we're not after "by"
168
- return !this.isAfterAddOrSubtractBy(context);
169
- }
170
-
171
- return false;
172
- }
173
-
174
- private isAfterAddOrSubtractBy(context: CompletionContext): boolean {
175
- // Match: "add <property> by " or "subtract <property> by " followed by optional word
176
- const pattern = /\b(add|subtract)\s+\w+\s+by\s+\w*$/;
177
- return pattern.test(context.textBeforeCursor);
178
- }
179
-
180
- private isInAssignment(context: CompletionContext): boolean {
181
- return /\w+\s*=\s*\w*$/.test(context.textBeforeCursor);
182
- }
183
-
184
- private isPropertyDotAccess(context: CompletionContext): boolean {
185
- // A member of an event property or a $-expression, or any depth of an event-context path - written as an
186
- // expression or as the dynamic key of a mapping target (propertyName.$eventContext.<path>).
187
- return /(\w+|\$\w+)\.(\w*)$/.test(context.textBeforeCursor) ||
188
- EVENT_CONTEXT_PATH.test(context.textBeforeCursor);
189
- }
190
-
191
- private addProjectionLineCompletions(suggestions: languages.CompletionItem[], context: CompletionContext): void {
192
- const trimmed = context.textBeforeCursor.trim();
193
-
194
- // Suggest "projection" keyword
195
- if (trimmed === '' || 'projection'.startsWith(trimmed)) {
196
- suggestions.push({
197
- label: 'projection',
198
- kind: 14, // Keyword
199
- insertText: 'projection ',
200
- documentation: 'Define a projection that transforms events into a read model',
201
- detail: 'projection <ProjectionName>',
202
- range: this.getRangeForWord(context),
203
- });
204
- }
205
-
206
- // Check if cursor is after "=>" to suggest read model names
207
- const arrowMatch = trimmed.match(/^projection\s+[\w.]+\s*=>\s*([\w.]*)$/);
208
- if (arrowMatch) {
209
- const partialReadModel = arrowMatch[1] || '';
210
- this.readModels.forEach((readModel) => {
211
- if (!partialReadModel || readModel.displayName.toLowerCase().startsWith(partialReadModel.toLowerCase())) {
212
- const label = `${readModel.displayName} (${readModel.identifier})`;
213
- suggestions.push({
214
- label,
215
- kind: 6, // Class
216
- insertText: readModel.displayName,
217
- filterText: `${readModel.displayName} ${readModel.identifier}`,
218
- documentation: `Read model: ${readModel.displayName} (${readModel.identifier})`,
219
- detail: readModel.identifier,
220
- range: this.getRangeForWord(context),
221
- });
222
- }
223
- });
224
- return;
225
- }
226
-
227
- // After "projection ", suggest read model names (for quick completion of both projection name and read model)
228
- if (trimmed.startsWith('projection ') && !trimmed.includes('=>')) {
229
- const afterProjection = trimmed.substring('projection '.length);
230
- this.readModels.forEach((readModel) => {
231
- if (!afterProjection || readModel.displayName.toLowerCase().startsWith(afterProjection.toLowerCase())) {
232
- const label = `${readModel.displayName} (${readModel.identifier})`;
233
- suggestions.push({
234
- label,
235
- kind: 6, // Class
236
- insertText: `${readModel.displayName} => ${readModel.displayName}`,
237
- filterText: `${readModel.displayName} ${readModel.identifier}`,
238
- documentation: `Read model: ${readModel.displayName} (${readModel.identifier})`,
239
- detail: readModel.identifier,
240
- range: this.getRangeForWord(context),
241
- });
242
- }
243
- });
244
- }
245
- }
246
-
247
- private addEventTypeCompletions(suggestions: languages.CompletionItem[], context: CompletionContext): void {
248
- Object.keys(this.eventSchemas).forEach((eventName) => {
249
- if (!context.currentWord || eventName.startsWith(context.currentWord)) {
250
- const schema = this.eventSchemas[eventName];
251
- suggestions.push({
252
- label: eventName,
253
- kind: 6, // Class
254
- insertText: eventName,
255
- documentation: this.getSchemaDescription(schema) || `Event type: ${eventName}`,
256
- detail: 'Event Type',
257
- range: this.getRangeForWord(context),
258
- });
259
- }
260
- });
261
- }
262
-
263
- private addEventSequenceCompletions(suggestions: languages.CompletionItem[], context: CompletionContext): void {
264
- this.eventSequences.forEach((sequenceId) => {
265
- if (!context.currentWord || sequenceId.startsWith(context.currentWord)) {
266
- suggestions.push({
267
- label: sequenceId,
268
- kind: 13, // Value
269
- insertText: sequenceId,
270
- documentation: `Event sequence: ${sequenceId}`,
271
- detail: 'Event Sequence',
272
- range: this.getRangeForWord(context),
273
- });
274
- }
275
- });
276
- }
277
-
278
- private addKeyCompletions(suggestions: languages.CompletionItem[], context: CompletionContext, activeSchema?: JsonSchema): void {
279
- // Check if this might be a composite key (type reference)
280
- if (activeSchema?.definitions) {
281
- Object.keys(activeSchema.definitions).forEach((typeName) => {
282
- const typeDef = activeSchema.definitions![typeName];
283
- if (typeDef.type === 'object') {
284
- suggestions.push({
285
- label: typeName,
286
- kind: 6, // Class
287
- insertText: typeName,
288
- documentation: `Composite key type: ${typeName}`,
289
- detail: 'Composite Key Type',
290
- range: this.getRangeForWord(context),
291
- });
292
- }
293
- });
294
- }
295
-
296
- // Suggest built-in expressions for simple keys
297
- this.addExpressionCompletions(suggestions, context);
298
- }
299
-
300
- private addParentCompletions(suggestions: languages.CompletionItem[], context: CompletionContext): void {
301
- this.addExpressionCompletions(suggestions, context);
302
- }
303
-
304
- private addNumericPropertyCompletions(suggestions: languages.CompletionItem[], context: CompletionContext, activeSchema?: JsonSchema): void {
305
- // After "count ", "increment ", or "decrement ", suggest numeric properties from read model
306
- if (activeSchema?.properties) {
307
- Object.entries(activeSchema.properties).forEach(([propName, propSchema]) => {
308
- // Suggest numeric properties (integer or number types)
309
- const isNumeric = propSchema.type === 'integer' || propSchema.type === 'number';
310
- if (isNumeric) {
311
- if (!context.currentWord || propName.startsWith(context.currentWord)) {
312
- suggestions.push({
313
- label: propName,
314
- kind: 9, // Property
315
- insertText: propName,
316
- documentation: `Numeric property: ${propName}`,
317
- detail: this.getPropertyTypeDescription(propSchema),
318
- range: this.getRangeForWord(context),
319
- });
320
- }
321
- }
322
- });
323
- }
324
-
325
- // Also check if "add" or "subtract" is being typed - need to handle "add <prop> by" pattern
326
- const addSubtractMatch = context.textBeforeCursor.match(/\b(add|subtract)\s+(\w*)$/);
327
- if (addSubtractMatch) {
328
- const partialProp = addSubtractMatch[2] || '';
329
- if (activeSchema?.properties) {
330
- Object.entries(activeSchema.properties).forEach(([propName, propSchema]) => {
331
- const isNumeric = propSchema.type === 'integer' || propSchema.type === 'number';
332
- if (isNumeric && (!partialProp || propName.startsWith(partialProp))) {
333
- suggestions.push({
334
- label: propName,
335
- kind: 9, // Property
336
- insertText: `${propName} by `,
337
- documentation: `Numeric property: ${propName}`,
338
- detail: this.getPropertyTypeDescription(propSchema),
339
- range: this.getRangeForWord(context),
340
- });
341
- }
342
- });
343
- }
344
- }
345
- }
346
-
347
- private addChildrenCompletions(suggestions: languages.CompletionItem[], context: CompletionContext, activeSchema?: JsonSchema): void {
348
- // After "children ", suggest collection properties from read model
349
- if (activeSchema?.properties) {
350
- Object.entries(activeSchema.properties).forEach(([propName, propSchema]) => {
351
- // Collections are typically arrays
352
- if (propSchema.type === 'array' || propSchema.items) {
353
- suggestions.push({
354
- label: propName,
355
- kind: 9, // Property
356
- insertText: `${propName} id `,
357
- documentation: `Collection property: ${propName}`,
358
- detail: this.getPropertyTypeDescription(propSchema),
359
- range: this.getRangeForWord(context),
360
- });
361
- }
362
- });
363
- }
364
- }
365
-
366
- private addJoinCompletions(suggestions: languages.CompletionItem[], context: CompletionContext, activeSchema?: JsonSchema): void {
367
- const trimmed = context.textBeforeCursor.trim();
368
-
369
- if (trimmed === 'join' || !trimmed.includes(' ')) {
370
- // Suggest collection properties
371
- if (activeSchema?.properties) {
372
- Object.entries(activeSchema.properties).forEach(([propName, propSchema]) => {
373
- if (propSchema.type === 'array' || propSchema.items) {
374
- suggestions.push({
375
- label: propName,
376
- kind: 9,
377
- insertText: `${propName} on `,
378
- documentation: `Join on collection: ${propName}`,
379
- range: this.getRangeForWord(context),
380
- });
381
- }
382
- });
383
- }
384
- } else if (trimmed.match(/join\s+\w+\s+on\s+/)) {
385
- // After "on", suggest properties to join on
386
- if (activeSchema?.properties) {
387
- Object.keys(activeSchema.properties).forEach((propName) => {
388
- suggestions.push({
389
- label: propName,
390
- kind: 9,
391
- insertText: propName,
392
- documentation: `Property: ${propName}`,
393
- range: this.getRangeForWord(context),
394
- });
395
- });
396
- }
397
- }
398
- }
399
-
400
- private addVariantNameCompletions(suggestions: languages.CompletionItem[], context: CompletionContext): void {
401
- // After "variant ", suggest read model names - a variant names the read model it produces.
402
- this.readModels.forEach((readModel) => {
403
- if (!context.currentWord || readModel.displayName.toLowerCase().startsWith(context.currentWord.toLowerCase())) {
404
- suggestions.push({
405
- label: readModel.displayName,
406
- kind: 6, // Class
407
- insertText: readModel.displayName,
408
- documentation: `Read model: ${readModel.displayName} (${readModel.identifier})`,
409
- detail: readModel.identifier,
410
- range: this.getRangeForWord(context),
411
- });
412
- }
413
- });
414
- }
415
-
416
- private addStatementCompletions(suggestions: languages.CompletionItem[], context: CompletionContext, activeSchema?: JsonSchema): void {
417
- // Add all statement keywords
418
- const keywords = [
419
- { label: 'sequence', insertText: 'sequence ', documentation: 'Specify which event sequence to use for this projection', detail: 'sequence <event-sequence-id>' },
420
- { label: 'variant', insertText: 'variant ', documentation: 'Declare one of several mutually exclusive named read models sharing this projection\'s identity', detail: 'variant <Name>' },
421
- { label: 'enters on', insertText: 'enters on ', documentation: 'Declare the event that activates a variant', detail: 'enters on <EventType>' },
422
- { label: 'from', insertText: 'from ', documentation: 'Handle specific event types', detail: 'from <EventType>' },
423
- { label: 'all', insertText: 'all', documentation: 'Subscribe to all event types in the system — mappings run for every event system-wide, regardless of explicit from blocks', detail: 'all' },
424
- { label: 'every', insertText: 'every', documentation: 'Handle all events for automatic mapping (only events explicitly subscribed to via from blocks)', detail: 'every' },
425
- { label: 'key', insertText: 'key ', documentation: 'Define the projection key', detail: 'key <expression>' },
426
- { label: 'parent', insertText: 'parent ', documentation: 'Define parent relationship', detail: 'parent <expression>' },
427
- { label: 'join', insertText: 'join ', documentation: 'Join with child collection', detail: 'join <collection> on <property>' },
428
- { label: 'children', insertText: 'children ', documentation: 'Define child collection operations', detail: 'children <collection> identified by <expression>' },
429
- { label: 'remove with', insertText: 'remove with ', documentation: 'Remove items based on event', detail: 'remove with <EventType>' },
430
- { label: 'remove via', insertText: 'remove via join on ', documentation: 'Remove items via join', detail: 'remove via join on <EventType>' },
431
- { label: 'no automap', insertText: 'no automap', documentation: 'Disable automatic property mapping', detail: 'no automap' },
432
- { label: 'clear', insertText: 'clear ', documentation: 'Remove the value a property holds', detail: 'clear <property>' },
433
- { label: 'increment', insertText: 'increment ', documentation: 'Increment a numeric property', detail: 'increment <property>' },
434
- { label: 'decrement', insertText: 'decrement ', documentation: 'Decrement a numeric property', detail: 'decrement <property>' },
435
- { label: 'count', insertText: 'count ', documentation: 'Count occurrences', detail: 'count <property>' },
436
- { label: 'add', insertText: 'add ', documentation: 'Add value to property', detail: 'add <property> by <expression>' },
437
- { label: 'subtract', insertText: 'subtract ', documentation: 'Subtract value from property', detail: 'subtract <property> by <expression>' },
438
- { label: 'exclude children', insertText: 'exclude children', documentation: 'Exclude child collections from automap', detail: 'exclude children' },
439
- ];
440
-
441
- keywords.forEach((kw) => {
442
- if (!context.currentWord || kw.label.startsWith(context.currentWord)) {
443
- suggestions.push({
444
- label: kw.label,
445
- kind: 14, // Keyword
446
- insertText: kw.insertText,
447
- documentation: kw.documentation,
448
- detail: kw.detail,
449
- range: this.getRangeForWord(context),
450
- });
451
- }
452
- });
453
-
454
- // Add property assignments
455
- if (activeSchema?.properties) {
456
- Object.entries(activeSchema.properties).forEach(([propName, propSchema]) => {
457
- if (!context.currentWord || propName.startsWith(context.currentWord)) {
458
- suggestions.push({
459
- label: propName,
460
- kind: 9, // Property
461
- insertText: `${propName} = `,
462
- documentation: `Assign value to property: ${propName}`,
463
- detail: this.getPropertyTypeDescription(propSchema),
464
- range: this.getRangeForWord(context),
465
- });
466
- }
467
- });
468
- }
469
- }
470
-
471
- private addAssignmentValueCompletions(
472
- suggestions: languages.CompletionItem[],
473
- context: CompletionContext,
474
- model: editor.ITextModel,
475
- position: Position
476
- ): void {
477
- // Get the event type from the current block
478
- const eventType = this.getCurrentEventType(model, position.lineNumber);
479
-
480
- // Suggest event properties if we know the event type
481
- if (eventType && this.eventSchemas[eventType]) {
482
- const eventSchema = this.eventSchemas[eventType];
483
- if (eventSchema.properties) {
484
- Object.entries(eventSchema.properties).forEach(([propName, propSchema]) => {
485
- suggestions.push({
486
- label: propName,
487
- kind: 9,
488
- insertText: propName,
489
- documentation: `Event property: ${propName}`,
490
- detail: this.getPropertyTypeDescription(propSchema),
491
- range: this.getRangeForWord(context),
492
- });
493
- });
494
- }
495
- }
496
-
497
- // Add expression completions
498
- this.addExpressionCompletions(suggestions, context);
499
- }
500
-
501
- private addExpressionCompletions(suggestions: languages.CompletionItem[], context: CompletionContext): void {
502
- const expressions = [
503
- {
504
- label: '$eventSourceId',
505
- insertText: '$eventSourceId',
506
- documentation: 'The identifier of the event source (aggregate/entity)',
507
- detail: 'Event Source Expression'
508
- },
509
- {
510
- label: '$causedBy',
511
- insertText: '$causedBy',
512
- documentation: 'The identity that caused the event. Chronicle cannot yet evaluate this short form (Cratis/Chronicle#4119) - prefer $eventContext.causedBy.<property>.',
513
- detail: 'Identity Expression'
514
- },
515
- {
516
- label: '$eventContext',
517
- insertText: '$eventContext.',
518
- documentation: `Metadata about the event: ${eventContextMembers.map((member) => member.name).join(', ')}`,
519
- detail: 'Event Context Expression'
520
- },
521
- ];
522
-
523
- expressions.forEach((expr) => {
524
- if (!context.currentWord || expr.label.startsWith(context.currentWord) || expr.label.startsWith('$' + context.currentWord)) {
525
- suggestions.push({
526
- label: expr.label,
527
- kind: 14, // Keyword/builtin
528
- insertText: expr.insertText,
529
- documentation: expr.documentation,
530
- detail: expr.detail,
531
- range: this.getRangeForWord(context),
532
- });
533
- }
534
- });
535
- }
536
-
537
- private addPropertyMemberCompletions(suggestions: languages.CompletionItem[], context: CompletionContext): void {
538
- // Any depth of an event-context path, as an expression or as a dynamic key (propertyName.$eventContext.<path>)
539
- const eventContextMatch = context.textBeforeCursor.match(EVENT_CONTEXT_PATH);
540
- if (eventContextMatch) {
541
- const segments = eventContextMatch[1].split('.').filter((segment) => segment.length > 0);
542
- this.addEventContextMemberCompletions(suggestions, context, segments, eventContextMatch[2]);
543
- return;
544
- }
545
-
546
- const match = context.textBeforeCursor.match(/(\w+|\$\w+)\.(\w*)$/);
547
- if (!match) return;
548
-
549
- const [, objectName, partialProp] = match;
550
-
551
- // $causedBy is the short form of $eventContext.causedBy, at any depth
552
- const causedByMatch = context.textBeforeCursor.match(/\$causedBy\.((?:\w+\.)*)(\w*)$/);
553
- if (causedByMatch) {
554
- const segments = causedByMatch[1].split('.').filter((segment) => segment.length > 0);
555
- this.addEventContextMemberCompletions(suggestions, context, ['causedBy', ...segments], causedByMatch[2]);
556
- return;
557
- }
558
-
559
- // Handle event type properties
560
- const eventSchema = this.eventSchemas[objectName];
561
- if (eventSchema?.properties) {
562
- Object.entries(eventSchema.properties).forEach(([propName, propSchema]) => {
563
- if (!partialProp || propName.startsWith(partialProp)) {
564
- suggestions.push({
565
- label: propName,
566
- kind: 9,
567
- insertText: propName,
568
- documentation: `Event property: ${propName}`,
569
- detail: this.getPropertyTypeDescription(propSchema),
570
- range: this.getRangeForWord(context),
571
- });
572
- }
573
- });
574
- }
575
- }
576
-
577
- private addEventContextMemberCompletions(
578
- suggestions: languages.CompletionItem[],
579
- context: CompletionContext,
580
- segments: readonly string[],
581
- partial: string
582
- ): void {
583
- const members = eventContextMembersAfter(segments) ?? [];
584
- members
585
- .filter((member) => !partial || member.name.startsWith(partial))
586
- .forEach((member) => {
587
- suggestions.push({
588
- label: member.name,
589
- kind: member.kind === 'function' ? 1 : 9,
590
- insertText: member.name,
591
- documentation: member.description,
592
- detail: member.type,
593
- range: this.getRangeForWord(context),
594
- });
595
- });
596
- }
597
-
598
- private getCurrentEventType(model: editor.ITextModel, lineNumber: number): string | null {
599
- // Search backwards to find the event type declaration
600
- for (let i = lineNumber; i >= 1; i--) {
601
- const line = model.getLineContent(i).trim();
602
- const fromMatch = line.match(/^from\s+([\w-]+)/);
603
- if (fromMatch) {
604
- return fromMatch[1];
605
- }
606
- const everyMatch = line.match(/^every$/);
607
- if (everyMatch) {
608
- return null; // every block doesn't have a specific event type
609
- }
610
- }
611
- return null;
612
- }
613
-
614
- private getActiveReadModelSchema(model: editor.ITextModel): JsonSchema | undefined {
615
- const firstLine = model.getLineContent(1).trim();
616
- const match = firstLine.match(/^projection\s+[\w.]+\s*=>\s*([\w.]+)/);
617
- if (!match) return undefined;
618
-
619
- const readModelName = match[1];
620
- const readModel = this.resolveReadModelInfo(readModelName);
621
- return readModel?.schema;
622
- }
623
-
624
- private resolveReadModelInfo(readModelToken: string): ReadModelInfo | null {
625
- return this.readModels.find(rm => rm.identifier === readModelToken || rm.displayName === readModelToken) || null;
626
- }
627
-
628
- private getSchemaName(schema: JsonSchema): string | undefined {
629
- if (schema.name) return schema.name;
630
- if (schema.title) return schema.title;
631
- if (typeof schema.$id === 'string') {
632
- const parts = schema.$id.split('/');
633
- return parts[parts.length - 1] || schema.$id;
634
- }
635
- return undefined;
636
- }
637
-
638
- private getSchemaDescription(schema: JsonSchema): string | undefined {
639
- return schema.description || undefined;
640
- }
641
-
642
- private getPropertyTypeDescription(propSchema: JsonSchemaProperty): string {
643
- if (propSchema.type === 'array' && propSchema.items) {
644
- const itemType = propSchema.items.type || propSchema.items.$id?.split('/').pop() || 'unknown';
645
- return `${itemType}[]`;
646
- }
647
- return propSchema.type || propSchema.$ref?.split('/').pop() || 'unknown';
648
- }
649
-
650
- private getRangeForWord(context: CompletionContext): IRange {
651
- const startColumn = context.textBeforeCursor.length - context.currentWord.length + 1;
652
- const endColumn = startColumn + context.currentWord.length;
653
- return {
654
- startLineNumber: context.lineNumber,
655
- startColumn: Math.max(1, startColumn),
656
- endLineNumber: context.lineNumber,
657
- endColumn: Math.max(1, endColumn),
658
- };
659
- }
660
- }