@atomic-ehr/fhirpath 0.0.1 → 0.0.2-canary.261bb9b.20250811212042

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 (104) hide show
  1. package/README.md +62 -56
  2. package/dist/index.d.ts +368 -99
  3. package/dist/index.js +3623 -1253
  4. package/dist/index.js.map +1 -1
  5. package/package.json +1 -1
  6. package/src/analyzer.ts +779 -120
  7. package/src/boxing.ts +124 -0
  8. package/src/completion-provider.ts +636 -0
  9. package/src/cursor-nodes.ts +111 -0
  10. package/src/errors.ts +247 -0
  11. package/src/index.ts +97 -14
  12. package/src/inspect.ts +309 -176
  13. package/src/interpreter.ts +380 -71
  14. package/src/lexer.ts +1 -0
  15. package/src/model-provider.ts +282 -39
  16. package/src/operations/abs-function.ts +40 -16
  17. package/src/operations/aggregate-function.ts +21 -10
  18. package/src/operations/all-function.ts +23 -11
  19. package/src/operations/allFalse-function.ts +14 -9
  20. package/src/operations/allTrue-function.ts +14 -9
  21. package/src/operations/and-operator.ts +21 -12
  22. package/src/operations/anyFalse-function.ts +11 -7
  23. package/src/operations/anyTrue-function.ts +11 -7
  24. package/src/operations/as-operator.ts +2 -1
  25. package/src/operations/ceiling-function.ts +14 -8
  26. package/src/operations/children-function.ts +98 -0
  27. package/src/operations/combine-function.ts +10 -6
  28. package/src/operations/combine-operator.ts +19 -5
  29. package/src/operations/contains-function.ts +26 -12
  30. package/src/operations/contains-operator.ts +15 -7
  31. package/src/operations/count-function.ts +7 -4
  32. package/src/operations/defineVariable-function.ts +11 -7
  33. package/src/operations/descendants-function.ts +64 -0
  34. package/src/operations/distinct-function.ts +21 -7
  35. package/src/operations/div-operator.ts +16 -3
  36. package/src/operations/divide-operator.ts +11 -6
  37. package/src/operations/dot-operator.ts +4 -2
  38. package/src/operations/empty-function.ts +7 -4
  39. package/src/operations/endsWith-function.ts +28 -14
  40. package/src/operations/equal-operator.ts +19 -7
  41. package/src/operations/equivalent-operator.ts +16 -11
  42. package/src/operations/exclude-function.ts +18 -14
  43. package/src/operations/exists-function.ts +19 -10
  44. package/src/operations/first-function.ts +12 -7
  45. package/src/operations/floor-function.ts +14 -8
  46. package/src/operations/greater-operator.ts +10 -5
  47. package/src/operations/greater-or-equal-operator.ts +10 -5
  48. package/src/operations/iif-function.ts +27 -23
  49. package/src/operations/implies-operator.ts +23 -8
  50. package/src/operations/in-operator.ts +18 -8
  51. package/src/operations/index.ts +3 -0
  52. package/src/operations/indexOf-function.ts +28 -14
  53. package/src/operations/intersect-function.ts +23 -24
  54. package/src/operations/is-operator.ts +67 -15
  55. package/src/operations/isDistinct-function.ts +17 -9
  56. package/src/operations/join-function.ts +22 -9
  57. package/src/operations/last-function.ts +12 -7
  58. package/src/operations/length-function.ts +17 -8
  59. package/src/operations/less-operator.ts +10 -5
  60. package/src/operations/less-or-equal-operator.ts +10 -5
  61. package/src/operations/less-than.ts +26 -2
  62. package/src/operations/lower-function.ts +17 -8
  63. package/src/operations/minus-operator.ts +10 -5
  64. package/src/operations/mod-operator.ts +20 -3
  65. package/src/operations/multiply-operator.ts +12 -7
  66. package/src/operations/not-equal-operator.ts +16 -7
  67. package/src/operations/not-equivalent-operator.ts +16 -11
  68. package/src/operations/not-function.ts +17 -7
  69. package/src/operations/ofType-function.ts +139 -0
  70. package/src/operations/or-operator.ts +21 -12
  71. package/src/operations/plus-operator.ts +19 -7
  72. package/src/operations/power-function.ts +27 -13
  73. package/src/operations/replace-function.ts +33 -14
  74. package/src/operations/round-function.ts +29 -12
  75. package/src/operations/select-function.ts +17 -9
  76. package/src/operations/single-function.ts +10 -6
  77. package/src/operations/skip-function.ts +18 -9
  78. package/src/operations/split-function.ts +30 -13
  79. package/src/operations/sqrt-function.ts +14 -8
  80. package/src/operations/startsWith-function.ts +26 -12
  81. package/src/operations/subsetOf-function.ts +20 -15
  82. package/src/operations/substring-function.ts +43 -24
  83. package/src/operations/supersetOf-function.ts +20 -15
  84. package/src/operations/tail-function.ts +8 -4
  85. package/src/operations/take-function.ts +18 -9
  86. package/src/operations/toBoolean-function.ts +23 -14
  87. package/src/operations/toDecimal-function.ts +18 -9
  88. package/src/operations/toInteger-function.ts +22 -12
  89. package/src/operations/toString-function.ts +26 -16
  90. package/src/operations/trace-function.ts +19 -10
  91. package/src/operations/trim-function.ts +16 -7
  92. package/src/operations/truncate-function.ts +14 -8
  93. package/src/operations/unary-minus-operator.ts +23 -13
  94. package/src/operations/unary-plus-operator.ts +2 -1
  95. package/src/operations/union-function.ts +25 -28
  96. package/src/operations/union-operator.ts +2 -1
  97. package/src/operations/upper-function.ts +17 -8
  98. package/src/operations/where-function.ts +20 -11
  99. package/src/operations/xor-operator.ts +9 -4
  100. package/src/parser.ts +527 -8
  101. package/src/quantity-value.ts +4 -8
  102. package/src/registry.ts +147 -5
  103. package/src/types.ts +42 -19
  104. package/src/parser-base.ts +0 -400
