@atomic-ehr/fhirpath 0.0.2 → 0.0.3

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 (143) hide show
  1. package/README.md +716 -238
  2. package/dist/index.d.ts +225 -119
  3. package/dist/index.js +10911 -5600
  4. package/dist/index.js.map +1 -1
  5. package/package.json +9 -4
  6. package/src/analyzer/augmentor.ts +242 -0
  7. package/src/analyzer/cursor-services.ts +75 -0
  8. package/src/analyzer/scope-manager.ts +57 -0
  9. package/src/analyzer/trivia-indexer.ts +58 -0
  10. package/src/analyzer/type-compat.ts +157 -0
  11. package/src/analyzer/utils.ts +132 -0
  12. package/src/analyzer.ts +921 -1208
  13. package/src/completion-provider.ts +209 -191
  14. package/src/{quantity-value.ts → complex-types/quantity-value.ts} +112 -22
  15. package/src/complex-types/temporal.ts +1737 -0
  16. package/src/errors.ts +25 -3
  17. package/src/index.ts +17 -104
  18. package/src/inspect.ts +4 -4
  19. package/src/{boxing.ts → interpreter/boxing.ts} +1 -1
  20. package/src/interpreter/navigator.ts +94 -0
  21. package/src/interpreter/runtime-context.ts +273 -0
  22. package/src/interpreter.ts +435 -469
  23. package/src/lexer.ts +188 -210
  24. package/src/model-provider.ts +71 -43
  25. package/src/operations/abs-function.ts +1 -1
  26. package/src/operations/aggregate-function.ts +84 -5
  27. package/src/operations/all-function.ts +4 -3
  28. package/src/operations/allFalse-function.ts +2 -1
  29. package/src/operations/allTrue-function.ts +2 -1
  30. package/src/operations/and-operator.ts +2 -1
  31. package/src/operations/anyFalse-function.ts +2 -1
  32. package/src/operations/anyTrue-function.ts +2 -1
  33. package/src/operations/as-function.ts +58 -0
  34. package/src/operations/as-operator.ts +57 -19
  35. package/src/operations/ceiling-function.ts +1 -1
  36. package/src/operations/children-function.ts +14 -5
  37. package/src/operations/combine-function.ts +6 -3
  38. package/src/operations/combine-operator.ts +6 -7
  39. package/src/operations/comparison.ts +692 -0
  40. package/src/operations/contains-function.ts +1 -1
  41. package/src/operations/contains-operator.ts +2 -1
  42. package/src/operations/convertsToBoolean-function.ts +78 -0
  43. package/src/operations/convertsToDecimal-function.ts +82 -0
  44. package/src/operations/convertsToInteger-function.ts +71 -0
  45. package/src/operations/convertsToLong-function.ts +89 -0
  46. package/src/operations/convertsToQuantity-function.ts +116 -0
  47. package/src/operations/convertsToString-function.ts +88 -0
  48. package/src/operations/count-function.ts +2 -1
  49. package/src/operations/dateOf-function.ts +69 -0
  50. package/src/operations/dayOf-function.ts +66 -0
  51. package/src/operations/decimal-boundaries.ts +133 -0
  52. package/src/operations/defineVariable-function.ts +130 -17
  53. package/src/operations/distinct-function.ts +1 -1
  54. package/src/operations/div-operator.ts +1 -1
  55. package/src/operations/divide-operator.ts +12 -7
  56. package/src/operations/dot-operator.ts +1 -1
  57. package/src/operations/empty-function.ts +30 -21
  58. package/src/operations/endsWith-function.ts +6 -1
  59. package/src/operations/equal-operator.ts +23 -32
  60. package/src/operations/equivalent-operator.ts +13 -53
  61. package/src/operations/exclude-function.ts +2 -1
  62. package/src/operations/exists-function.ts +4 -3
  63. package/src/operations/first-function.ts +1 -1
  64. package/src/operations/floor-function.ts +1 -1
  65. package/src/operations/greater-operator.ts +20 -3
  66. package/src/operations/greater-or-equal-operator.ts +20 -3
  67. package/src/operations/highBoundary-function.ts +120 -0
  68. package/src/operations/hourOf-function.ts +66 -0
  69. package/src/operations/iif-function.ts +186 -7
  70. package/src/operations/implies-operator.ts +1 -1
  71. package/src/operations/in-operator.ts +2 -1
  72. package/src/operations/index.ts +41 -0
  73. package/src/operations/indexOf-function.ts +1 -1
  74. package/src/operations/intersect-function.ts +1 -1
  75. package/src/operations/is-function.ts +59 -0
  76. package/src/operations/is-operator.ts +20 -9
  77. package/src/operations/isDistinct-function.ts +2 -1
  78. package/src/operations/join-function.ts +1 -1
  79. package/src/operations/last-function.ts +1 -1
  80. package/src/operations/lastIndexOf-function.ts +85 -0
  81. package/src/operations/length-function.ts +1 -1
  82. package/src/operations/less-operator.ts +20 -3
  83. package/src/operations/less-or-equal-operator.ts +20 -3
  84. package/src/operations/less-than.ts +2 -2
  85. package/src/operations/lowBoundary-function.ts +120 -0
  86. package/src/operations/lower-function.ts +1 -1
  87. package/src/operations/matches-function.ts +86 -0
  88. package/src/operations/matchesFull-function.ts +96 -0
  89. package/src/operations/millisecondOf-function.ts +66 -0
  90. package/src/operations/minus-operator.ts +69 -4
  91. package/src/operations/minuteOf-function.ts +66 -0
  92. package/src/operations/mod-operator.ts +1 -1
  93. package/src/operations/monthOf-function.ts +66 -0
  94. package/src/operations/multiply-operator.ts +27 -3
  95. package/src/operations/not-equal-operator.ts +24 -30
  96. package/src/operations/not-equivalent-operator.ts +13 -53
  97. package/src/operations/not-function.ts +1 -1
  98. package/src/operations/ofType-function.ts +8 -12
  99. package/src/operations/or-operator.ts +2 -1
  100. package/src/operations/plus-operator.ts +71 -7
  101. package/src/operations/power-function.ts +35 -10
  102. package/src/operations/repeat-function.ts +169 -0
  103. package/src/operations/replace-function.ts +1 -1
  104. package/src/operations/replaceMatches-function.ts +120 -0
  105. package/src/operations/round-function.ts +1 -1
  106. package/src/operations/secondOf-function.ts +66 -0
  107. package/src/operations/select-function.ts +66 -5
  108. package/src/operations/single-function.ts +1 -1
  109. package/src/operations/skip-function.ts +1 -1
  110. package/src/operations/split-function.ts +1 -1
  111. package/src/operations/sqrt-function.ts +15 -8
  112. package/src/operations/startsWith-function.ts +1 -1
  113. package/src/operations/subsetOf-function.ts +6 -2
  114. package/src/operations/substring-function.ts +1 -1
  115. package/src/operations/supersetOf-function.ts +6 -2
  116. package/src/operations/tail-function.ts +1 -1
  117. package/src/operations/take-function.ts +1 -1
  118. package/src/operations/temporal-functions.ts +555 -0
  119. package/src/operations/timeOf-function.ts +67 -0
  120. package/src/operations/timezoneOffsetOf-function.ts +69 -0
  121. package/src/operations/toBoolean-function.ts +27 -8
  122. package/src/operations/toChars-function.ts +56 -0
  123. package/src/operations/toDecimal-function.ts +27 -8
  124. package/src/operations/toInteger-function.ts +15 -3
  125. package/src/operations/toLong-function.ts +98 -0
  126. package/src/operations/toQuantity-function.ts +181 -0
  127. package/src/operations/toString-function.ts +45 -3
  128. package/src/operations/trace-function.ts +1 -1
  129. package/src/operations/trim-function.ts +1 -1
  130. package/src/operations/truncate-function.ts +1 -1
  131. package/src/operations/unary-minus-operator.ts +2 -2
  132. package/src/operations/unary-plus-operator.ts +1 -1
  133. package/src/operations/union-function.ts +1 -1
  134. package/src/operations/union-operator.ts +16 -26
  135. package/src/operations/upper-function.ts +1 -1
  136. package/src/operations/where-function.ts +3 -3
  137. package/src/operations/xor-operator.ts +1 -1
  138. package/src/operations/yearOf-function.ts +66 -0
  139. package/src/{cursor-nodes.ts → parser/cursor-nodes.ts} +10 -7
  140. package/src/parser.ts +248 -501
  141. package/src/registry.ts +53 -42
  142. package/src/types.ts +128 -16
  143. package/src/utils/pprint.ts +151 -0
