@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.
- package/README.md +716 -238
- package/dist/index.d.ts +225 -119
- package/dist/index.js +10911 -5600
- package/dist/index.js.map +1 -1
- package/package.json +9 -4
- package/src/analyzer/augmentor.ts +242 -0
- package/src/analyzer/cursor-services.ts +75 -0
- package/src/analyzer/scope-manager.ts +57 -0
- package/src/analyzer/trivia-indexer.ts +58 -0
- package/src/analyzer/type-compat.ts +157 -0
- package/src/analyzer/utils.ts +132 -0
- package/src/analyzer.ts +921 -1208
- package/src/completion-provider.ts +209 -191
- package/src/{quantity-value.ts → complex-types/quantity-value.ts} +112 -22
- package/src/complex-types/temporal.ts +1737 -0
- package/src/errors.ts +25 -3
- package/src/index.ts +17 -104
- package/src/inspect.ts +4 -4
- package/src/{boxing.ts → interpreter/boxing.ts} +1 -1
- package/src/interpreter/navigator.ts +94 -0
- package/src/interpreter/runtime-context.ts +273 -0
- package/src/interpreter.ts +435 -469
- package/src/lexer.ts +188 -210
- package/src/model-provider.ts +71 -43
- package/src/operations/abs-function.ts +1 -1
- package/src/operations/aggregate-function.ts +84 -5
- package/src/operations/all-function.ts +4 -3
- package/src/operations/allFalse-function.ts +2 -1
- package/src/operations/allTrue-function.ts +2 -1
- package/src/operations/and-operator.ts +2 -1
- package/src/operations/anyFalse-function.ts +2 -1
- package/src/operations/anyTrue-function.ts +2 -1
- package/src/operations/as-function.ts +58 -0
- package/src/operations/as-operator.ts +57 -19
- package/src/operations/ceiling-function.ts +1 -1
- package/src/operations/children-function.ts +14 -5
- package/src/operations/combine-function.ts +6 -3
- package/src/operations/combine-operator.ts +6 -7
- package/src/operations/comparison.ts +692 -0
- package/src/operations/contains-function.ts +1 -1
- package/src/operations/contains-operator.ts +2 -1
- package/src/operations/convertsToBoolean-function.ts +78 -0
- package/src/operations/convertsToDecimal-function.ts +82 -0
- package/src/operations/convertsToInteger-function.ts +71 -0
- package/src/operations/convertsToLong-function.ts +89 -0
- package/src/operations/convertsToQuantity-function.ts +116 -0
- package/src/operations/convertsToString-function.ts +88 -0
- package/src/operations/count-function.ts +2 -1
- package/src/operations/dateOf-function.ts +69 -0
- package/src/operations/dayOf-function.ts +66 -0
- package/src/operations/decimal-boundaries.ts +133 -0
- package/src/operations/defineVariable-function.ts +130 -17
- package/src/operations/distinct-function.ts +1 -1
- package/src/operations/div-operator.ts +1 -1
- package/src/operations/divide-operator.ts +12 -7
- package/src/operations/dot-operator.ts +1 -1
- package/src/operations/empty-function.ts +30 -21
- package/src/operations/endsWith-function.ts +6 -1
- package/src/operations/equal-operator.ts +23 -32
- package/src/operations/equivalent-operator.ts +13 -53
- package/src/operations/exclude-function.ts +2 -1
- package/src/operations/exists-function.ts +4 -3
- package/src/operations/first-function.ts +1 -1
- package/src/operations/floor-function.ts +1 -1
- package/src/operations/greater-operator.ts +20 -3
- package/src/operations/greater-or-equal-operator.ts +20 -3
- package/src/operations/highBoundary-function.ts +120 -0
- package/src/operations/hourOf-function.ts +66 -0
- package/src/operations/iif-function.ts +186 -7
- package/src/operations/implies-operator.ts +1 -1
- package/src/operations/in-operator.ts +2 -1
- package/src/operations/index.ts +41 -0
- package/src/operations/indexOf-function.ts +1 -1
- package/src/operations/intersect-function.ts +1 -1
- package/src/operations/is-function.ts +59 -0
- package/src/operations/is-operator.ts +20 -9
- package/src/operations/isDistinct-function.ts +2 -1
- package/src/operations/join-function.ts +1 -1
- package/src/operations/last-function.ts +1 -1
- package/src/operations/lastIndexOf-function.ts +85 -0
- package/src/operations/length-function.ts +1 -1
- package/src/operations/less-operator.ts +20 -3
- package/src/operations/less-or-equal-operator.ts +20 -3
- package/src/operations/less-than.ts +2 -2
- package/src/operations/lowBoundary-function.ts +120 -0
- package/src/operations/lower-function.ts +1 -1
- package/src/operations/matches-function.ts +86 -0
- package/src/operations/matchesFull-function.ts +96 -0
- package/src/operations/millisecondOf-function.ts +66 -0
- package/src/operations/minus-operator.ts +69 -4
- package/src/operations/minuteOf-function.ts +66 -0
- package/src/operations/mod-operator.ts +1 -1
- package/src/operations/monthOf-function.ts +66 -0
- package/src/operations/multiply-operator.ts +27 -3
- package/src/operations/not-equal-operator.ts +24 -30
- package/src/operations/not-equivalent-operator.ts +13 -53
- package/src/operations/not-function.ts +1 -1
- package/src/operations/ofType-function.ts +8 -12
- package/src/operations/or-operator.ts +2 -1
- package/src/operations/plus-operator.ts +71 -7
- package/src/operations/power-function.ts +35 -10
- package/src/operations/repeat-function.ts +169 -0
- package/src/operations/replace-function.ts +1 -1
- package/src/operations/replaceMatches-function.ts +120 -0
- package/src/operations/round-function.ts +1 -1
- package/src/operations/secondOf-function.ts +66 -0
- package/src/operations/select-function.ts +66 -5
- package/src/operations/single-function.ts +1 -1
- package/src/operations/skip-function.ts +1 -1
- package/src/operations/split-function.ts +1 -1
- package/src/operations/sqrt-function.ts +15 -8
- package/src/operations/startsWith-function.ts +1 -1
- package/src/operations/subsetOf-function.ts +6 -2
- package/src/operations/substring-function.ts +1 -1
- package/src/operations/supersetOf-function.ts +6 -2
- package/src/operations/tail-function.ts +1 -1
- package/src/operations/take-function.ts +1 -1
- package/src/operations/temporal-functions.ts +555 -0
- package/src/operations/timeOf-function.ts +67 -0
- package/src/operations/timezoneOffsetOf-function.ts +69 -0
- package/src/operations/toBoolean-function.ts +27 -8
- package/src/operations/toChars-function.ts +56 -0
- package/src/operations/toDecimal-function.ts +27 -8
- package/src/operations/toInteger-function.ts +15 -3
- package/src/operations/toLong-function.ts +98 -0
- package/src/operations/toQuantity-function.ts +181 -0
- package/src/operations/toString-function.ts +45 -3
- package/src/operations/trace-function.ts +1 -1
- package/src/operations/trim-function.ts +1 -1
- package/src/operations/truncate-function.ts +1 -1
- package/src/operations/unary-minus-operator.ts +2 -2
- package/src/operations/unary-plus-operator.ts +1 -1
- package/src/operations/union-function.ts +1 -1
- package/src/operations/union-operator.ts +16 -26
- package/src/operations/upper-function.ts +1 -1
- package/src/operations/where-function.ts +3 -3
- package/src/operations/xor-operator.ts +1 -1
- package/src/operations/yearOf-function.ts +66 -0
- package/src/{cursor-nodes.ts → parser/cursor-nodes.ts} +10 -7
- package/src/parser.ts +248 -501
- package/src/registry.ts +53 -42
- package/src/types.ts +128 -16
- 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
|
|
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, {
|
|
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
|
-
|
|
82
|
-
let typeBeforeCursor = analysis.cursorContext?.typeBeforeCursor;
|
|
135
|
+
const { typeBeforeCursor, functionCall } = analysis.cursorContext || {};
|
|
83
136
|
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
366
|
-
const hasParams = funcDef.signatures?.
|
|
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?.
|
|
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
|
-
//
|
|
466
|
-
|
|
467
|
-
if (
|
|
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
|
-
//
|
|
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
|
+
}
|