@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.
- package/README.md +62 -56
- package/dist/index.d.ts +368 -99
- package/dist/index.js +3623 -1253
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/analyzer.ts +779 -120
- package/src/boxing.ts +124 -0
- package/src/completion-provider.ts +636 -0
- package/src/cursor-nodes.ts +111 -0
- package/src/errors.ts +247 -0
- package/src/index.ts +97 -14
- package/src/inspect.ts +309 -176
- package/src/interpreter.ts +380 -71
- package/src/lexer.ts +1 -0
- package/src/model-provider.ts +282 -39
- package/src/operations/abs-function.ts +40 -16
- package/src/operations/aggregate-function.ts +21 -10
- package/src/operations/all-function.ts +23 -11
- package/src/operations/allFalse-function.ts +14 -9
- package/src/operations/allTrue-function.ts +14 -9
- package/src/operations/and-operator.ts +21 -12
- package/src/operations/anyFalse-function.ts +11 -7
- package/src/operations/anyTrue-function.ts +11 -7
- package/src/operations/as-operator.ts +2 -1
- package/src/operations/ceiling-function.ts +14 -8
- package/src/operations/children-function.ts +98 -0
- package/src/operations/combine-function.ts +10 -6
- package/src/operations/combine-operator.ts +19 -5
- package/src/operations/contains-function.ts +26 -12
- package/src/operations/contains-operator.ts +15 -7
- package/src/operations/count-function.ts +7 -4
- package/src/operations/defineVariable-function.ts +11 -7
- package/src/operations/descendants-function.ts +64 -0
- package/src/operations/distinct-function.ts +21 -7
- package/src/operations/div-operator.ts +16 -3
- package/src/operations/divide-operator.ts +11 -6
- package/src/operations/dot-operator.ts +4 -2
- package/src/operations/empty-function.ts +7 -4
- package/src/operations/endsWith-function.ts +28 -14
- package/src/operations/equal-operator.ts +19 -7
- package/src/operations/equivalent-operator.ts +16 -11
- package/src/operations/exclude-function.ts +18 -14
- package/src/operations/exists-function.ts +19 -10
- package/src/operations/first-function.ts +12 -7
- package/src/operations/floor-function.ts +14 -8
- package/src/operations/greater-operator.ts +10 -5
- package/src/operations/greater-or-equal-operator.ts +10 -5
- package/src/operations/iif-function.ts +27 -23
- package/src/operations/implies-operator.ts +23 -8
- package/src/operations/in-operator.ts +18 -8
- package/src/operations/index.ts +3 -0
- package/src/operations/indexOf-function.ts +28 -14
- package/src/operations/intersect-function.ts +23 -24
- package/src/operations/is-operator.ts +67 -15
- package/src/operations/isDistinct-function.ts +17 -9
- package/src/operations/join-function.ts +22 -9
- package/src/operations/last-function.ts +12 -7
- package/src/operations/length-function.ts +17 -8
- package/src/operations/less-operator.ts +10 -5
- package/src/operations/less-or-equal-operator.ts +10 -5
- package/src/operations/less-than.ts +26 -2
- package/src/operations/lower-function.ts +17 -8
- package/src/operations/minus-operator.ts +10 -5
- package/src/operations/mod-operator.ts +20 -3
- package/src/operations/multiply-operator.ts +12 -7
- package/src/operations/not-equal-operator.ts +16 -7
- package/src/operations/not-equivalent-operator.ts +16 -11
- package/src/operations/not-function.ts +17 -7
- package/src/operations/ofType-function.ts +139 -0
- package/src/operations/or-operator.ts +21 -12
- package/src/operations/plus-operator.ts +19 -7
- package/src/operations/power-function.ts +27 -13
- package/src/operations/replace-function.ts +33 -14
- package/src/operations/round-function.ts +29 -12
- package/src/operations/select-function.ts +17 -9
- package/src/operations/single-function.ts +10 -6
- package/src/operations/skip-function.ts +18 -9
- package/src/operations/split-function.ts +30 -13
- package/src/operations/sqrt-function.ts +14 -8
- package/src/operations/startsWith-function.ts +26 -12
- package/src/operations/subsetOf-function.ts +20 -15
- package/src/operations/substring-function.ts +43 -24
- package/src/operations/supersetOf-function.ts +20 -15
- package/src/operations/tail-function.ts +8 -4
- package/src/operations/take-function.ts +18 -9
- package/src/operations/toBoolean-function.ts +23 -14
- package/src/operations/toDecimal-function.ts +18 -9
- package/src/operations/toInteger-function.ts +22 -12
- package/src/operations/toString-function.ts +26 -16
- package/src/operations/trace-function.ts +19 -10
- package/src/operations/trim-function.ts +16 -7
- package/src/operations/truncate-function.ts +14 -8
- package/src/operations/unary-minus-operator.ts +23 -13
- package/src/operations/unary-plus-operator.ts +2 -1
- package/src/operations/union-function.ts +25 -28
- package/src/operations/union-operator.ts +2 -1
- package/src/operations/upper-function.ts +17 -8
- package/src/operations/where-function.ts +20 -11
- package/src/operations/xor-operator.ts +9 -4
- package/src/parser.ts +527 -8
- package/src/quantity-value.ts +4 -8
- package/src/registry.ts +147 -5
- package/src/types.ts +42 -19
- 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
|
+
}
|