@@ -2,10 +2,55 @@ import { parse } from './parser';
2
2
  import { Analyzer } from './analyzer';
3
3
  import type { TypeInfo, ModelProvider } from './types';
4
4
 
5
- import { CursorContext, isCursorNode } from './cursor-nodes';
6
- import type { AnyCursorNode } from './cursor-nodes';
5
+ import { CursorContext, isCursorNode } from './parser/cursor-nodes';
6
+ import type { AnyCursorNode } from './parser/cursor-nodes';
7
7
  import { registry } from './registry';
8
8
 
9
+ /**
10
+ * Type guard for ErrorNode
11
+ */
12
+ function isErrorNode(node: any): node is import('./types').ErrorNode {
13
+ return node.type === 'Error';
14
+ }
15
+
16
+ /**
17
+ * Find the cursor node in the AST
18
+ */
19
+ function findCursorNode(node: import('./types').ASTNode): AnyCursorNode | undefined {
20
+ if (isCursorNode(node)) {
21
+ return node as AnyCursorNode;
22
+ }
23
+
24
+ switch (node.type) {
25
+ case 'Binary':
26
+ return findCursorNode(node.left) || findCursorNode(node.right);
27
+ case 'Unary':
28
+ return findCursorNode(node.operand);
29
+ case 'Function':
30
+ // It's more likely for the cursor to be in the arguments, so check them first.
31
+ for (const arg of node.arguments) {
32
+ const found = findCursorNode(arg);
33
+ if (found) return found;
34
+ }
35
+ return findCursorNode(node.name);
36
+ case 'Index':
37
+ return findCursorNode(node.expression) || findCursorNode(node.index);
38
+ case 'Collection':
39
+ for (const element of node.elements) {
40
+ const found = findCursorNode(element);
41
+ if (found) return found;
42
+ }
43
+ break;
44
+ case 'TypeCast':
45
+ return findCursorNode((node as any).expression);
46
+ case 'MembershipTest':
47
+ return findCursorNode((node as any).expression);
48
+ }
49
+
50
+ return undefined;
51
+ }
52
+
53
+
9
54
  /**
10
55
  * Kind of completion item
11
56
  */