@@ -0,0 +1,636 @@
1
+ import { parse } from './parser';
2
+ import { Analyzer } from './analyzer';
3
+ import type { TypeInfo, ModelProvider } from './types';
4
+
5
+ import { CursorContext, isCursorNode } from './cursor-nodes';
6
+ import type { AnyCursorNode } from './cursor-nodes';
7
+ import { registry } from './registry';
8
+
9
+ /**
10
+ * Kind of completion item
11
+ */
12
+ export enum CompletionKind {
13
+ Property = 'property',
14
+ Function = 'function',
15
+ Variable = 'variable',
16
+ Operator = 'operator',
17
+ Type = 'type',
18
+ Keyword = 'keyword',
19
+ Constant = 'constant'
20
+ }
21
+
22
+ /**
23
+ * Represents a completion item
24
+ */
25
+ export interface CompletionItem {
26
+ /** Display text */
27
+ label: string;
28
+ /** Kind of completion */
29
+ kind: CompletionKind;
30
+ /** Short description */
31
+ detail?: string;
32
+ /** Full documentation */
33
+ documentation?: string;
34
+ /** Text to insert (if different from label) */
35
+ insertText?: string;
36
+ /** Sort order */
37
+ sortText?: string;
38
+ }
39
+
40
+ /**
41
+ * Options for completion provider
42
+ */
43
+ export interface CompletionOptions {
44
+ /** Model provider for FHIR types */
45
+ modelProvider?: ModelProvider;
46
+ /** Variables in scope */
47
+ variables?: Record<string, any>;
48
+ /** Input type for the expression */
49
+ inputType?: TypeInfo;
50
+ /** Maximum number of completions to return */
51
+ maxCompletions?: number;
52
+ }
53
+
54
+ /**
55
+ * Provides context-aware completions for FHIRPath expressions
56
+ */
57
+ export async function provideCompletions(
58
+ expression: string,
59
+ cursorPosition: number,
60
+ options: CompletionOptions = {}
61
+ ): Promise<CompletionItem[]> {
62
+ const { modelProvider, variables, inputType, maxCompletions = 100 } = options;
63
+
64
+ try {
65
+ // Parse with cursor
66
+ const parseResult = parse(expression, { cursorPosition });
67
+ if (!parseResult.ast) {
68
+ return [];
69
+ }
70
+
71
+ // Analyze with cursor mode
72
+ const analyzer = new Analyzer(modelProvider);
73
+ const analysis = await analyzer.analyze(
74
+ parseResult.ast,
75
+ variables,
76
+ inputType,
77
+ { cursorMode: true }
78
+ );
79
+
80
+ // Extract cursor context
81
+ let cursorNode = analysis.cursorContext?.cursorNode;
82
+ let typeBeforeCursor = analysis.cursorContext?.typeBeforeCursor;
83
+
84
+ // Check if cursor is in a binary expression as the right operand
85
+ // This handles cases where analyzer found the cursor but we need to adjust context
86
+ if (parseResult.ast) {
87
+ const ast = parseResult.ast as any;
88
+ if (ast.type === 'Binary' && ast.right && isCursorNode(ast.right)) {
89
+ cursorNode = ast.right;
90
+ // Get type from left side
91
+ typeBeforeCursor = ast.left?.typeInfo;
92
+
93
+ // Special case for navigation: cursor right after identifier
94
+ if (ast.left?.type === 'Binary' && ast.left.operator === '.') {
95
+ if (ast.left.right?.type === 'Identifier') {
96
+ const identifierEnd = ast.left.right.range?.end?.offset;
97
+
98
+ // If cursor is right after identifier with no space
99
+ if (identifierEnd === cursorPosition) {
100
+ // Check if this could be a partial identifier by seeing if there are
101
+ // any properties that would match if we treated it as a prefix
102
+ const partialText = extractPartialText(expression, cursorPosition);
103
+ if (partialText && modelProvider) {
104
+ // Try to get elements from the type before the identifier
105
+ const baseType = ast.left.left?.typeInfo;
106
+ if (baseType) {
107
+ const typeName = baseType.name || baseType.type;
108
+ // Skip if type is 'Any' as it's not a real FHIR type
109
+ if (typeName && typeName !== 'Any') {
110
+ const elements = await modelProvider.getElements(typeName);
111
+ if (elements.length > 0) {
112
+ // Check if any elements start with the partial text
113
+ const hasMatches = elements.some(p =>
114
+ p.name.toLowerCase().startsWith(partialText.toLowerCase()) &&
115
+ p.name.toLowerCase() !== partialText.toLowerCase()
116
+ );
117
+
118
+ if (hasMatches) {
119
+ // There are potential completions - treat as identifier context
120
+ cursorNode = {
121
+ ...cursorNode,
122
+ context: CursorContext.Identifier
123
+ } as any;
124
+ typeBeforeCursor = baseType;
125
+ }
126
+ }
127
+ }
128
+ }
129
+ }
130
+ }
131
+ }
132
+ }
133
+ }
134
+ }
135
+
136
+ // Check if cursor is in a function argument and fix the function name
137
+ if (cursorNode && cursorNode.context === CursorContext.Argument) {
138
+ const functionName = findFunctionName(parseResult.ast as any, cursorNode);
139
+ if (functionName) {
140
+ (cursorNode as any).functionName = functionName;
141
+
142
+ // Special case: ofType, as, is should treat their arguments as type context
143
+ if (functionName === 'ofType' || functionName === 'as' || functionName === 'is') {
144
+ // Convert to type cursor node
145
+ cursorNode = {
146
+ ...cursorNode,
147
+ context: CursorContext.Type,
148
+ typeOperator: functionName
149
+ } as any;
150
+ }
151
+ }
152
+ }
153
+
154
+ if (!cursorNode) {
155
+ // Fallback: provide general completions if no cursor node found
156
+ return getGeneralCompletions();
157
+ }
158
+
159
+ const expectedType = analysis.cursorContext?.expectedType;
160
+
161
+ // Get partial text for filtering
162
+ const partialText = extractPartialText(expression, cursorPosition);
163
+
164
+ // Generate completions based on cursor context
165
+ let completions: CompletionItem[] = [];
166
+
167
+ switch (cursorNode.context) {
168
+ case CursorContext.Identifier:
169
+ completions = await getIdentifierCompletions(typeBeforeCursor, modelProvider);
170
+ break;
171
+
172
+ case CursorContext.Operator:
173
+ completions = getOperatorCompletions(typeBeforeCursor);
174
+ break;
175
+
176
+ case CursorContext.Type:
177
+ completions = await getTypeCompletions(cursorNode, modelProvider);
178
+ break;
179
+
180
+ case CursorContext.Argument:
181
+ completions = await getArgumentCompletions(cursorNode, typeBeforeCursor, modelProvider, variables);
182
+ break;
183
+
184
+ case CursorContext.Index:
185
+ completions = getIndexCompletions(typeBeforeCursor, variables);
186
+ break;
187
+ }
188
+
189
+ // Filter by partial text
190
+ if (partialText) {
191
+ completions = filterCompletions(completions, partialText);
192
+ }
193
+
194
+ // Sort and limit
195
+ completions = rankCompletions(completions);
196
+ if (maxCompletions > 0 && completions.length > maxCompletions) {
197
+ completions = completions.slice(0, maxCompletions);
198
+ }
199
+
200
+ return completions;
201
+ } catch (error) {
202
+ // Return empty array on error
203
+ return [];
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Get general completions when no specific context
209
+ */
210
+ function getGeneralCompletions(): CompletionItem[] {
211
+ const completions: CompletionItem[] = [];
212
+
213
+ // Get operators from registry
214
+ const operatorNames = registry.listOperators();
215
+ for (const opName of operatorNames) {
216
+ const opDef = registry.getOperatorDefinition(opName);
217
+ if (opDef) {
218
+ completions.push({
219
+ label: opDef.symbol,
220
+ kind: CompletionKind.Operator,
221
+ detail: opDef.description || `${opDef.name} operator`
222
+ });
223
+ }
224
+ }
225
+
226
+ // No hardcoded constants - these should come from context
227
+
228
+ return completions;
229
+ }
230
+
231
+ /**
232
+ * Check if a function is applicable to a given type
233
+ */
234
+ function isFunctionApplicable(funcDef: any, typeInfo: TypeInfo): boolean {
235
+ if (!typeInfo || !typeInfo.type) return true;
236
+ // Pass type with collection info: append [] if not singleton
237
+ const typeForRegistry = typeInfo.singleton === false
238
+ ? `${typeInfo.type}[]`
239
+ : typeInfo.type;
240
+ return registry.isFunctionApplicableToType(funcDef.name, typeForRegistry);
241
+ }
242
+
243
+ /**
244
+ * Check if an operator is applicable to a given type
245
+ */
246
+ function isOperatorApplicable(opDef: any, typeInfo: TypeInfo): boolean {
247
+ if (!typeInfo || !typeInfo.type) return true;
248
+ return registry.isOperatorApplicableToType(opDef.symbol, typeInfo.type);
249
+ }
250
+
251
+ /**
252
+ * Get sort text for operator to ensure common operators appear first
253
+ */
254
+ function getSortTextForOperator(opDef: any): string {
255
+ // Common operators should appear first
256
+ const commonOps = ['.', '=', '!=', '<', '>', '<=', '>=', '+', '-', 'and', 'or'];
257
+ const index = commonOps.indexOf(opDef.symbol);
258
+ if (index >= 0) {
259
+ return `0${index.toString().padStart(2, '0')}_${opDef.symbol}`;
260
+ }
261
+ return `1_${opDef.symbol}`;
262
+ }
263
+
264
+ /**
265
+ * Find function name for a cursor node in an argument position
266
+ */
267
+ function findFunctionName(ast: any, cursorNode: any): string | null {
268
+ // Check if AST is a function with cursor in arguments
269
+ if (ast.type === 'Function' && ast.arguments?.includes(cursorNode)) {
270
+ return ast.name?.name || null;
271
+ }
272
+
273
+ // Check if AST is binary with function on right
274
+ if (ast.type === 'Binary' && ast.right?.type === 'Function') {
275
+ if (ast.right.arguments?.includes(cursorNode)) {
276
+ return ast.right.name?.name || null;
277
+ }
278
+ }
279
+
280
+ // Recursively check children
281
+ if (ast.left) {
282
+ const leftResult = findFunctionName(ast.left, cursorNode);
283
+ if (leftResult) return leftResult;
284
+ }
285
+
286
+ if (ast.right) {
287
+ const rightResult = findFunctionName(ast.right, cursorNode);
288
+ if (rightResult) return rightResult;
289
+ }
290
+
291
+ if (ast.arguments) {
292
+ for (const arg of ast.arguments) {
293
+ if (arg !== cursorNode) {
294
+ const argResult = findFunctionName(arg, cursorNode);
295
+ if (argResult) return argResult;
296
+ }
297
+ }
298
+ }
299
+
300
+ return null;
301
+ }
302
+
303
+ /**
304
+ * Extract partial text before cursor for filtering
305
+ */
306
+ function extractPartialText(expression: string, cursorPosition: number): string {
307
+ // Handle case where cursor is right after a dot
308
+ if (cursorPosition > 0 && expression[cursorPosition - 1] === '.') {
309
+ return '';
310
+ }
311
+
312
+ let start = cursorPosition;
313
+
314
+ // Move back to find start of identifier
315
+ while (start > 0) {
316
+ const char = expression[start - 1];
317
+ if (char && /[a-zA-Z0-9_$%]/.test(char)) {
318
+ start--;
319
+ } else {
320
+ break;
321
+ }
322
+ }
323
+
324
+ return expression.substring(start, cursorPosition);
325
+ }
326
+
327
+ /**
328
+ * Get completions for identifier context (after dot)
329
+ */
330
+ async function getIdentifierCompletions(
331
+ typeBeforeCursor?: TypeInfo,
332
+ modelProvider?: ModelProvider
333
+ ): Promise<CompletionItem[]> {
334
+ const completions: CompletionItem[] = [];
335
+
336
+ // Add elements from type if available
337
+ if (typeBeforeCursor && modelProvider) {
338
+ // Use the name property which contains the actual FHIR type name
339
+ // Skip if type is 'Any' as it's not a real FHIR type
340
+ const typeName = typeBeforeCursor.name || typeBeforeCursor.type;
341
+ if (typeName && typeName !== 'Any') {
342
+ const elements = await modelProvider.getElements(typeName);
343
+ if (elements.length > 0) {
344
+ for (const element of elements) {
345
+ completions.push({
346
+ label: element.name,
347
+ kind: CompletionKind.Property,
348
+ detail: element.type,
349
+ documentation: element.documentation
350
+ });
351
+ }
352
+ }
353
+ }
354
+ }
355
+
356
+ // Add FHIRPath functions from registry
357
+ const functionNames = registry.listFunctions();
358
+ for (const name of functionNames) {
359
+ const funcDef = registry.getFunction(name);
360
+ if (funcDef) {
361
+ // Check if function is appropriate for the current type context
362
+ const isApplicable = !typeBeforeCursor || isFunctionApplicable(funcDef, typeBeforeCursor);
363
+
364
+ if (isApplicable) {
365
+ // Determine if function takes parameters
366
+ const hasParams = funcDef.signatures?.[0]?.parameters && funcDef.signatures[0].parameters.length > 0;
367
+ const funcDescription = funcDef.description || `FHIRPath ${name} function`;
368
+
369
+ completions.push({
370
+ label: name,
371
+ kind: CompletionKind.Function,
372
+ detail: funcDescription,
373
+ insertText: name + (hasParams ? '()' : '()')
374
+ });
375
+ }
376
+ }
377
+ }
378
+
379
+ // Add type-specific functions from registry
380
+ if (typeBeforeCursor && typeBeforeCursor.type) {
381
+ // Pass type with collection info: append [] if not singleton
382
+ const typeForRegistry = typeBeforeCursor.singleton === false
383
+ ? `${typeBeforeCursor.type}[]`
384
+ : typeBeforeCursor.type;
385
+ const typeFunctions = registry.getFunctionsForType(typeForRegistry);
386
+ for (const func of typeFunctions) {
387
+ // Check if function is already added from general functions
388
+ if (!completions.some(c => c.label === func.name)) {
389
+ const hasParams = func.signatures?.[0]?.parameters && func.signatures[0].parameters.length > 0;
390
+ completions.push({
391
+ label: func.name,
392
+ kind: CompletionKind.Function,
393
+ detail: func.description || `FHIRPath ${func.name} function`,
394
+ insertText: func.name + (hasParams ? '()' : '()')
395
+ });
396
+ }
397
+ }
398
+ }
399
+
400
+ return completions;
401
+ }
402
+
403
+ /**
404
+ * Get completions for operator context (between expressions)
405
+ */
406
+ function getOperatorCompletions(typeBeforeCursor?: TypeInfo): CompletionItem[] {
407
+ const completions: CompletionItem[] = [];
408
+ const addedOperators = new Set<string>();
409
+
410
+ // Get all operators from registry
411
+ const operatorNames = registry.listOperators();
412
+
413
+ for (const opName of operatorNames) {
414
+ const opDef = registry.getOperatorDefinition(opName);
415
+ if (opDef) {
416
+ // Check if operator is applicable to the current type
417
+ const isApplicable = !typeBeforeCursor || isOperatorApplicable(opDef, typeBeforeCursor);
418
+
419
+ if (isApplicable && !addedOperators.has(opDef.symbol)) {
420
+ completions.push({
421
+ label: opDef.symbol,
422
+ kind: CompletionKind.Operator,
423
+ detail: opDef.description || `${opDef.name} operator`,
424
+ sortText: getSortTextForOperator(opDef)
425
+ });
426
+ addedOperators.add(opDef.symbol);
427
+ }
428
+ }
429
+ }
430
+
431
+ return completions;
432
+ }
433
+
434
+ /**
435
+ * Get completions for type context (after is/as/ofType)
436
+ */
437
+ async function getTypeCompletions(
438
+ cursorNode: AnyCursorNode,
439
+ modelProvider?: ModelProvider
440
+ ): Promise<CompletionItem[]> {
441
+ const completions: CompletionItem[] = [];
442
+
443
+ // Primitive types - only if modelProvider is available
444
+ if (modelProvider) {
445
+ const primitiveTypes = await modelProvider.getPrimitiveTypes();
446
+ for (const type of primitiveTypes) {
447
+ completions.push({
448
+ label: type,
449
+ kind: CompletionKind.Type,
450
+ detail: 'FHIRPath primitive type'
451
+ });
452
+ }
453
+
454
+ // Complex types
455
+ const complexTypes = await modelProvider.getComplexTypes();
456
+ for (const type of complexTypes) {
457
+ completions.push({
458
+ label: type,
459
+ kind: CompletionKind.Type,
460
+ detail: 'FHIR complex type'
461
+ });
462
+ }
463
+ }
464
+
465
+ // For ofType, add resource types
466
+ const typeOperator = (cursorNode as any).typeOperator;
467
+ if (typeOperator === 'ofType' && modelProvider) {
468
+ const resourceTypes = await modelProvider.getResourceTypes();
469
+ for (const type of resourceTypes) {
470
+ completions.push({
471
+ label: type,
472
+ kind: CompletionKind.Type,
473
+ detail: 'FHIR resource type'
474
+ });
475
+ }
476
+ }
477
+
478
+ return completions;
479
+ }
480
+
481
+ /**
482
+ * Get completions for argument context (in function arguments)
483
+ */
484
+ async function getArgumentCompletions(
485
+ cursorNode: AnyCursorNode,
486
+ typeBeforeCursor?: TypeInfo,
487
+ modelProvider?: ModelProvider,
488
+ variables?: Record<string, any>
489
+ ): Promise<CompletionItem[]> {
490
+ const completions: CompletionItem[] = [];
491
+ const argNode = cursorNode as any;
492
+
493
+ // Add user variables if available
494
+ if (variables) {
495
+ for (const varName of Object.keys(variables)) {
496
+ if (varName === '$this') {
497
+ completions.push({
498
+ label: '$this',
499
+ kind: CompletionKind.Variable,
500
+ detail: 'Current item in iteration'
501
+ });
502
+ } else if (varName === '$index') {
503
+ completions.push({
504
+ label: '$index',
505
+ kind: CompletionKind.Variable,
506
+ detail: 'Current index in iteration'
507
+ });
508
+ } else {
509
+ completions.push({
510
+ label: varName.startsWith('%') ? varName : '%' + varName,
511
+ kind: CompletionKind.Variable,
512
+ detail: 'User-defined variable'
513
+ });
514
+ }
515
+ }
516
+ }
517
+
518
+ // Add elements if in lambda context
519
+ const functionName = argNode.functionName;
520
+ // Check if the function accepts lambda expressions (could be determined from function signature in registry)
521
+ const lambdaFunctions = ['where', 'select', 'all', 'exists', 'any', 'repeat'];
522
+ if (functionName && lambdaFunctions.includes(functionName)) {
523
+ // In lambda function, provide elements of item type
524
+ if (typeBeforeCursor && modelProvider) {
525
+ const itemType = { ...typeBeforeCursor, singleton: true };
526
+ const typeName = itemType.name || itemType.type;
527
+ // Skip if type is 'Any' as it's not a real FHIR type
528
+ if (typeName && typeName !== 'Any') {
529
+ const elements = await modelProvider.getElements(typeName);
530
+ if (elements.length > 0) {
531
+ for (const element of elements) {
532
+ completions.push({
533
+ label: element.name,
534
+ kind: CompletionKind.Property,
535
+ detail: element.type
536
+ });
537
+ }
538
+ }
539
+ }
540
+ }
541
+ }
542
+
543
+ // No hardcoded constants - these should come from context or be typed by user
544
+
545
+ return completions;
546
+ }
547
+
548
+ /**
549
+ * Get completions for index context (in brackets)
550
+ */
551
+ function getIndexCompletions(
552
+ typeBeforeCursor?: TypeInfo,
553
+ variables?: Record<string, any>
554
+ ): CompletionItem[] {
555
+ const completions: CompletionItem[] = [];
556
+
557
+ // Add user variables if available
558
+ if (variables) {
559
+ // Add $index if it's in scope (should be provided by context)
560
+ if ('$index' in variables) {
561
+ completions.push({
562
+ label: '$index',
563
+ kind: CompletionKind.Variable,
564
+ detail: 'Current index'
565
+ });
566
+ }
567
+
568
+ // Add other user variables
569
+ for (const varName of Object.keys(variables)) {
570
+ if (!varName.startsWith('$')) {
571
+ completions.push({
572
+ label: varName.startsWith('%') ? varName : '%' + varName,
573
+ kind: CompletionKind.Variable,
574
+ detail: 'User-defined variable'
575
+ });
576
+ }
577
+ }
578
+ }
579
+
580
+ // Get index-related functions from registry
581
+ const functionNames = registry.listFunctions();
582
+ for (const name of functionNames) {
583
+ if (name === 'first' || name === 'last') {
584
+ const funcDef = registry.getFunction(name);
585
+ if (funcDef) {
586
+ completions.push({
587
+ label: name + '()',
588
+ kind: CompletionKind.Function,
589
+ detail: funcDef.description || `${name} function`,
590
+ insertText: name + '()'
591
+ });
592
+ }
593
+ }
594
+ }
595
+
596
+ return completions;
597
+ }
598
+
599
+
600
+ /**
601
+ * Filter completions by partial text
602
+ */
603
+ function filterCompletions(completions: CompletionItem[], partialText: string): CompletionItem[] {
604
+ const lowerPartial = partialText.toLowerCase();
605
+ return completions.filter(item =>
606
+ item.label.toLowerCase().startsWith(lowerPartial)
607
+ );
608
+ }
609
+
610
+ /**
611
+ * Rank completions by relevance
612
+ */
613
+ function rankCompletions(completions: CompletionItem[]): CompletionItem[] {
614
+ return completions.sort((a, b) => {
615
+ // Sort by kind priority
616
+ const kindPriority: Record<CompletionKind, number> = {
617
+ [CompletionKind.Property]: 1,
618
+ [CompletionKind.Variable]: 2,
619
+ [CompletionKind.Function]: 3,
620
+ [CompletionKind.Operator]: 4,
621
+ [CompletionKind.Type]: 5,
622
+ [CompletionKind.Keyword]: 6,
623
+ [CompletionKind.Constant]: 7
624
+ };
625
+
626
+ const aPriority = kindPriority[a.kind] || 10;
627
+ const bPriority = kindPriority[b.kind] || 10;
628
+
629
+ if (aPriority !== bPriority) {
630
+ return aPriority - bPriority;
631
+ }
632
+
633
+ // Then alphabetically
634
+ return a.label.localeCompare(b.label);
635
+ });
636
+ }