@@ -33,7 +78,7 @@ export interface CompletionItem {
33
78
  documentation?: string;
34
79
  /** Text to insert (if different from label) */
35
80
  insertText?: string;
36
- /** Sort order */
81
+ /** Sort text for ordering (if different from label) */
37
82
  sortText?: string;
38
83
  }
39
84
 
@@ -63,10 +108,19 @@ export async function provideCompletions(
63
108
 
64
109
  try {
65
110
  // Parse with cursor
66
- const parseResult = parse(expression, { cursorPosition });
111
+ const parseResult = parse(expression, {
112
+ mode: 'lsp',
113
+ cursorPosition,
114
+ });
67
115
  if (!parseResult.ast) {
68
116
  return [];
69
117
  }
118
+
119
+ // Find the cursor node
120
+ const cursorNode = findCursorNode(parseResult.ast);
121
+ if (!cursorNode || isErrorNode(cursorNode)) {
122
+ return [];
123
+ }
70
124
 
71
125
  // Analyze with cursor mode
72
126
  const analyzer = new Analyzer(modelProvider);
@@ -78,89 +132,22 @@ export async function provideCompletions(
78
132
  );
79
133
 
80
134
  // Extract cursor context
81
- let cursorNode = analysis.cursorContext?.cursorNode;
82
- let typeBeforeCursor = analysis.cursorContext?.typeBeforeCursor;
135
+ const { typeBeforeCursor, functionCall } = analysis.cursorContext || {};
83
136
 
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
- }
137
+ // Get partial text for filtering
138
+ // Determine partial text for filtering (prefer AST-provided, else infer from source)
139
+ let partialText = (cursorNode as any).partialText || '';
140
+ if (!partialText && cursorNode.context === CursorContext.Type) {
141
+ // Heuristic: scan left from cursor to preceding '(' or whitespace and take the identifier fragment
142
+ const cp = cursorPosition;
143
+ const left = expression.slice(0, cp);
144
+ // Take the trailing run of identifier characters
145
+ const m = left.match(/[A-Za-z][A-Za-z0-9.]*$/);
146
+ if (m) {
147
+ partialText = m[0];
151
148
  }
152
149
  }
153
150
 
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
151
  // Generate completions based on cursor context
165
152
  let completions: CompletionItem[] = [];
166
153
 
@@ -170,7 +157,13 @@ export async function provideCompletions(
170
157
  break;
171
158
 
172
159
  case CursorContext.Operator:
173
- completions = getOperatorCompletions(typeBeforeCursor);
160
+ // Fallback to provided inputType if analyzer did not provide typeBeforeCursor
161
+ completions = getOperatorCompletions(typeBeforeCursor || (inputType as any));
162
+ // Post-filter: when left side is a collection, exclude operators that require singleton left (e.g., 'in')
163
+ const leftType = (typeBeforeCursor || (inputType as any));
164
+ if (leftType && leftType.singleton === false) {
165
+ completions = completions.filter(c => c.label !== 'in');
166
+ }
174
167
  break;
175
168
 
176
169
  case CursorContext.Type:
@@ -178,7 +171,7 @@ export async function provideCompletions(
178
171
  break;
179
172
 
180
173
  case CursorContext.Argument:
181
- completions = await getArgumentCompletions(cursorNode, typeBeforeCursor, modelProvider, variables);
174
+ completions = await getArgumentCompletions(cursorNode, typeBeforeCursor, modelProvider, variables, functionCall);
182
175
  break;
183
176
 
184
177
  case CursorContext.Index:
@@ -245,7 +238,16 @@ function isFunctionApplicable(funcDef: any, typeInfo: TypeInfo): boolean {
245
238
  */
246
239
  function isOperatorApplicable(opDef: any, typeInfo: TypeInfo): boolean {
247
240
  if (!typeInfo || !typeInfo.type) return true;
248
- return registry.isOperatorApplicableToType(opDef.symbol, typeInfo.type);
241
+ const isCollection = typeInfo.singleton === false;
242
+ // If left operand must be singleton for all signatures, do not suggest for collections
243
+ if (isCollection && opDef.signatures && opDef.signatures.length > 0) {
244
+ const allowsCollection = opDef.signatures.some((s: any) => s.left && s.left.singleton === false);
245
+ if (!allowsCollection) {
246
+ return false;
247
+ }
248
+ }
249
+ const typeForRegistry = isCollection ? `${typeInfo.type}[]` : typeInfo.type;
250
+ return registry.isOperatorApplicableToType(opDef.symbol, typeForRegistry);
249
251
  }
250
252
 
251
253
  /**
@@ -261,69 +263,6 @@ function getSortTextForOperator(opDef: any): string {
261
263
  return `1_${opDef.symbol}`;
262
264
  }
263
265
 
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
266
  /**
328
267
  * Get completions for identifier context (after dot)
329
268
  */
@@ -362,15 +301,15 @@ async function getIdentifierCompletions(
362
301
  const isApplicable = !typeBeforeCursor || isFunctionApplicable(funcDef, typeBeforeCursor);
363
302
 
364
303
  if (isApplicable) {
365
- // Determine if function takes parameters
366
- const hasParams = funcDef.signatures?.[0]?.parameters && funcDef.signatures[0].parameters.length > 0;
304
+ // Determine if any signature takes parameters
305
+ const hasParams = funcDef.signatures?.some(sig => (sig.parameters?.length ?? 0) > 0) ?? false;
367
306
  const funcDescription = funcDef.description || `FHIRPath ${name} function`;
368
307
 
369
308
  completions.push({
370
309
  label: name,
371
310
  kind: CompletionKind.Function,
372
311
  detail: funcDescription,
373
- insertText: name + (hasParams ? '()' : '()')
312
+ insertText: name + (hasParams ? '()' : '')
374
313
  });
375
314
  }
376
315
  }
@@ -386,12 +325,12 @@ async function getIdentifierCompletions(
386
325
  for (const func of typeFunctions) {
387
326
  // Check if function is already added from general functions
388
327
  if (!completions.some(c => c.label === func.name)) {
389
- const hasParams = func.signatures?.[0]?.parameters && func.signatures[0].parameters.length > 0;
328
+ const hasParams = func.signatures?.some(sig => (sig.parameters?.length ?? 0) > 0) ?? false;
390
329
  completions.push({
391
330
  label: func.name,
392
331
  kind: CompletionKind.Function,
393
332
  detail: func.description || `FHIRPath ${func.name} function`,
394
- insertText: func.name + (hasParams ? '()' : '()')
333
+ insertText: func.name + (hasParams ? '()' : '')
395
334
  });
396
335
  }
397
336
  }
@@ -413,6 +352,13 @@ function getOperatorCompletions(typeBeforeCursor?: TypeInfo): CompletionItem[] {
413
352
  for (const opName of operatorNames) {
414
353
  const opDef = registry.getOperatorDefinition(opName);
415
354
  if (opDef) {
355
+ // If we clearly have a collection to the left and no signature allows a collection left operand, skip early
356
+ if (typeBeforeCursor && typeBeforeCursor.singleton === false && opDef.signatures && opDef.signatures.length > 0) {
357
+ const allowsCollection = opDef.signatures.some((s: any) => s.left && s.left.singleton === false);
358
+ if (!allowsCollection) {
359
+ continue;
360
+ }
361
+ }
416
362
  // Check if operator is applicable to the current type
417
363
  const isApplicable = !typeBeforeCursor || isOperatorApplicable(opDef, typeBeforeCursor);
418
364
 
@@ -447,7 +393,8 @@ async function getTypeCompletions(
447
393
  completions.push({
448
394
  label: type,
449
395
  kind: CompletionKind.Type,
450
- detail: 'FHIRPath primitive type'
396
+ detail: 'FHIRPath primitive type',
397
+ sortText: `0_${type}` // Priority 0 for primitives
451
398
  });
452
399
  }
453
400
 
@@ -457,20 +404,22 @@ async function getTypeCompletions(
457
404
  completions.push({
458
405
  label: type,
459
406
  kind: CompletionKind.Type,
460
- detail: 'FHIR complex type'
407
+ detail: 'FHIR complex type',
408
+ sortText: `1_${type}` // Priority 1 for complex types
461
409
  });
462
410
  }
463
411
  }
464
412
 
465
- // For ofType, add resource types
466
- const typeOperator = (cursorNode as any).typeOperator;
467
- if (typeOperator === 'ofType' && modelProvider) {
413
+ // Always add resource types when we have a model provider
414
+ // They're valid type names in contexts like ofType()
415
+ if (modelProvider) {
468
416
  const resourceTypes = await modelProvider.getResourceTypes();
469
417
  for (const type of resourceTypes) {
470
418
  completions.push({
471
419
  label: type,
472
420
  kind: CompletionKind.Type,
473
- detail: 'FHIR resource type'
421
+ detail: 'FHIR resource type',
422
+ sortText: `2_${type}` // Priority 2 for resource types
474
423
  });
475
424
  }
476
425
  }
@@ -485,12 +434,91 @@ async function getArgumentCompletions(
485
434
  cursorNode: AnyCursorNode,
486
435
  typeBeforeCursor?: TypeInfo,
487
436
  modelProvider?: ModelProvider,
488
- variables?: Record<string, any>
437
+ variables?: Record<string, any>,
438
+ functionCall?: {
439
+ definition: import('./types').FunctionDefinition;
440
+ argumentIndex: number;
441
+ }
489
442
  ): Promise<CompletionItem[]> {
443
+ // Inspect all overloads to determine parameter kinds for this argument position
444
+ if (functionCall?.definition && functionCall.argumentIndex >= 0) {
445
+ const argIndex = functionCall.argumentIndex;
446
+ let expectsTypeReference = false;
447
+ let expectsLambda = false;
448
+
449
+ for (const sig of functionCall.definition.signatures) {
450
+ const param = sig.parameters[argIndex];
451
+ if (!param) {
452
+ continue;
453
+ }
454
+ if (param.typeReference) {
455
+ expectsTypeReference = true;
456
+ }
457
+ if (param.expression) {
458
+ expectsLambda = true;
459
+ }
460
+ }
461
+
462
+ if (expectsTypeReference) {
463
+ // This parameter expects a type name, provide type completions
464
+ return getTypeCompletions(cursorNode, modelProvider);
465
+ }
466
+
467
+ // If lambda is expected, suggest element properties from the item type below
468
+ // (continue to build completions; variables will also be added)
469
+ const completions: CompletionItem[] = [];
470
+
471
+ // Add user variables if available
472
+ if (variables) {
473
+ for (const varName of Object.keys(variables)) {
474
+ if (varName === '$this') {
475
+ completions.push({
476
+ label: '$this',
477
+ kind: CompletionKind.Variable,
478
+ detail: 'Current item in iteration'
479
+ });
480
+ } else if (varName === '$index') {
481
+ completions.push({
482
+ label: '$index',
483
+ kind: CompletionKind.Variable,
484
+ detail: 'Current index in iteration'
485
+ });
486
+ } else {
487
+ completions.push({
488
+ label: varName.startsWith('%') ? varName : '%' + varName,
489
+ kind: CompletionKind.Variable,
490
+ detail: 'User-defined variable'
491
+ });
492
+ }
493
+ }
494
+ }
495
+
496
+ if (expectsLambda && typeBeforeCursor && modelProvider) {
497
+ const itemType = { ...typeBeforeCursor, singleton: true };
498
+ const typeName = itemType.name || itemType.type;
499
+ if (typeName && typeName !== 'Any') {
500
+ const elements = await modelProvider.getElements(typeName);
501
+ if (elements.length > 0) {
502
+ for (const element of elements) {
503
+ completions.push({
504
+ label: element.name,
505
+ kind: CompletionKind.Property,
506
+ detail: element.type
507
+ });
508
+ }
509
+ }
510
+ }
511
+ }
512
+
513
+ // If overloads do not indicate typeRef or lambda, fall through to variables-only completions below
514
+ if (completions.length > 0) {
515
+ return completions;
516
+ }
517
+ }
490
518
  const completions: CompletionItem[] = [];
491
519
  const argNode = cursorNode as any;
492
-
493
- // Add user variables if available
520
+
521
+ // Default: suggest variables in scope
494
522
  if (variables) {
495
523
  for (const varName of Object.keys(variables)) {
496
524
  if (varName === '$this') {
@@ -514,34 +542,8 @@ async function getArgumentCompletions(
514
542
  }
515
543
  }
516
544
  }
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
-
545
+
543
546
  // No hardcoded constants - these should come from context or be typed by user
544
-
545
547
  return completions;
546
548
  }
547
549
 
@@ -556,6 +558,15 @@ function getIndexCompletions(
556
558
 
557
559
  // Add user variables if available
558
560
  if (variables) {
561
+ // Add $this if it's in scope (for consistency with argument context)
562
+ if ('$this' in variables) {
563
+ completions.push({
564
+ label: '$this',
565
+ kind: CompletionKind.Variable,
566
+ detail: 'Current item'
567
+ });
568
+ }
569
+
559
570
  // Add $index if it's in scope (should be provided by context)
560
571
  if ('$index' in variables) {
561
572
  completions.push({
@@ -612,6 +623,13 @@ function filterCompletions(completions: CompletionItem[], partialText: string):
612
623
  */
613
624
  function rankCompletions(completions: CompletionItem[]): CompletionItem[] {
614
625
  return completions.sort((a, b) => {
626
+ // First check if items have sortText
627
+ if (a.sortText || b.sortText) {
628
+ const aSortText = a.sortText || a.label;
629
+ const bSortText = b.sortText || b.label;
630
+ return aSortText.localeCompare(bSortText);
631
+ }
632
+
615
633
  // Sort by kind priority
616
634
  const kindPriority: Record<CompletionKind, number> = {
617
635
  [CompletionKind.Property]: 1,
@@ -633,4 +651,4 @@ function rankCompletions(completions: CompletionItem[]): CompletionItem[] {
633
651
  // Then alphabetically
634
652
  return a.label.localeCompare(b.label);
635
653
  });
636
- }
654
+ }