From 9b4c0bcd62e6418c318b114d0539e5216d968308 Mon Sep 17 00:00:00 2001 From: Panthevm Date: Thu, 12 Mar 2026 16:34:38 +0300 Subject: [PATCH 01/55] enhance code-editor: add SQL completion, improve styling - Add SQL autocompletion with table/function metadata fetching - Add Tab to accept completion, Enter to suppress when completion active - Improve line numbers styling and gutter active line colors - Style completion matched text with link color and bold - Add table icon for table completions - Use CSS variables for search match colors --- .../src/components/code-editor/index.tsx | 118 +- .../components/code-editor/sql-completion.ts | 1092 +++++++++++++++++ 2 files changed, 1196 insertions(+), 14 deletions(-) create mode 100644 packages/react-components/src/components/code-editor/sql-completion.ts diff --git a/packages/react-components/src/components/code-editor/index.tsx b/packages/react-components/src/components/code-editor/index.tsx index 0f5501c8..1a56edb3 100644 --- a/packages/react-components/src/components/code-editor/index.tsx +++ b/packages/react-components/src/components/code-editor/index.tsx @@ -5,6 +5,7 @@ import { closeBrackets, closeBracketsKeymap, completionKeymap, + completionStatus, } from "@codemirror/autocomplete"; import { defaultKeymap, history, historyKeymap } from "@codemirror/commands"; import { json, jsonParseLinter } from "@codemirror/lang-json"; @@ -34,6 +35,7 @@ import { Compartment, EditorState, type Extension, + Prec, RangeSet, StateEffect, StateField, @@ -55,13 +57,18 @@ import { type ViewUpdate, } from "@codemirror/view"; import { tags } from "@lezer/highlight"; -import { Braces, ChevronDown, ChevronUp, Terminal, X } from "lucide-react"; +import { ChevronDown, ChevronUp, ChevronsRight, Table2, Terminal, X } from "lucide-react"; import * as React from "react"; import { flushSync } from "react-dom"; import { createRoot } from "react-dom/client"; import { ComplexTypeIcon, SquareFunctionIcon, TypCodeIcon } from "../../icons"; import { http } from "./http"; +import { + type SqlConfig, + buildSqlCompletionExtensions, + fetchSqlMetadata, +} from "./sql-completion"; // --- Issue lines: gutter highlighting, line background, hover tooltip --- @@ -234,11 +241,16 @@ const baseTheme = EditorView.theme({ border: "none", }, ".cm-lineNumbers": { - paddingLeft: "16px", + minWidth: "3.5ch", + }, + ".cm-lineNumbers .cm-gutterElement": { + minWidth: "3.5ch", + paddingRight: "4px", + color: "var(--color-text-quaternary)", }, - ".cm-activeLineGutter": { + ".cm-lineNumbers .cm-gutterElement.cm-activeLineGutter": { backgroundColor: "var(--color-bg-primary)", - color: "var(--color-text-primary)", + color: "var(--color-text-secondary)", }, ".cm-activeLine": { backgroundColor: "rgba(255, 255, 255, 0)", @@ -266,6 +278,14 @@ const completionTheme = EditorView.theme({ ".cm-completionLabel": { flex: "1", minWidth: "0", + fontFamily: "var(--font-family-mono)", + fontSize: "var(--font-size-sm)", + lineHeight: "var(--font-leading-5)", + }, + ".cm-completionMatchedText": { + textDecoration: "none", + fontWeight: "600", + color: "var(--color-text-link)", }, ".cm-completionDetail": { color: "var(--color-text-tertiary)", @@ -326,11 +346,16 @@ const readOnlyTheme = EditorView.theme({ border: "none", }, ".cm-lineNumbers": { - paddingLeft: "16px", + minWidth: "3.5ch", + }, + ".cm-lineNumbers .cm-gutterElement": { + minWidth: "3.5ch", + paddingRight: "4px", + color: "var(--color-text-quaternary)", }, - ".cm-activeLineGutter": { + ".cm-lineNumbers .cm-gutterElement.cm-activeLineGutter": { backgroundColor: "var(--color-bg-secondary)", - color: "var(--color-text-primary)", + color: "var(--color-text-secondary)", }, ".cm-activeLine": { backgroundColor: "rgba(255, 255, 255, 0)", @@ -573,10 +598,10 @@ const searchPanelTheme = EditorView.baseTheme({ border: "none", }, ".cm-searchMatch": { - backgroundColor: "#e9f2fc", + backgroundColor: "var(--color-blue-200)", }, ".cm-searchMatch-selected": { - backgroundColor: "#d0e2f8", + backgroundColor: "var(--color-blue-400)", }, }); @@ -743,11 +768,17 @@ type CodeEditorProps = { foldGutter?: boolean; lintGutter?: boolean; lineNumbers?: boolean; - sqlExtraBuiltins?: string[]; + sql?: SqlConfig; }; export type CodeEditorView = EditorView; +export type { + SqlConfig, + SqlQueryType, + SqlMetadata, +} from "./sql-completion"; + export function CodeEditor({ defaultValue, currentValue, @@ -763,7 +794,7 @@ export function CodeEditor({ foldGutter: enableFoldGutter = true, lintGutter: enableLintGutter = true, lineNumbers: enableLineNumbers = true, - sqlExtraBuiltins, + sql, }: CodeEditorProps) { const domRef = React.useRef(null); const [view, setView] = React.useState(null); @@ -776,6 +807,11 @@ export function CodeEditor({ const readOnlyCompartment = React.useRef(new Compartment()); const themeCompartment = React.useRef(new Compartment()); const additionalExtensionsCompartment = React.useRef(new Compartment()); + const sqlCompletionCompartment = React.useRef(new Compartment()); + const [sqlFunctions, setSqlFunctions] = React.useState< + string[] | undefined + >(); + const executeSqlRef = React.useRef(sql?.executeSql); React.useEffect(() => { if (!domRef.current) { @@ -819,6 +855,18 @@ export function CodeEditor({ highlightActiveLine(), highlightActiveLineGutter(), highlightSelectionMatches(), + Prec.highest( + keymap.of([ + { + key: "Tab", + run: acceptCompletion, + }, + { + key: "Enter", + run: (v) => completionStatus(v.state) === "active", + }, + ]), + ), themeCompartment.current.of(baseTheme), completionTheme, keymap.of([ @@ -837,6 +885,7 @@ export function CodeEditor({ onChangeComparment.current.of([]), onUpdateComparment.current.of([]), additionalExtensionsCompartment.current.of([]), + sqlCompletionCompartment.current.of([]), ], }), }); @@ -849,6 +898,45 @@ export function CodeEditor({ }; }, [enableFoldGutter, enableLineNumbers, enableLintGutter]); + React.useEffect(() => { + executeSqlRef.current = sql?.executeSql; + }); + + React.useEffect(() => { + if (!view || !sql) { + if (view) { + view.dispatch({ + effects: sqlCompletionCompartment.current.reconfigure([]), + }); + } + setSqlFunctions(undefined); + return; + } + + let cancelled = false; + + fetchSqlMetadata(sql.executeSql) + .then((metadata) => { + if (cancelled) return; + setSqlFunctions(metadata.functions); + const extensions = buildSqlCompletionExtensions( + metadata, + (query, type) => + executeSqlRef.current?.(query, type) ?? Promise.resolve([]), + ); + view.dispatch({ + effects: sqlCompletionCompartment.current.reconfigure( + extensions, + ), + }); + }) + .catch(() => {}); + + return () => { + cancelled = true; + }; + }, [view, sql]); + React.useEffect(() => { if (viewCallback && view) { viewCallback(view); @@ -903,10 +991,10 @@ export function CodeEditor({ } view.dispatch({ effects: languageCompartment.current.reconfigure( - languageExtensions(mode, sqlExtraBuiltins), + languageExtensions(mode, sqlFunctions), ), }); - }, [mode, view, sqlExtraBuiltins]); + }, [mode, view, sqlFunctions]); React.useEffect(() => { if (view === null) { @@ -1012,12 +1100,14 @@ const editorInputTheme = EditorView.theme({ }); const KeywordIcon = () => ; -const OperatorIcon = () => ; +const OperatorIcon = () => ; +const TableIcon = () => ; function getCompletionIcon(completion: Completion): React.FC | null { if (completion.type === "function") return SquareFunctionIcon; if (completion.type === "keyword") return KeywordIcon; if (completion.type === "operator") return OperatorIcon; + if (completion.type === "table") return TableIcon; const detail = completion.detail; if (!detail) { if (completion.type === "variable") return SquareFunctionIcon; diff --git a/packages/react-components/src/components/code-editor/sql-completion.ts b/packages/react-components/src/components/code-editor/sql-completion.ts new file mode 100644 index 00000000..74d349d1 --- /dev/null +++ b/packages/react-components/src/components/code-editor/sql-completion.ts @@ -0,0 +1,1092 @@ +import { + autocompletion, + type Completion, + type CompletionContext, + type CompletionResult, + type CompletionSource, + startCompletion, +} from "@codemirror/autocomplete"; +import { EditorState, type Extension } from "@codemirror/state"; +import { EditorView } from "@codemirror/view"; + +// ── Public types ── + +export type SqlQueryType = "tables" | "columns" | "functions" | "jsonb_columns" | "structure_definition"; + +interface StructureDefinitionElementType { + code: string; +} + +interface StructureDefinitionElement { + path?: string; + type?: StructureDefinitionElementType[]; + max?: string; + short?: string; + definition?: string; +} + +interface StructureDefinition { + name?: string; + snapshot?: { + element: StructureDefinitionElement[]; + }; +} + +export interface SqlConfig { + executeSql: ( + query: string, + type: SqlQueryType, + ) => Promise[]>; +} + +// ── Internal types ── + +type SchemaMap = Record; +type JsonbColumnMap = Record; +type ColumnInfo = { name: string; dataType: string }; +type ColumnMap = Record; + +type FhirFieldInfo = { + name: string; + datatype: string; + isArray: boolean; + description: string | undefined; +}; + +type FhirPathChildren = Record; + +type JsonbChain = { + tableOrAlias: string | null; + column: string; + path: string[]; + isPathOp: boolean; + partialInput: string; + insideQuote: boolean; + lastRawSegment: string | null; +}; + +type AliasEntry = { schema: string; table: string }; + +export interface SqlMetadata { + schemas: SchemaMap; + jsonbColumns: JsonbColumnMap; + functions: string[]; + columns: ColumnMap; +} + +// ── SQL queries ── + +const TABLES_QUERY = `SELECT table_schema, table_name FROM information_schema.tables WHERE table_schema NOT IN ('pg_catalog', 'information_schema', 'pgagent') AND table_type = 'BASE TABLE' ORDER BY table_schema, table_name`; + +const JSONB_COLUMNS_QUERY = `SELECT c.table_schema, c.table_name, c.column_name FROM information_schema.columns c JOIN information_schema.tables t ON c.table_schema = t.table_schema AND c.table_name = t.table_name WHERE t.table_type = 'BASE TABLE' AND c.table_schema NOT IN ('pg_catalog', 'information_schema', 'pgagent') AND c.udt_name = 'jsonb'`; + +const FUNCTIONS_QUERY = `SELECT DISTINCT p.proname AS name FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid LEFT JOIN pg_depend d ON d.objid = p.oid AND d.deptype = 'e' WHERE n.nspname NOT IN ('pg_catalog', 'information_schema') AND d.objid IS NULL ORDER BY p.proname`; + +const COLUMNS_QUERY = `SELECT c.table_schema, c.table_name, c.column_name, c.data_type FROM information_schema.columns c JOIN information_schema.tables t ON c.table_schema = t.table_schema AND c.table_name = t.table_name WHERE t.table_type = 'BASE TABLE' AND c.table_schema NOT IN ('pg_catalog', 'information_schema', 'pgagent') ORDER BY c.table_schema, c.table_name, c.ordinal_position`; + +// ── FHIR StructureDefinition processing ── + +function isExpandedVariant( + path: string, + unionBases: Set, +): boolean { + const parts = path.split("."); + if (parts.length < 2) return false; + const parentPath = parts.slice(0, -1).join("."); + const name = parts[parts.length - 1]!; + + for (const base of unionBases) { + const baseParts = base.split("."); + const baseName = baseParts[baseParts.length - 1]!; + const baseParent = baseParts.slice(0, -1).join("."); + + if ( + parentPath === baseParent && + name.startsWith(baseName) && + name.length > baseName.length && + /^[A-Z]/.test(name.slice(baseName.length)) + ) { + return true; + } + } + return false; +} + +function buildFromStructureDefinition( + sd: StructureDefinition, +): FhirPathChildren { + const elements = sd.snapshot?.element ?? []; + const result: FhirPathChildren = {}; + + const unionBases = new Set(); + for (const el of elements) { + if (!el.path) continue; + const name = el.path.split(".").pop() ?? ""; + if (name.endsWith("[x]")) { + unionBases.add(el.path.replace(/\[x\]$/, "")); + } + } + + for (const el of elements) { + if (!el.path) continue; + const parts = el.path.split("."); + if (parts.length < 2) continue; + + if (isExpandedVariant(el.path, unionBases)) continue; + + const parentPath = parts.slice(0, -1).join("."); + const rawName = parts[parts.length - 1]!; + + if (!result[parentPath]) result[parentPath] = []; + + if (rawName.endsWith("[x]")) { + const name = rawName.slice(0, -3); + if (result[parentPath].some((f) => f.name === name)) continue; + + result[parentPath].push({ + name, + datatype: "union", + isArray: el.max === "*", + description: el.short ?? el.definition, + }); + + const unionPath = `${parentPath}.${name}`; + if (!result[unionPath]) result[unionPath] = []; + for (const t of el.type ?? []) { + if (!result[unionPath].some((f) => f.name === t.code)) { + result[unionPath].push({ + name: t.code, + datatype: t.code, + isArray: false, + description: el.short ?? el.definition, + }); + } + } + } else { + if (result[parentPath].some((f) => f.name === rawName)) continue; + + result[parentPath].push({ + name: rawName, + datatype: el.type?.[0]?.code ?? "", + isArray: el.max === "*", + description: el.short ?? el.definition, + }); + } + } + + return result; +} + +function transformReferenceFields(result: FhirPathChildren): void { + for (const [path, children] of Object.entries(result)) { + const hasReferenceField = children.some((c) => c.name === "reference"); + if (!hasReferenceField) continue; + + result[path] = children.flatMap((child) => { + if (child.name === "reference") { + return [ + { + name: "id", + datatype: "string", + isArray: false, + description: "Resource ID" as string | undefined, + }, + { + name: "resourceType", + datatype: "string", + isArray: false, + description: "Resource type" as string | undefined, + }, + ]; + } + return [child]; + }); + } +} + +// ── SQL parsing utilities ── + +const SQL_TABLE_KEYWORDS = + /\b(?:from|join|inner\s+join|left\s+join|right\s+join|full\s+join|cross\s+join|into|update|table)\s+$/i; + +function isInsideString(textBefore: string): boolean { + let count = 0; + for (let i = 0; i < textBefore.length; i++) { + if (textBefore[i] === "'") { + if (i + 1 < textBefore.length && textBefore[i + 1] === "'") { + i++; + } else { + count++; + } + } + } + return count % 2 !== 0; +} + +function isInJsonbContext(textBefore: string): boolean { + return parseJsonbChain(textBefore) !== null; +} + +function parseJsonbChain(textBefore: string): JsonbChain | null { + const pathOpMatch = textBefore.match(/((?:\w+\.)?\w+)\s*#>>?\s*'\{([^}]*)$/); + if (pathOpMatch) { + const ref = pathOpMatch[1]!; + const pathContent = pathOpMatch[2]!; + const segments = pathContent ? pathContent.split(",") : []; + const partialInput = segments.length > 0 ? (segments.pop() ?? "") : ""; + const lastRawSegment = + segments.length > 0 ? (segments[segments.length - 1] ?? null) : null; + const path = segments.filter((s) => !/^\d+$/.test(s)); + + const dotParts = ref.split("."); + if (dotParts.length === 2) { + return { + tableOrAlias: dotParts[0]!, + column: dotParts[1]!, + path, + isPathOp: true, + partialInput, + insideQuote: false, + lastRawSegment, + }; + } + return { + tableOrAlias: null, + column: dotParts[0]!, + path, + isPathOp: true, + partialInput, + insideQuote: false, + lastRawSegment, + }; + } + + const arrowPattern = + /(?:((?:\w+\.)?\w+)((?:\s*->>?\s*(?:'[^']*'|\d+))*)\s*->>?\s*)('?)([^']*)?$/; + const arrowMatch = textBefore.match(arrowPattern); + if (!arrowMatch) return null; + + const ref = arrowMatch[1]!; + const chainPart = arrowMatch[2] || ""; + const insideQuote = arrowMatch[3] === "'"; + const partialInput = arrowMatch[4] ?? ""; + + const chainSegments: string[] = []; + let lastRawSeg: string | null = null; + const segmentRegex = /->>?\s*(?:'([^']*)'|(\d+))/g; + for ( + let m = segmentRegex.exec(chainPart); + m !== null; + m = segmentRegex.exec(chainPart) + ) { + const seg = m[1] ?? m[2] ?? ""; + lastRawSeg = seg; + if (!/^\d+$/.test(seg)) { + chainSegments.push(seg); + } + } + + const dotParts = ref.split("."); + if (dotParts.length === 2) { + return { + tableOrAlias: dotParts[0]!, + column: dotParts[1]!, + path: chainSegments, + isPathOp: false, + partialInput, + insideQuote, + lastRawSegment: lastRawSeg, + }; + } + return { + tableOrAlias: null, + column: dotParts[0]!, + path: chainSegments, + isPathOp: false, + partialInput, + insideQuote, + lastRawSegment: lastRawSeg, + }; +} + +function buildAliasMap( + sql: string, + schemas: SchemaMap, +): Record { + const aliases: Record = {}; + const regex = /\b(?:FROM|JOIN)\s+((?:\w+\.)?\w+)(?:\s+(?:AS\s+)?(\w+))?/gi; + + for (let match = regex.exec(sql); match !== null; match = regex.exec(sql)) { + const fullTable = match[1]!; + const alias = match[2]; + + let schema: string; + let table: string; + if (fullTable.includes(".")) { + const parts = fullTable.split("."); + schema = parts[0]!; + table = parts[1]!; + } else { + table = fullTable; + let found: string | null = null; + for (const [s, tables] of Object.entries(schemas)) { + if (tables.includes(table)) { + found = s; + if (s === "public") break; + } + } + schema = found ?? "public"; + } + + if (alias) { + aliases[alias.toLowerCase()] = { schema, table }; + } + aliases[table.toLowerCase()] = { schema, table }; + } + + return aliases; +} + +function tableToResourceType(table: string): string { + return table + .split("_") + .map((s) => s.charAt(0).toUpperCase() + s.slice(1)) + .join(""); +} + +function getCurrentStatement(doc: string, pos: number): string { + let start = 0; + let end = doc.length; + + const before = doc.lastIndexOf(";", pos - 1); + if (before !== -1) start = before + 1; + + const after = doc.indexOf(";", pos); + if (after !== -1) end = after; + + return doc.slice(start, end); +} + +// ── Completion result builders ── + +function buildJsonbResult( + chain: JsonbChain, + pathChildren: FhirPathChildren, + resourceType: string, + context: CompletionContext, +): CompletionResult | null { + const lookupPath = + chain.path.length > 0 + ? `${resourceType}.${chain.path.join(".")}` + : resourceType; + + const children = pathChildren[lookupPath]; + if (!children || children.length === 0) return null; + + const partial = chain.partialInput.toLowerCase(); + const filtered = partial + ? children.filter((f) => f.name.toLowerCase().startsWith(partial)) + : children; + + if (filtered.length === 0) return null; + + if (chain.isPathOp) { + return { + from: context.pos - chain.partialInput.length, + validFor: /^\w*$/, + options: filtered.map((f): Completion => ({ + label: f.name, + type: "property", + detail: f.datatype + (f.isArray ? "[]" : ""), + ...(f.description != null ? { info: f.description } : {}), + })), + }; + } + + if (chain.insideQuote) { + return { + from: context.pos - chain.partialInput.length, + validFor: /^\w*$/, + options: filtered.map((f): Completion => ({ + label: f.name, + type: "property", + detail: f.datatype + (f.isArray ? "[]" : ""), + ...(f.description != null ? { info: f.description } : {}), + apply: (view: EditorView, _completion: Completion, from: number, to: number) => { + const after = view.state.sliceDoc(to, to + 1); + const end = after === "'" ? to + 1 : to; + const insert = `${f.name}'`; + view.dispatch({ + changes: { from, to: end, insert }, + selection: { anchor: from + insert.length }, + }); + }, + })), + }; + } + + return { + from: context.pos - chain.partialInput.length, + validFor: /^'?\w*'?$/, + options: filtered.map((f): Completion => ({ + label: `'${f.name}'`, + type: "property", + detail: f.datatype + (f.isArray ? "[]" : ""), + ...(f.description != null ? { info: f.description } : {}), + apply: `'${f.name}'`, + })), + }; +} + +function isArrayPosition( + chain: JsonbChain, + pathChildren: FhirPathChildren, + resourceType: string, +): boolean { + if (!chain.lastRawSegment || /^\d+$/.test(chain.lastRawSegment)) return false; + if (!chain.isPathOp && chain.insideQuote) return false; + + const fieldName = chain.lastRawSegment; + const parentPath = + chain.path.length > 1 + ? `${resourceType}.${chain.path.slice(0, -1).join(".")}` + : resourceType; + const parentChildren = pathChildren[parentPath]; + if (!parentChildren) return false; + + const element = parentChildren.find((f) => f.name === fieldName); + return !!element?.isArray; +} + +function buildArrayIndexResult( + chain: JsonbChain, + context: CompletionContext, +): CompletionResult { + return { + from: context.pos - chain.partialInput.length, + options: [ + { + label: "0", + type: "enum", + detail: "array index", + }, + ], + }; +} + +async function resolveNestedTypes( + pathChildren: FhirPathChildren, + resourceType: string, + path: string[], + fetchSchema: (type: string) => Promise, +): Promise { + for (let i = 0; i < path.length; i++) { + const currentPath = `${resourceType}.${path.slice(0, i + 1).join(".")}`; + + if (pathChildren[currentPath]) continue; + + const parentPath = + i === 0 ? resourceType : `${resourceType}.${path.slice(0, i).join(".")}`; + const parentChildren = pathChildren[parentPath]; + if (!parentChildren) return; + + const segmentName = path[i]!; + const element = parentChildren.find((f) => f.name === segmentName); + if (!element?.datatype) return; + + if (element.datatype === "union") continue; + + const firstChar = element.datatype[0]!; + if (firstChar !== firstChar.toUpperCase()) return; + + const typeChildren = await fetchSchema(element.datatype); + if (!typeChildren) return; + + const typeName = element.datatype; + for (const [key, children] of Object.entries(typeChildren)) { + const suffix = key === typeName ? "" : key.slice(typeName.length); + pathChildren[currentPath + suffix] = children; + } + } +} + +// ── Completion extensions ── + +function tableCompletionExtension(schemas: SchemaMap): Extension { + const source = (context: CompletionContext): CompletionResult | null => { + const line = context.state.doc.lineAt(context.pos); + const textBefore = line.text.slice(0, context.pos - line.from); + + if (isInsideString(textBefore)) return null; + + const schemaDot = textBefore.match(/(\w+)\.(\w*)$/); + if (schemaDot) { + const schemaName = schemaDot[1]!; + const tables = schemas[schemaName]; + if (!tables) return null; + return { + from: context.pos - (schemaDot[2] ?? "").length, + options: tables.map((t) => ({ label: t, type: "table" })), + }; + } + + const word = context.matchBefore(/\w*/); + if (!word) return null; + + const beforeWord = textBefore.slice(0, word.from - line.from); + if (!SQL_TABLE_KEYWORDS.test(beforeWord) && !context.explicit) return null; + + const options: { label: string; type: string; detail?: string }[] = []; + + for (const [schema, tables] of Object.entries(schemas)) { + options.push({ label: `${schema}.`, type: "keyword", detail: "schema" }); + for (const table of tables) { + if (schema === "public") { + options.push({ label: table, type: "table" }); + } else { + options.push({ + label: `${schema}.${table}`, + type: "table", + detail: schema, + }); + } + } + } + + return { from: word.from, options }; + }; + + return EditorState.languageData.of(() => [{ autocomplete: source }]); +} + +function columnCompletionExtension(ctx: { + schemas: SchemaMap; + columns: ColumnMap; +}): Extension { + const source = (context: CompletionContext): CompletionResult | null => { + const line = context.state.doc.lineAt(context.pos); + const textBefore = line.text.slice(0, context.pos - line.from); + + if (isInsideString(textBefore)) return null; + + const fullDoc = context.state.doc.toString(); + const statement = getCurrentStatement(fullDoc, context.pos); + const aliases = buildAliasMap(statement, ctx.schemas); + + if (Object.keys(aliases).length === 0) return null; + + const aliasDot = textBefore.match(/(\w+)\.(\w*)$/); + if (aliasDot) { + const beforeAlias = textBefore.slice( + 0, + textBefore.length - aliasDot[0].length, + ); + if (SQL_TABLE_KEYWORDS.test(beforeAlias)) return null; + + const aliasName = aliasDot[1]!; + const entry = aliases[aliasName.toLowerCase()]; + if (!entry) return null; + const key = `${entry.schema}.${entry.table}`; + const cols = ctx.columns[key]; + if (!cols || cols.length === 0) return null; + + return { + from: context.pos - (aliasDot[2] ?? "").length, + options: cols.map((c) => ({ + label: c.name, + type: "variable", + detail: c.dataType, + })), + }; + } + + const word = context.matchBefore(/\w*/); + if (!word) return null; + if (word.from === word.to && !context.explicit) return null; + + const textBeforeWord = textBefore.slice(0, word.from - line.from); + if (SQL_TABLE_KEYWORDS.test(textBeforeWord)) return null; + + const seen = new Set(); + const options: { label: string; type: string; detail: string }[] = []; + for (const entry of Object.values(aliases)) { + const key = `${entry.schema}.${entry.table}`; + const cols = ctx.columns[key]; + if (!cols) continue; + for (const c of cols) { + const dedup = `${c.name}::${entry.table}`; + if (seen.has(dedup)) continue; + seen.add(dedup); + options.push({ + label: c.name, + type: "variable", + detail: `${c.dataType} · ${entry.table}`, + }); + } + } + + if (options.length === 0) return null; + return { from: word.from, options }; + }; + + return EditorState.languageData.of(() => [{ autocomplete: source }]); +} + +function triggerCompletionAfter(view: EditorView) { + requestAnimationFrame(() => startCompletion(view)); +} + +function jsonbOperatorExtension(): Extension { + const source = (context: CompletionContext): CompletionResult | null => { + const line = context.state.doc.lineAt(context.pos); + const textBefore = line.text.slice(0, context.pos - line.from); + + if (/\w\s*#$/.test(textBefore)) { + return { + from: context.pos - 1, + options: [ + { + label: "#>> '{}'", + type: "operator", + apply: (view, _completion, from, to) => { + const insert = "#>> '{"; + view.dispatch({ + changes: { from, to, insert: `${insert}}' ` }, + selection: { anchor: from + insert.length }, + }); + triggerCompletionAfter(view); + }, + }, + { + label: "#> '{}'", + type: "operator", + apply: (view, _completion, from, to) => { + const insert = "#> '{"; + view.dispatch({ + changes: { from, to, insert: `${insert}}' ` }, + selection: { anchor: from + insert.length }, + }); + triggerCompletionAfter(view); + }, + }, + ], + }; + } + + if (/\w\s*->$/.test(textBefore) && !/\w\s*->>$/.test(textBefore)) { + return { + from: context.pos - 2, + options: [ + { + label: "->> ''", + type: "operator", + apply: (view, _completion, from, to) => { + const insert = "->> '"; + view.dispatch({ + changes: { from, to, insert: `${insert}' ` }, + selection: { anchor: from + insert.length }, + }); + triggerCompletionAfter(view); + }, + }, + { + label: "-> ''", + type: "operator", + apply: (view, _completion, from, to) => { + const insert = "-> '"; + view.dispatch({ + changes: { from, to, insert: `${insert}' ` }, + selection: { anchor: from + insert.length }, + }); + triggerCompletionAfter(view); + }, + }, + { + label: "->> 0", + type: "operator", + apply: (view, _completion, from, to) => { + const insert = "->> "; + view.dispatch({ + changes: { from, to, insert: `${insert}0 ` }, + selection: { + anchor: from + insert.length, + head: from + insert.length + 1, + }, + }); + }, + }, + { + label: "-> 0", + type: "operator", + apply: (view, _completion, from, to) => { + const insert = "-> "; + view.dispatch({ + changes: { from, to, insert: `${insert}0 ` }, + selection: { + anchor: from + insert.length, + head: from + insert.length + 1, + }, + }); + }, + }, + ], + }; + } + + return null; + }; + + return EditorState.languageData.of(() => [{ autocomplete: source }]); +} + +function jsonbCompletionExtension(ctx: { + schemas: SchemaMap; + jsonbColumns: JsonbColumnMap; + sdCache: Record; + sdNotFound: Set; + fetchSchema: (type: string) => Promise; +}): Extension { + const resolveChain = ( + context: CompletionContext, + ): { chain: JsonbChain; resourceType: string } | null => { + const line = context.state.doc.lineAt(context.pos); + const textBefore = line.text.slice(0, context.pos - line.from); + + const chain = parseJsonbChain(textBefore); + if (!chain) return null; + + const fullDoc = context.state.doc.toString(); + const statement = getCurrentStatement(fullDoc, context.pos); + const aliases = buildAliasMap(statement, ctx.schemas); + + let resolved: AliasEntry | null = null; + + if (chain.tableOrAlias) { + resolved = aliases[chain.tableOrAlias.toLowerCase()] ?? null; + } else { + for (const entry of Object.values(aliases)) { + const key = `${entry.schema}.${entry.table}`; + const cols = ctx.jsonbColumns[key]; + if (cols?.includes(chain.column)) { + resolved = entry; + break; + } + } + } + + if (!resolved) return null; + + const jsonbKey = `${resolved.schema}.${resolved.table}`; + const jsonbCols = ctx.jsonbColumns[jsonbKey]; + if (!jsonbCols?.includes(chain.column)) return null; + + if (chain.column !== "resource") return null; + + const resourceType = tableToResourceType(resolved.table); + if (ctx.sdNotFound.has(resourceType)) return null; + + return { chain, resourceType }; + }; + + const complete = async ( + chain: JsonbChain, + resourceType: string, + pathChildren: FhirPathChildren, + context: CompletionContext, + ): Promise => { + if (chain.path.length > 0) { + await resolveNestedTypes( + pathChildren, + resourceType, + chain.path, + ctx.fetchSchema, + ); + } + + if (isArrayPosition(chain, pathChildren, resourceType)) { + return buildArrayIndexResult(chain, context); + } + + return buildJsonbResult(chain, pathChildren, resourceType, context); + }; + + const source = ( + context: CompletionContext, + ): CompletionResult | null | Promise => { + const info = resolveChain(context); + if (!info) return null; + + const { chain, resourceType } = info; + + const cached = ctx.sdCache[resourceType]; + if (cached) { + if (chain.path.length === 0) { + return buildJsonbResult(chain, cached, resourceType, context); + } + return complete(chain, resourceType, cached, context); + } + + return ctx.fetchSchema(resourceType).then((fetched) => { + if (!fetched) return null; + return complete(chain, resourceType, fetched, context); + }); + }; + + return EditorState.languageData.of(() => [{ autocomplete: source }]); +} + +function sqlCompletionOverride(): Extension { + return autocompletion({ + override: [ + async ( + context: CompletionContext, + ): Promise => { + const line = context.state.doc.lineAt(context.pos); + const textBefore = line.text.slice(0, context.pos - line.from); + const inJsonb = isInJsonbContext(textBefore); + + const langSources = context.state.languageDataAt( + "autocomplete", + context.pos, + ); + + const results = ( + await Promise.all( + langSources.map((src) => Promise.resolve(src(context))), + ) + ).filter((r): r is CompletionResult => r !== null); + + if (results.length === 0) return null; + + if (inJsonb) { + const jsonbTypes = new Set(["property", "enum", "operator"]); + const jsonbResult = results.find((r) => + r.options.some((o) => jsonbTypes.has(o.type ?? "")), + ); + if (!jsonbResult) return null; + return { + ...jsonbResult, + options: jsonbResult.options.filter((o) => + jsonbTypes.has(o.type ?? ""), + ), + }; + } + + const hasTableResults = results.some((r) => + r.options.some((o) => o.type === "table"), + ); + + if (hasTableResults) { + const tableOptions = results.flatMap((r) => + r.options.filter( + (o) => o.type === "table" || o.type === "keyword", + ), + ); + const from = results.find((r) => + r.options.some((o) => o.type === "table"), + )?.from; + if (from == null) return null; + return { from, options: tableOptions }; + } + + if (results.length === 1) return results[0]!; + + const groups = new Map(); + for (const r of results) { + const existing = groups.get(r.from); + if (existing) { + existing.options.push(...r.options); + } else { + groups.set(r.from, { + from: r.from, + options: [...r.options], + }); + } + } + + let best: { from: number; options: Completion[] } | null = null; + for (const g of groups.values()) { + if (!best || g.options.length > best.options.length) best = g; + } + if (best) { + best.options = best.options.map((o) => { + if (o.type === "keyword") return { ...o, boost: 2 }; + if (o.type === "type") return { ...o, boost: 1 }; + if (o.type === "variable") return { ...o, boost: -1 }; + return o; + }); + } + return best; + }, + ], + }); +} + +// ── Public API ── + +export async function fetchSqlMetadata( + executeSql: SqlConfig["executeSql"], +): Promise { + const [tablesRows, jsonbRows, functionsRows, columnsRows] = + await Promise.all([ + executeSql(TABLES_QUERY, "tables"), + executeSql(JSONB_COLUMNS_QUERY, "jsonb_columns"), + executeSql(FUNCTIONS_QUERY, "functions"), + executeSql(COLUMNS_QUERY, "columns"), + ]); + + const schemas: SchemaMap = {}; + for (const row of tablesRows) { + const s = String(row.table_schema); + if (!schemas[s]) schemas[s] = []; + schemas[s].push(String(row.table_name)); + } + + const jsonbColumns: JsonbColumnMap = {}; + for (const row of jsonbRows) { + const key = `${row.table_schema}.${row.table_name}`; + if (!jsonbColumns[key]) jsonbColumns[key] = []; + jsonbColumns[key].push(String(row.column_name)); + } + + const functions = functionsRows.map((r) => String(r.name)); + + const columns: ColumnMap = {}; + for (const row of columnsRows) { + const key = `${row.table_schema}.${row.table_name}`; + if (!columns[key]) columns[key] = []; + columns[key].push({ + name: String(row.column_name), + dataType: String(row.data_type), + }); + } + + return { schemas, jsonbColumns, functions, columns }; +} + +export function buildSqlCompletionExtensions( + metadata: SqlMetadata, + executeSql: SqlConfig["executeSql"], +): Extension[] { + const sdCache: Record = {}; + const sdNotFound = new Set(); + + const fetchSchema = async ( + resourceType: string, + ): Promise => { + if (sdCache[resourceType]) return sdCache[resourceType]; + if (sdNotFound.has(resourceType)) return null; + + try { + const name = resourceType.replace(/'/g, "''"); + const rows = await executeSql( + `SELECT resource FROM far.canonicalresource WHERE rt = 'StructureDefinition' AND resource->>'name' = '${name}' LIMIT 1`, + "structure_definition", + ); + const sd = (rows[0]?.resource ?? null) as StructureDefinition | null; + if (!sd) { + sdNotFound.add(resourceType); + return null; + } + + const pathChildren = buildFromStructureDefinition(sd); + transformReferenceFields(pathChildren); + sdCache[resourceType] = pathChildren; + return pathChildren; + } catch { + sdNotFound.add(resourceType); + return null; + } + }; + + return [ + EditorView.theme({ + ".cm-tooltip.cm-tooltip-autocomplete": { + background: "var(--color-bg-primary)", + border: "1px solid var(--color-border-primary)", + borderRadius: "var(--radius-md)", + padding: "4px", + boxShadow: "0 4px 12px rgba(0, 0, 0, 0.1)", + fontFamily: "var(--font-family-sans)", + fontSize: "14px", + }, + ".cm-tooltip.cm-tooltip-autocomplete > ul": { + maxHeight: "300px", + }, + ".cm-tooltip-autocomplete ul li": { + padding: "4px 8px", + borderRadius: "4px", + }, + ".cm-tooltip-autocomplete ul li[aria-selected]": { + background: "var(--color-bg-quaternary)", + color: "var(--color-text-primary)", + }, + ".cm-completionLabel": { + color: "var(--color-text-primary)", + fontSize: "14px", + }, + ".cm-completionDetail": { + color: "var(--color-text-tertiary)", + fontSize: "12px", + fontStyle: "normal", + marginLeft: "8px", + }, + ".cm-completionIcon": { + padding: "0", + marginRight: "6px", + width: "18px", + height: "18px", + display: "inline-flex", + alignItems: "center", + justifyContent: "center", + borderRadius: "4px", + fontSize: "11px", + fontWeight: "600", + lineHeight: "1", + boxSizing: "border-box", + }, + ".cm-completionIcon-table": { + background: "var(--color-blue-100)", + color: "var(--color-blue-600)", + }, + ".cm-completionIcon-table::after": { + content: "'T'", + }, + ".cm-completionIcon-keyword": { + background: "var(--color-green-200)", + color: "var(--color-green-700)", + }, + ".cm-completionIcon-keyword::after": { + content: "'S'", + }, + ".cm-completionIcon-property": { + background: "var(--color-purple-100)", + color: "var(--color-purple-600)", + }, + ".cm-completionIcon-property::after": { + content: "'F'", + }, + ".cm-completionIcon-variable": { + background: "var(--color-yellow-200)", + color: "var(--color-yellow-700)", + }, + ".cm-completionIcon-variable::after": { + content: "'C'", + }, + }), + tableCompletionExtension(metadata.schemas), + columnCompletionExtension({ + schemas: metadata.schemas, + columns: metadata.columns, + }), + jsonbCompletionExtension({ + schemas: metadata.schemas, + jsonbColumns: metadata.jsonbColumns, + sdCache, + sdNotFound, + fetchSchema, + }), + jsonbOperatorExtension(), + sqlCompletionOverride(), + ]; +} From 567b152ffa44a2856834d11b9595ecbae1148631 Mon Sep 17 00:00:00 2001 From: Panthevm Date: Mon, 16 Mar 2026 16:03:53 +0300 Subject: [PATCH 02/55] Fix resize panel --- packages/react-components/package.json | 2 +- .../src/components/code-editor/index.tsx | 14 ++++---- .../src/shadcn/components/ui/resizable.tsx | 36 +++++++------------ 3 files changed, 22 insertions(+), 30 deletions(-) diff --git a/packages/react-components/package.json b/packages/react-components/package.json index 8b2ffb4f..fd2b0c49 100644 --- a/packages/react-components/package.json +++ b/packages/react-components/package.json @@ -89,7 +89,7 @@ "react-day-picker": "^9.14.0", "react-dom": "^19.2.4", "react-hook-form": "^7.71.2", - "react-resizable-panels": "^4.7.2", + "react-resizable-panels": "^3.0.6", "recharts": "3.8.0", "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", diff --git a/packages/react-components/src/components/code-editor/index.tsx b/packages/react-components/src/components/code-editor/index.tsx index 1a56edb3..ac35b19a 100644 --- a/packages/react-components/src/components/code-editor/index.tsx +++ b/packages/react-components/src/components/code-editor/index.tsx @@ -48,8 +48,6 @@ import { EditorView, GutterMarker, gutterLineClass, - highlightActiveLine, - highlightActiveLineGutter, highlightSpecialChars, keymap, lineNumbers, @@ -252,8 +250,11 @@ const baseTheme = EditorView.theme({ backgroundColor: "var(--color-bg-primary)", color: "var(--color-text-secondary)", }, + ".cm-activeLineGutter": { + backgroundColor: "transparent !important", + }, ".cm-activeLine": { - backgroundColor: "rgba(255, 255, 255, 0)", + backgroundColor: "transparent !important", }, ".cm-errorLineGutter": { color: "var(--color-text-error-primary)", @@ -357,8 +358,11 @@ const readOnlyTheme = EditorView.theme({ backgroundColor: "var(--color-bg-secondary)", color: "var(--color-text-secondary)", }, + ".cm-activeLineGutter": { + backgroundColor: "transparent !important", + }, ".cm-activeLine": { - backgroundColor: "rgba(255, 255, 255, 0)", + backgroundColor: "transparent !important", }, ".cm-errorLineGutter": { color: "var(--color-text-error-primary)", @@ -852,8 +856,6 @@ export function CodeEditor({ }), rectangularSelection(), crosshairCursor(), - highlightActiveLine(), - highlightActiveLineGutter(), highlightSelectionMatches(), Prec.highest( keymap.of([ diff --git a/packages/react-components/src/shadcn/components/ui/resizable.tsx b/packages/react-components/src/shadcn/components/ui/resizable.tsx index cb78d5a8..5f41a2a8 100644 --- a/packages/react-components/src/shadcn/components/ui/resizable.tsx +++ b/packages/react-components/src/shadcn/components/ui/resizable.tsx @@ -1,54 +1,44 @@ "use client"; import { GripVerticalIcon } from "lucide-react"; import type * as React from "react"; -import { Group, Panel, Separator } from "react-resizable-panels"; +import * as ResizablePrimitive from "react-resizable-panels"; import { cn } from "#shadcn/lib/utils"; function ResizablePanelGroup({ className, - direction, - orientation = direction, ...props -}: React.ComponentProps & { - /** @deprecated Use `orientation` instead */ - direction?: "horizontal" | "vertical"; -}) { +}: React.ComponentProps) { return ( - ); } function ResizablePanel({ - style, ...props -}: React.ComponentProps) { - return ( - - ); +}: React.ComponentProps) { + return ; } function ResizableHandle({ withHandle, className, ...props -}: React.ComponentProps & { +}: React.ComponentProps & { withHandle?: boolean; }) { return ( - div]:rotate-90", + "bg-border border-x border-border hover:border-bg-link hover:bg-bg-link focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:border-x-0 data-[panel-group-direction=vertical]:border-y data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90", className, )} {...props} @@ -58,7 +48,7 @@ function ResizableHandle({ )} - + ); } From d5ee59d42b55f947488723a744d4053ed2c1de61 Mon Sep 17 00:00:00 2001 From: Panthevm Date: Mon, 16 Mar 2026 17:29:54 +0300 Subject: [PATCH 03/55] Fix typo-body-xs line-height clipping descenders Changed line-height from --font-leading-3 (12px) to --font-leading-4 (16px) for .typo-body-xs class. The previous value caused descender characters (g, y, p, q) to be clipped when combined with overflow: hidden. --- packages/react-components/src/typography.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-components/src/typography.css b/packages/react-components/src/typography.css index 7058e238..dd70e45a 100644 --- a/packages/react-components/src/typography.css +++ b/packages/react-components/src/typography.css @@ -46,7 +46,7 @@ body { font-size: var(--font-size-xs); font-family: var(--font-family-sans); font-weight: var(--font-weight-normal); - line-height: var(--font-leading-3); + line-height: var(--font-leading-4); } .typo-label-xs { font-size: var(--font-size-xs); From d4808f87f1de455db68e45066b739acd9c475a23 Mon Sep 17 00:00:00 2001 From: Panthevm Date: Mon, 16 Mar 2026 17:47:21 +0300 Subject: [PATCH 04/55] Add HTTP method syntax highlighting to code editor Color GET/POST/PUT/PATCH/DELETE in raw HTTP editor using utility color tokens matching the request method selector. --- .../src/components/code-editor/http/index.ts | 73 ++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/packages/react-components/src/components/code-editor/http/index.ts b/packages/react-components/src/components/code-editor/http/index.ts index b8e93045..1a422560 100644 --- a/packages/react-components/src/components/code-editor/http/index.ts +++ b/packages/react-components/src/components/code-editor/http/index.ts @@ -4,11 +4,21 @@ import { type Language, LanguageSupport, LRLanguage, + syntaxTree, } from "@codemirror/language"; +import { RangeSetBuilder } from "@codemirror/state"; +import { + Decoration, + type DecorationSet, + EditorView, + ViewPlugin, + type ViewUpdate, +} from "@codemirror/view"; import { parseMixed } from "@lezer/common"; import { styleTags, tags } from "@lezer/highlight"; import type { LRParser } from "@lezer/lr"; import { parser } from "./grammar/http"; +import { HttpRequestMethod } from "./grammar/http.terms"; function makeParser( bodyLanguages: (contentType: string) => Language | null, @@ -78,10 +88,71 @@ function makeParser( }); } +const methodDecorations: Record = { + GET: Decoration.mark({ class: "cm-http-method-get" }), + POST: Decoration.mark({ class: "cm-http-method-post" }), + PUT: Decoration.mark({ class: "cm-http-method-put" }), + PATCH: Decoration.mark({ class: "cm-http-method-patch" }), + DELETE: Decoration.mark({ class: "cm-http-method-delete" }), +}; + +function buildMethodDecorations(view: EditorView): DecorationSet { + const builder = new RangeSetBuilder(); + const tree = syntaxTree(view.state); + tree.iterate({ + enter(node) { + if (node.type.id === HttpRequestMethod) { + const text = view.state.sliceDoc(node.from, node.to).toUpperCase(); + const deco = methodDecorations[text]; + if (deco) { + builder.add(node.from, node.to, deco); + } + } + }, + }); + return builder.finish(); +} + +const httpMethodHighlighter = ViewPlugin.fromClass( + class { + decorations: DecorationSet; + constructor(view: EditorView) { + this.decorations = buildMethodDecorations(view); + } + update(update: ViewUpdate) { + if (update.docChanged || update.viewportChanged) { + this.decorations = buildMethodDecorations(update.view); + } + } + }, + { decorations: (v) => v.decorations }, +); + +const httpMethodTheme = EditorView.baseTheme({ + ".cm-http-method-get": { + color: "var(--color-utility-green)", + }, + ".cm-http-method-post": { + color: "var(--color-utility-yellow)", + }, + ".cm-http-method-put": { + color: "var(--color-utility-blue)", + }, + ".cm-http-method-patch": { + color: "var(--color-utility-violet)", + }, + ".cm-http-method-delete": { + color: "var(--color-utility-red)", + }, +}); + function http(bodyLanguages: (contentType: string) => Language | null) { const parser = makeParser(bodyLanguages); const language = LRLanguage.define({ parser: parser }); - return new LanguageSupport(language, []); + return new LanguageSupport(language, [ + httpMethodHighlighter, + httpMethodTheme, + ]); } export { http }; From 49e0b89ddf50d085a572a6e882129708e8fd4be0 Mon Sep 17 00:00:00 2001 From: Panthevm Date: Mon, 16 Mar 2026 19:50:12 +0300 Subject: [PATCH 05/55] Add FHIR resource autocomplete to CodeEditor - New getStructureDefinition prop on CodeEditor - Autocomplete suggests fields based on StructureDefinition differential - Resolves base definitions recursively (Resource, DomainResource, Element) - Expands choice types to FHIR format (deceasedBoolean, deceasedDateTime) - Adds primitive extensions (_birthDate, _active, etc.) - Apply inserts typed snippets ("name": [{}], "active": , "gender": "") - Caches loaded StructureDefinitions --- .../components/code-editor/fhir-completion.ts | 409 ++++++++++++++++++ .../src/components/code-editor/index.tsx | 25 ++ 2 files changed, 434 insertions(+) create mode 100644 packages/react-components/src/components/code-editor/fhir-completion.ts diff --git a/packages/react-components/src/components/code-editor/fhir-completion.ts b/packages/react-components/src/components/code-editor/fhir-completion.ts new file mode 100644 index 00000000..19855434 --- /dev/null +++ b/packages/react-components/src/components/code-editor/fhir-completion.ts @@ -0,0 +1,409 @@ +import type { + Completion, + CompletionContext, + CompletionResult, + CompletionSource, +} from "@codemirror/autocomplete"; +import { jsonLanguage } from "@codemirror/lang-json"; +import type { Extension } from "@codemirror/state"; + +// ── Types ────────────────────────────────────────────────────────────── + +interface FhirElementType { + code: string; + targetProfile?: string[]; +} + +interface FhirElement { + path: string; + short?: string; + definition?: string; + min?: number; + max?: string; + type?: FhirElementType[]; + binding?: { valueSet: string; strength: string }; +} + +interface StructureDefinition { + type: string; + name?: string; + baseDefinition?: string; + differential?: { element: FhirElement[] }; +} + +export type GetStructureDefinition = ( + type: string, +) => Promise; + +// ── Cache ────────────────────────────────────────────────────────────── + +const sdCache = new Map(); +const pendingRequests = new Map>(); + +async function getCachedSD( + type: string, + getSD: GetStructureDefinition, +): Promise { + if (sdCache.has(type)) return sdCache.get(type) ?? null; + + let pending = pendingRequests.get(type); + if (!pending) { + pending = getSD(type) + .then((sd) => { + sdCache.set(type, sd); + pendingRequests.delete(type); + return sd; + }) + .catch(() => { + pendingRequests.delete(type); + sdCache.set(type, null); + return null; + }); + pendingRequests.set(type, pending); + } + return pending; +} + +// ── JSON path at cursor ──────────────────────────────────────────────── + +function getJsonPathAtCursor(doc: string, pos: number): string[] { + const path: string[] = []; + let inString = false; + let escape = false; + let currentKey = ""; + let collectingKey = false; + let lastKey = ""; + + for (let i = 0; i < pos; i++) { + const ch = doc[i]; + + if (escape) { + if (collectingKey) currentKey += ch; + escape = false; + continue; + } + if (ch === "\\") { + escape = true; + if (collectingKey) currentKey += ch; + continue; + } + if (ch === '"') { + if (!inString) { + inString = true; + collectingKey = true; + currentKey = ""; + } else { + inString = false; + if (collectingKey) { + lastKey = currentKey; + collectingKey = false; + } + } + continue; + } + if (inString) { + if (collectingKey) currentKey += ch; + continue; + } + if (ch === "{") { + if (lastKey) path.push(lastKey); + lastKey = ""; + } else if (ch === "}") { + path.pop(); + lastKey = ""; + } else if (ch === ",") { + lastKey = ""; + } + } + return path; +} + +// ── Element helpers ──────────────────────────────────────────────────── + +function fieldName(element: FhirElement): string { + const parts = element.path.split("."); + return (parts[parts.length - 1] ?? "").replace("[x]", ""); +} + +function directChildren( + elements: FhirElement[], + parentPath: string, +): FhirElement[] { + const prefix = `${parentPath}.`; + return elements.filter((el) => { + if (!el.path.startsWith(prefix)) return false; + const rest = el.path.slice(prefix.length); + return !rest.includes("."); + }); +} + +function findElement( + elements: FhirElement[], + parentPath: string, + key: string, +): FhirElement | undefined { + // Direct match + const direct = elements.find((el) => { + if (!el.path.startsWith(`${parentPath}.`)) return false; + const name = fieldName(el); + return name === key || name.toLowerCase() === key.toLowerCase(); + }); + if (direct) return direct; + + // Choice type match: key "deceasedBoolean" → element "deceased[x]" with type boolean + return elements.find((el) => { + if (!el.path.endsWith("[x]")) return false; + if (!el.path.startsWith(`${parentPath}.`)) return false; + const baseName = fieldName(el); + if (!key.toLowerCase().startsWith(baseName.toLowerCase())) return false; + const typeSuffix = key.slice(baseName.length).toLowerCase(); + return el.type?.some((t) => t.code.toLowerCase() === typeSuffix) ?? false; + }); +} + +// ── Resolve completions at path ──────────────────────────────────────── + +// Collect all elements including inherited from base definitions +async function collectAllElements( + type: string, + getSD: GetStructureDefinition, +): Promise<{ elements: FhirElement[]; basePath: string } | null> { + const sd = await getCachedSD(type, getSD); + if (!sd?.differential?.element) return null; + + const elements = [...sd.differential.element]; + + // Recursively load base definition elements + if (sd.baseDefinition) { + const base = await collectAllElements(sd.baseDefinition, getSD); + if (base) { + for (const baseEl of base.elements) { + const remappedPath = baseEl.path.replace( + new RegExp(`^${base.basePath}`), + sd.type, + ); + if (!elements.some((e) => e.path === remappedPath)) { + elements.push({ ...baseEl, path: remappedPath }); + } + } + } + } + + return { elements, basePath: sd.type }; +} + +async function resolveCompletions( + path: string[], + resourceType: string, + getSD: GetStructureDefinition, +): Promise { + const result = await collectAllElements(resourceType, getSD); + if (!result) return []; + + let currentPath = resourceType; + let currentElements = result.elements; + + for (const key of path) { + if (key === "resourceType") return []; + + const el = findElement(currentElements, currentPath, key); + if (!el?.type?.[0]) return []; + + const typeCode = el.type[0].code; + + if (typeCode === "BackboneElement") { + currentPath = el.path; + continue; + } + + // Complex type — load with inheritance + const typeResult = await collectAllElements(typeCode, getSD); + if (!typeResult) return []; + currentPath = typeResult.basePath; + currentElements = typeResult.elements; + } + + const children = directChildren(currentElements, currentPath); + const completions: Completion[] = []; + + for (const el of children) { + const name = fieldName(el); + const isChoiceType = el.path.endsWith("[x]"); + + if (isChoiceType && el.type && el.type.length > 0) { + // Expand choice type into concrete FHIR variants + for (const t of el.type) { + const expanded: FhirElement = { + ...el, + path: el.path.replace("[x]", t.code.charAt(0).toUpperCase() + t.code.slice(1)), + type: [t], + }; + completions.push(toCompletion(expanded)); + } + } else { + completions.push(toCompletion(el)); + } + + // Add primitive extension (_field) for primitive types + const firstTypeCode = el.type?.[0]?.code; + if (!isChoiceType && el.type?.length === 1 && firstTypeCode && PRIMITIVE_TYPES.has(firstTypeCode)) { + const extCompletion: Completion = { + label: `_${name}`, + type: "property", + detail: "Element [0..1]", + boost: -1, + }; + extCompletion.info = "Primitive element extension"; + completions.push(extCompletion); + } + } + + return completions; +} + +const PRIMITIVE_TYPES = new Set([ + "boolean", + "integer", + "string", + "decimal", + "uri", + "url", + "canonical", + "base64Binary", + "instant", + "date", + "dateTime", + "time", + "code", + "oid", + "id", + "markdown", + "unsignedInt", + "positiveInt", + "uuid", + "xhtml", +]); + +function applySnippet(element: FhirElement): string { + const name = fieldName(element); + const isArray = element.max === "*"; + const typeCode = element.type?.[0]?.code; + + if (!typeCode) return `"${name}": `; + + if (isArray) { + if (PRIMITIVE_TYPES.has(typeCode)) { + return `"${name}": []`; + } + return `"${name}": [{}]`; + } + + if (typeCode === "boolean") return `"${name}": `; + if ( + typeCode === "integer" || + typeCode === "decimal" || + typeCode === "positiveInt" || + typeCode === "unsignedInt" + ) { + return `"${name}": `; + } + if (PRIMITIVE_TYPES.has(typeCode)) return `"${name}": ""`; + // Complex type — object + return `"${name}": {}`; +} + +function toCompletion(element: FhirElement): Completion { + const name = fieldName(element); + const types = element.type?.map((t) => t.code).join(" | ") ?? ""; + + const snippet = applySnippet(element); + const completion: Completion = { + label: name, + type: "property", + detail: types, + boost: element.min && element.min > 0 ? 2 : 0, + apply: (view, _completion, from, to) => { + // Expand range to include surrounding quotes + const doc = view.state.doc.toString(); + let actualFrom = from; + let actualTo = to; + if (actualFrom > 0 && doc[actualFrom - 1] === '"') actualFrom--; + if (actualTo < doc.length && doc[actualTo] === '"') actualTo++; + + // Find cursor position (inside {} or [] or at end of value) + const cursorOffset = snippet.includes("{}") + ? snippet.indexOf("{}") + 1 + : snippet.includes("[]") + ? snippet.indexOf("[]") + 1 + : snippet.includes('""') + ? snippet.indexOf('""') + 1 + : snippet.length; + + view.dispatch({ + changes: { from: actualFrom, to: actualTo, insert: snippet }, + selection: { anchor: actualFrom + cursorOffset }, + }); + }, + }; + if (element.short) completion.info = element.short; + return completion; +} + +// ── Completion source ────────────────────────────────────────────────── + +export function fhirCompletionSource( + getSD: GetStructureDefinition, +): CompletionSource { + return async ( + context: CompletionContext, + ): Promise => { + const { state, pos } = context; + const doc = state.doc.toString(); + + console.log("[fhir-completion] source called, pos:", pos); + + const rtMatch = doc.match(/"resourceType"\s*:\s*"([^"]+)"/); + if (!rtMatch?.[1]) { + console.log("[fhir-completion] no resourceType found"); + return null; + } + + const line = state.doc.lineAt(pos); + const beforeCursor = line.text.slice(0, pos - line.from).trimStart(); + + const isPropertyPosition = + beforeCursor === "" || + beforeCursor === '"' || + /^"[\w]*$/.test(beforeCursor); + + console.log("[fhir-completion] beforeCursor:", JSON.stringify(beforeCursor), "isProperty:", isPropertyPosition); + + if (!isPropertyPosition) return null; + + const path = getJsonPathAtCursor(doc, pos); + console.log("[fhir-completion] path:", path, "resourceType:", rtMatch[1]); + const completions = await resolveCompletions(path, rtMatch[1], getSD); + console.log("[fhir-completion] completions:", completions.length); + if (completions.length === 0) return null; + + // Match word being typed, skip opening quote for label matching + const word = context.matchBefore(/"?\w*/); + let from = word?.from ?? pos; + if (from < doc.length && doc[from] === '"') from++; + + return { + from, + options: completions, + validFor: /^\w*$/, + }; + }; +} + +// ── Public API ───────────────────────────────────────────────────────── + +export function buildFhirCompletionExtension( + getSD: GetStructureDefinition, +): Extension { + const source = fhirCompletionSource(getSD); + return jsonLanguage.data.of({ autocomplete: source }); +} diff --git a/packages/react-components/src/components/code-editor/index.tsx b/packages/react-components/src/components/code-editor/index.tsx index ac35b19a..343e6810 100644 --- a/packages/react-components/src/components/code-editor/index.tsx +++ b/packages/react-components/src/components/code-editor/index.tsx @@ -67,6 +67,10 @@ import { buildSqlCompletionExtensions, fetchSqlMetadata, } from "./sql-completion"; +import { + buildFhirCompletionExtension, + type GetStructureDefinition, +} from "./fhir-completion"; // --- Issue lines: gutter highlighting, line background, hover tooltip --- @@ -773,6 +777,7 @@ type CodeEditorProps = { lintGutter?: boolean; lineNumbers?: boolean; sql?: SqlConfig; + getStructureDefinition?: GetStructureDefinition; }; export type CodeEditorView = EditorView; @@ -783,6 +788,8 @@ export type { SqlMetadata, } from "./sql-completion"; +export type { GetStructureDefinition } from "./fhir-completion"; + export function CodeEditor({ defaultValue, currentValue, @@ -799,6 +806,7 @@ export function CodeEditor({ lintGutter: enableLintGutter = true, lineNumbers: enableLineNumbers = true, sql, + getStructureDefinition, }: CodeEditorProps) { const domRef = React.useRef(null); const [view, setView] = React.useState(null); @@ -812,6 +820,7 @@ export function CodeEditor({ const themeCompartment = React.useRef(new Compartment()); const additionalExtensionsCompartment = React.useRef(new Compartment()); const sqlCompletionCompartment = React.useRef(new Compartment()); + const fhirCompletionCompartment = React.useRef(new Compartment()); const [sqlFunctions, setSqlFunctions] = React.useState< string[] | undefined >(); @@ -888,6 +897,7 @@ export function CodeEditor({ onUpdateComparment.current.of([]), additionalExtensionsCompartment.current.of([]), sqlCompletionCompartment.current.of([]), + fhirCompletionCompartment.current.of([]), ], }), }); @@ -939,6 +949,21 @@ export function CodeEditor({ }; }, [view, sql]); + React.useEffect(() => { + if (!view) return; + if (getStructureDefinition) { + view.dispatch({ + effects: fhirCompletionCompartment.current.reconfigure( + buildFhirCompletionExtension(getStructureDefinition), + ), + }); + } else { + view.dispatch({ + effects: fhirCompletionCompartment.current.reconfigure([]), + }); + } + }, [view, getStructureDefinition]); + React.useEffect(() => { if (viewCallback && view) { viewCallback(view); From 97557175166f04491dd3c010bd78ebcfbe14a5bd Mon Sep 17 00:00:00 2001 From: Panthevm Date: Tue, 17 Mar 2026 18:38:44 +0300 Subject: [PATCH 06/55] Update code editor --- .../components/code-editor/fhir-completion.ts | 549 +++++++++++++++--- .../src/components/code-editor/http/index.ts | 249 +++++++- .../src/components/code-editor/index.tsx | 153 +++-- .../components/code-editor/sql-completion.ts | 99 ++-- pnpm-lock.yaml | 14 +- 5 files changed, 878 insertions(+), 186 deletions(-) diff --git a/packages/react-components/src/components/code-editor/fhir-completion.ts b/packages/react-components/src/components/code-editor/fhir-completion.ts index 19855434..c6abde28 100644 --- a/packages/react-components/src/components/code-editor/fhir-completion.ts +++ b/packages/react-components/src/components/code-editor/fhir-completion.ts @@ -5,7 +5,9 @@ import type { CompletionSource, } from "@codemirror/autocomplete"; import { jsonLanguage } from "@codemirror/lang-json"; +import { yamlLanguage } from "@codemirror/lang-yaml"; import type { Extension } from "@codemirror/state"; +import type { EditorView } from "@codemirror/view"; // ── Types ────────────────────────────────────────────────────────────── @@ -31,35 +33,106 @@ interface StructureDefinition { differential?: { element: FhirElement[] }; } -export type GetStructureDefinition = ( - type: string, -) => Promise; +export interface StructureDefinitionSearchParams { + type?: string; + url?: string; + derivation?: string; + "derivation:missing"?: string; + kind?: string; + _count?: string; + _elements?: string; +} + +export type GetStructureDefinitions = ( + params: StructureDefinitionSearchParams, +) => Promise; // ── Cache ────────────────────────────────────────────────────────────── const sdCache = new Map(); const pendingRequests = new Map>(); +const listCache = new Map(); +const pendingListRequests = new Map>(); + +const SD_ELEMENTS = "differential,type,name,baseDefinition"; + +function cacheKey(params: StructureDefinitionSearchParams): string { + return JSON.stringify(params); +} + +async function getCachedSDList( + params: StructureDefinitionSearchParams, + getSDs: GetStructureDefinitions, +): Promise { + const key = cacheKey(params); + if (listCache.has(key)) return listCache.get(key) ?? []; + + let pending = pendingListRequests.get(key); + if (!pending) { + pending = getSDs(params) + .then((list) => { + listCache.set(key, list); + pendingListRequests.delete(key); + // Only cache individual SDs that have differential + for (const sd of list) { + if (sd.differential?.element) { + sdCache.set(sd.type, sd); + } + } + return list; + }) + .catch(() => { + pendingListRequests.delete(key); + listCache.set(key, []); + return []; + }); + pendingListRequests.set(key, pending); + } + return pending; +} async function getCachedSD( type: string, - getSD: GetStructureDefinition, + getSDs: GetStructureDefinitions, ): Promise { if (sdCache.has(type)) return sdCache.get(type) ?? null; - let pending = pendingRequests.get(type); + const key = `single:${type}`; + let pending = pendingRequests.get(key); if (!pending) { - pending = getSD(type) + const isUrl = type.includes("/"); + const searchByType = (params: StructureDefinitionSearchParams) => + getSDs(params).then((list) => list[0] ?? null); + + pending = ( + isUrl + ? searchByType({ url: type, _elements: SD_ELEMENTS, _count: "1" }) + : searchByType({ + type, + derivation: "specialization", + _elements: SD_ELEMENTS, + _count: "1", + }).then( + (sd) => + sd ?? + searchByType({ + type, + "derivation:missing": "true", + _elements: SD_ELEMENTS, + _count: "1", + }), + ) + ) .then((sd) => { sdCache.set(type, sd); - pendingRequests.delete(type); + pendingRequests.delete(key); return sd; }) .catch(() => { - pendingRequests.delete(type); - sdCache.set(type, null); + pendingRequests.delete(key); return null; }); - pendingRequests.set(type, pending); + pendingRequests.set(key, pending); } return pending; } @@ -69,7 +142,7 @@ async function getCachedSD( function getJsonPathAtCursor(doc: string, pos: number): string[] { const path: string[] = []; let inString = false; - let escape = false; + let isEscaped = false; let currentKey = ""; let collectingKey = false; let lastKey = ""; @@ -77,13 +150,13 @@ function getJsonPathAtCursor(doc: string, pos: number): string[] { for (let i = 0; i < pos; i++) { const ch = doc[i]; - if (escape) { + if (isEscaped) { if (collectingKey) currentKey += ch; - escape = false; + isEscaped = false; continue; } if (ch === "\\") { - escape = true; + isEscaped = true; if (collectingKey) currentKey += ch; continue; } @@ -118,6 +191,57 @@ function getJsonPathAtCursor(doc: string, pos: number): string[] { return path; } +// ── YAML path at cursor ───────────────────────────────────────────────── + +function getYamlPathAtCursor(doc: string, pos: number): string[] { + const lines = doc.slice(0, pos).split("\n"); + const currentLine = lines[lines.length - 1] ?? ""; + const currentIndent = currentLine.search(/\S/); + + // Walk backwards to build path from indentation + const path: string[] = []; + let targetIndent = currentIndent; + + for (let i = lines.length - 2; i >= 0; i--) { + const line = lines[i] ?? ""; + const trimmed = line.trimStart(); + if (!trimmed || trimmed.startsWith("#")) continue; + + const indent = line.search(/\S/); + // Strip leading "- " for array items + const content = trimmed.startsWith("- ") ? trimmed.slice(2) : trimmed; + const colonIdx = content.indexOf(":"); + + if (indent < targetIndent && colonIdx > 0) { + const key = content.slice(0, colonIdx).trim(); + path.unshift(key); + targetIndent = indent; + } + } + + return path; +} + +function getYamlResourceType(doc: string): string | null { + const match = doc.match(/^resourceType:\s*(\S+)/m); + return match?.[1] ?? null; +} + +function isYamlPropertyPosition(beforeCursor: string): boolean { + const trimmed = beforeCursor.trimStart(); + // Empty line, or typing a key (no colon yet), or after "- " + if (trimmed === "" || trimmed === "-" || trimmed === "- ") return true; + if (trimmed.includes(":")) return false; + // Typing a word without colon = key position + return /^(-\s+)?[\w]*$/.test(trimmed); +} + +function isYamlValuePosition(beforeCursor: string): string | null { + const match = beforeCursor.match(/(\w+):\s*(\S*)$/); + if (match) return match[1] ?? null; + return null; +} + // ── Element helpers ──────────────────────────────────────────────────── function fieldName(element: FhirElement): string { @@ -166,16 +290,24 @@ function findElement( // Collect all elements including inherited from base definitions async function collectAllElements( type: string, - getSD: GetStructureDefinition, + getSDs: GetStructureDefinitions, ): Promise<{ elements: FhirElement[]; basePath: string } | null> { - const sd = await getCachedSD(type, getSD); + const sd = await getCachedSD(type, getSDs); + console.log( + "[fhir] collectAllElements:", + type, + "sd:", + sd?.type, + "elements:", + sd?.differential?.element?.length, + ); if (!sd?.differential?.element) return null; const elements = [...sd.differential.element]; // Recursively load base definition elements if (sd.baseDefinition) { - const base = await collectAllElements(sd.baseDefinition, getSD); + const base = await collectAllElements(sd.baseDefinition, getSDs); if (base) { for (const baseEl of base.elements) { const remappedPath = baseEl.path.replace( @@ -192,12 +324,12 @@ async function collectAllElements( return { elements, basePath: sd.type }; } -async function resolveCompletions( +async function resolveElements( path: string[], resourceType: string, - getSD: GetStructureDefinition, -): Promise { - const result = await collectAllElements(resourceType, getSD); + getSDs: GetStructureDefinitions, +): Promise { + const result = await collectAllElements(resourceType, getSDs); if (!result) return []; let currentPath = resourceType; @@ -216,48 +348,62 @@ async function resolveCompletions( continue; } - // Complex type — load with inheritance - const typeResult = await collectAllElements(typeCode, getSD); + const typeResult = await collectAllElements(typeCode, getSDs); if (!typeResult) return []; currentPath = typeResult.basePath; currentElements = typeResult.elements; } + // Expand choice types and collect const children = directChildren(currentElements, currentPath); - const completions: Completion[] = []; + const expanded: FhirElement[] = []; for (const el of children) { - const name = fieldName(el); const isChoiceType = el.path.endsWith("[x]"); - if (isChoiceType && el.type && el.type.length > 0) { - // Expand choice type into concrete FHIR variants for (const t of el.type) { - const expanded: FhirElement = { + expanded.push({ ...el, - path: el.path.replace("[x]", t.code.charAt(0).toUpperCase() + t.code.slice(1)), + path: el.path.replace( + "[x]", + t.code.charAt(0).toUpperCase() + t.code.slice(1), + ), type: [t], - }; - completions.push(toCompletion(expanded)); + }); } } else { - completions.push(toCompletion(el)); + expanded.push(el); } + } + + return expanded; +} - // Add primitive extension (_field) for primitive types +function elementsToCompletions( + elements: FhirElement[], + mapFn: (el: FhirElement) => Completion, +): Completion[] { + const completions: Completion[] = []; + for (const el of elements) { + completions.push(mapFn(el)); + // Primitive extensions + const name = fieldName(el); const firstTypeCode = el.type?.[0]?.code; - if (!isChoiceType && el.type?.length === 1 && firstTypeCode && PRIMITIVE_TYPES.has(firstTypeCode)) { - const extCompletion: Completion = { + if ( + el.type?.length === 1 && + firstTypeCode && + PRIMITIVE_TYPES.has(firstTypeCode) + ) { + const ext: Completion = { label: `_${name}`, type: "property", - detail: "Element [0..1]", + detail: "Element", boost: -1, }; - extCompletion.info = "Primitive element extension"; - completions.push(extCompletion); + ext.info = "Primitive element extension"; + completions.push(ext); } } - return completions; } @@ -284,63 +430,94 @@ const PRIMITIVE_TYPES = new Set([ "xhtml", ]); -function applySnippet(element: FhirElement): string { - const name = fieldName(element); +type SnippetKind = + | "array-complex" + | "array-primitive" + | "object" + | "string" + | "number" + | "bare"; + +function snippetKind(element: FhirElement): SnippetKind { const isArray = element.max === "*"; const typeCode = element.type?.[0]?.code; - - if (!typeCode) return `"${name}": `; - - if (isArray) { - if (PRIMITIVE_TYPES.has(typeCode)) { - return `"${name}": []`; - } - return `"${name}": [{}]`; - } - - if (typeCode === "boolean") return `"${name}": `; + if (!typeCode) return "bare"; + if (isArray) + return PRIMITIVE_TYPES.has(typeCode) ? "array-primitive" : "array-complex"; if ( + typeCode === "boolean" || typeCode === "integer" || typeCode === "decimal" || typeCode === "positiveInt" || typeCode === "unsignedInt" - ) { - return `"${name}": `; + ) + return "number"; + if (PRIMITIVE_TYPES.has(typeCode)) return "string"; + return "object"; +} + +function buildSnippet( + name: string, + kind: SnippetKind, + indent: string, +): { text: string; cursorOffset: number } { + const inner = indent + " "; + const innerInner = inner + " "; + switch (kind) { + case "array-complex": { + const text = `"${name}": [\n${inner}{\n${innerInner}\n${inner}}\n${indent}]`; + return { + text, + cursorOffset: text.indexOf(innerInner) + innerInner.length, + }; + } + case "array-primitive": { + const text = `"${name}": []`; + return { text, cursorOffset: text.length - 1 }; + } + case "object": { + const text = `"${name}": {\n${inner}\n${indent}}`; + return { text, cursorOffset: text.indexOf(inner + "\n") + inner.length }; + } + case "string": { + const text = `"${name}": ""`; + return { text, cursorOffset: text.length - 1 }; + } + case "number": + case "bare": + default: { + const text = `"${name}": `; + return { text, cursorOffset: text.length }; + } } - if (PRIMITIVE_TYPES.has(typeCode)) return `"${name}": ""`; - // Complex type — object - return `"${name}": {}`; } function toCompletion(element: FhirElement): Completion { const name = fieldName(element); const types = element.type?.map((t) => t.code).join(" | ") ?? ""; + const kind = snippetKind(element); - const snippet = applySnippet(element); const completion: Completion = { label: name, type: "property", detail: types, boost: element.min && element.min > 0 ? 2 : 0, apply: (view, _completion, from, to) => { - // Expand range to include surrounding quotes const doc = view.state.doc.toString(); let actualFrom = from; let actualTo = to; if (actualFrom > 0 && doc[actualFrom - 1] === '"') actualFrom--; if (actualTo < doc.length && doc[actualTo] === '"') actualTo++; - // Find cursor position (inside {} or [] or at end of value) - const cursorOffset = snippet.includes("{}") - ? snippet.indexOf("{}") + 1 - : snippet.includes("[]") - ? snippet.indexOf("[]") + 1 - : snippet.includes('""') - ? snippet.indexOf('""') + 1 - : snippet.length; + // Detect current indentation + const line = view.state.doc.lineAt(actualFrom); + const lineText = line.text; + const indent = lineText.match(/^(\s*)/)?.[1] ?? ""; + + const { text, cursorOffset } = buildSnippet(name, kind, indent); view.dispatch({ - changes: { from: actualFrom, to: actualTo, insert: snippet }, + changes: { from: actualFrom, to: actualTo, insert: text }, selection: { anchor: actualFrom + cursorOffset }, }); }, @@ -351,8 +528,15 @@ function toCompletion(element: FhirElement): Completion { // ── Completion source ────────────────────────────────────────────────── +// Check if cursor is in a value position (after "key": or key: ) +function isValuePosition(beforeCursor: string): string | null { + const match = beforeCursor.match(/"?(\w+)"?\s*:\s*"?([^"]*)?$/); + if (match) return match[1] ?? null; + return null; +} + export function fhirCompletionSource( - getSD: GetStructureDefinition, + getSDs: GetStructureDefinitions, ): CompletionSource { return async ( context: CompletionContext, @@ -360,50 +544,231 @@ export function fhirCompletionSource( const { state, pos } = context; const doc = state.doc.toString(); - console.log("[fhir-completion] source called, pos:", pos); - - const rtMatch = doc.match(/"resourceType"\s*:\s*"([^"]+)"/); - if (!rtMatch?.[1]) { - console.log("[fhir-completion] no resourceType found"); - return null; - } - const line = state.doc.lineAt(pos); const beforeCursor = line.text.slice(0, pos - line.from).trimStart(); + // Check if we're in a value position for resourceType + const valueKey = isValuePosition(beforeCursor); + if (valueKey === "resourceType") { + const sds = await getCachedSDList( + { + derivation: "specialization", + kind: "resource", + _elements: "type", + _count: "500", + }, + getSDs, + ); + const options: Completion[] = sds.map((sd) => ({ + label: sd.type, + type: "type", + apply: (view: EditorView, _c: Completion, from: number, to: number) => { + const d = view.state.doc.toString(); + let actualTo = to; + if (actualTo < d.length && d[actualTo] === '"') actualTo++; + view.dispatch({ + changes: { from, to: actualTo, insert: `${sd.type}"` }, + selection: { anchor: from + sd.type.length + 1 }, + }); + }, + })); + if (options.length === 0) return null; + const word = context.matchBefore(/[\w]*/); + return { from: word?.from ?? pos, options, validFor: /^\w*$/ }; + } + + // Property name position — with or without quotes const isPropertyPosition = beforeCursor === "" || beforeCursor === '"' || - /^"[\w]*$/.test(beforeCursor); - - console.log("[fhir-completion] beforeCursor:", JSON.stringify(beforeCursor), "isProperty:", isPropertyPosition); + /^"?[\w]*$/.test(beforeCursor); if (!isPropertyPosition) return null; const path = getJsonPathAtCursor(doc, pos); - console.log("[fhir-completion] path:", path, "resourceType:", rtMatch[1]); - const completions = await resolveCompletions(path, rtMatch[1], getSD); - console.log("[fhir-completion] completions:", completions.length); + const rtMatch = doc.match(/"resourceType"\s*:\s*"([^"]+)"/) ?? + doc.match(/resourceType\s*:\s*"([^"]+)"/); + const resourceType = rtMatch?.[1]; + + let completions: Completion[]; + if (resourceType) { + const elements = await resolveElements(path, resourceType, getSDs); + completions = elementsToCompletions(elements, toCompletion); + } else if (path.length === 0) { + const rtCompletion: Completion = { + label: "resourceType", + type: "property", + detail: "string", + boost: 10, + apply: (view, _completion, from, to) => { + const d = view.state.doc.toString(); + let actualFrom = from; + let actualTo = to; + if (actualFrom > 0 && d[actualFrom - 1] === '"') actualFrom--; + if (actualTo < d.length && d[actualTo] === '"') actualTo++; + const text = '"resourceType": ""'; + view.dispatch({ + changes: { from: actualFrom, to: actualTo, insert: text }, + selection: { anchor: actualFrom + text.length - 1 }, + }); + }, + }; + rtCompletion.info = "FHIR resource type"; + const domainElements = await resolveElements( + path, + "DomainResource", + getSDs, + ); + completions = [ + rtCompletion, + ...elementsToCompletions(domainElements, toCompletion), + ]; + } else { + return null; + } + if (completions.length === 0) return null; - // Match word being typed, skip opening quote for label matching const word = context.matchBefore(/"?\w*/); let from = word?.from ?? pos; if (from < doc.length && doc[from] === '"') from++; - return { - from, - options: completions, - validFor: /^\w*$/, - }; + return { from, options: completions, validFor: /^\w*$/ }; + }; +} + +// ── YAML completion helpers ───────────────────────────────────────────── + +function toYamlFieldCompletion(element: FhirElement): Completion { + const name = fieldName(element); + const types = element.type?.map((t) => t.code).join(" | ") ?? ""; + const isArray = element.max === "*"; + const typeCode = element.type?.[0]?.code; + const isPrimitive = typeCode ? PRIMITIVE_TYPES.has(typeCode) : false; + + const completion: Completion = { + label: name, + type: "property", + detail: types, + boost: element.min && element.min > 0 ? 2 : 0, + apply: (view, _completion, from, to) => { + const line = view.state.doc.lineAt(from); + // Indent = everything before cursor position on this line + const charsBeforeFrom = from - line.from; + const indent = " ".repeat(charsBeforeFrom); + const inner = `${indent} `; + + let text: string; + let cursorOffset: number; + const isString = typeCode === "string" || typeCode === "code" || typeCode === "uri" || typeCode === "url" || typeCode === "canonical" || typeCode === "id" || typeCode === "markdown" || typeCode === "oid" || typeCode === "uuid" || typeCode === "base64Binary" || typeCode === "xhtml"; + if (isArray) { + text = `${name}:\n${inner}- `; + cursorOffset = text.length; + } else if (isString) { + text = `${name}: ''`; + cursorOffset = text.length - 1; + } else if (isPrimitive) { + text = `${name}: `; + cursorOffset = text.length; + } else { + text = `${name}:\n${inner}`; + cursorOffset = text.length; + } + + view.dispatch({ + changes: { from, to, insert: text }, + selection: { anchor: from + cursorOffset }, + }); + }, + }; + if (element.short) completion.info = element.short; + return completion; +} + +// ── YAML completion source ────────────────────────────────────────────── + +export function yamlFhirCompletionSource( + getSDs: GetStructureDefinitions, +): CompletionSource { + return async ( + context: CompletionContext, + ): Promise => { + const { state, pos } = context; + const doc = state.doc.toString(); + + const line = state.doc.lineAt(pos); + const beforeCursor = line.text.slice(0, pos - line.from); + + // Value position for resourceType + const valueKey = isYamlValuePosition(beforeCursor); + if (valueKey === "resourceType") { + const sds = await getCachedSDList( + { + derivation: "specialization", + kind: "resource", + _elements: "type", + _count: "500", + }, + getSDs, + ); + const options: Completion[] = sds.map((sd) => ({ + label: sd.type, + type: "type", + })); + if (options.length === 0) return null; + const word = context.matchBefore(/[\w]*/); + return { from: word?.from ?? pos, options, validFor: /^\w*$/ }; + } + + if (!isYamlPropertyPosition(beforeCursor)) return null; + + const path = getYamlPathAtCursor(doc, pos); + const resourceType = getYamlResourceType(doc); + + let completions: Completion[]; + if (resourceType) { + const elements = await resolveElements(path, resourceType, getSDs); + completions = elementsToCompletions(elements, toYamlFieldCompletion); + } else if (path.length === 0) { + const rtCompletion: Completion = { + label: "resourceType", + type: "property", + detail: "string", + boost: 10, + apply: "resourceType: ", + }; + rtCompletion.info = "FHIR resource type"; + const domainElements = await resolveElements( + path, + "DomainResource", + getSDs, + ); + completions = [ + rtCompletion, + ...elementsToCompletions(domainElements, toYamlFieldCompletion), + ]; + } else { + return null; + } + + if (completions.length === 0) return null; + + // Strip "- " prefix for matching + const word = context.matchBefore(/[\w]*/); + + return { from: word?.from ?? pos, options: completions, validFor: /^\w*$/ }; }; } // ── Public API ───────────────────────────────────────────────────────── export function buildFhirCompletionExtension( - getSD: GetStructureDefinition, + getSDs: GetStructureDefinitions, ): Extension { - const source = fhirCompletionSource(getSD); - return jsonLanguage.data.of({ autocomplete: source }); + const jsonSource = fhirCompletionSource(getSDs); + const yamlSource = yamlFhirCompletionSource(getSDs); + return [ + jsonLanguage.data.of({ autocomplete: jsonSource }), + yamlLanguage.data.of({ autocomplete: yamlSource }), + ]; } diff --git a/packages/react-components/src/components/code-editor/http/index.ts b/packages/react-components/src/components/code-editor/http/index.ts index 1a422560..769046a6 100644 --- a/packages/react-components/src/components/code-editor/http/index.ts +++ b/packages/react-components/src/components/code-editor/http/index.ts @@ -146,13 +146,256 @@ const httpMethodTheme = EditorView.baseTheme({ }, }); -function http(bodyLanguages: (contentType: string) => Language | null) { +// ── HTTP header autocomplete ──────────────────────────────────────────── + +import type { + Completion, + CompletionContext, + CompletionResult, +} from "@codemirror/autocomplete"; +import { + HttpHeaderName, + HttpHeaders, + HttpHeaderValue, + HttpRequestPath, +} from "./grammar/http.terms"; + +const COMMON_HEADERS: Completion[] = [ + // Standard HTTP + { label: "Accept", type: "header", apply: "Accept: " }, + { label: "Accept-Encoding", type: "header", apply: "Accept-Encoding: " }, + { label: "Accept-Language", type: "header", apply: "Accept-Language: " }, + { label: "Authorization", type: "header", apply: "Authorization: " }, + { label: "Cache-Control", type: "header", apply: "Cache-Control: " }, + { label: "Content-Type", type: "header", apply: "Content-Type: " }, + { label: "Cookie", type: "header", apply: "Cookie: " }, + { label: "Host", type: "header", apply: "Host: " }, + { label: "If-Match", type: "header", apply: "If-Match: " }, + { label: "If-Modified-Since", type: "header", apply: "If-Modified-Since: " }, + { label: "If-None-Match", type: "header", apply: "If-None-Match: " }, + { label: "Origin", type: "header", apply: "Origin: " }, + { label: "Prefer", type: "header", apply: "Prefer: " }, + // Aidbox-specific + { label: "x-audit", type: "header", apply: "x-audit: " }, + { label: "x-correlation-id", type: "header", apply: "x-correlation-id: " }, + { label: "x-debug", type: "header", apply: "x-debug: " }, + { + label: "x-external-user-id", + type: "header", + apply: "x-external-user-id: ", + }, + { + label: "x-max-isolation-level", + type: "header", + apply: "x-max-isolation-level: ", + }, + { label: "x-original-uri", type: "header", apply: "x-original-uri: " }, + { label: "x-request-id", type: "header", apply: "x-request-id: " }, + { label: "x-use-ro-replica", type: "header", apply: "x-use-ro-replica: " }, + { label: "su", type: "header", apply: "su: " }, + { label: "traceparent", type: "header", apply: "traceparent: " }, +]; + +// Header value completions by header name +const HEADER_VALUES: Record = { + "content-type": [ + { label: "application/json", type: "text" }, + { label: "application/fhir+json", type: "text" }, + { label: "text/yaml", type: "text" }, + { label: "application/ndjson", type: "text" }, + { label: "application/gzip", type: "text" }, + { label: "text/csv", type: "text" }, + ], + accept: [ + { label: "application/json", type: "text" }, + { label: "application/yaml", type: "text" }, + { label: "text/yaml", type: "text" }, + ], + prefer: [ + { label: "respond-async", type: "text" }, + { label: "return=minimal", type: "text" }, + { label: "return=representation", type: "text" }, + { label: "return=OperationOutcome", type: "text" }, + ], + "x-debug": [{ label: "policy", type: "text" }], + "x-max-isolation-level": [ + { label: "read-committed", type: "text" }, + { label: "repeatable-read", type: "text" }, + { label: "serializable", type: "text" }, + ], + "cache-control": [ + { label: "no-cache", type: "text" }, + { label: "no-store", type: "text" }, + { label: "max-age=0", type: "text" }, + ], + authorization: [ + { label: "Bearer ", type: "text" }, + { label: "Basic ", type: "text" }, + ], +}; + +const HTTP_METHODS: Completion[] = [ + { label: "GET", type: "keyword", apply: "GET /" }, + { label: "POST", type: "keyword", apply: "POST /" }, + { label: "PUT", type: "keyword", apply: "PUT /" }, + { label: "PATCH", type: "keyword", apply: "PATCH /" }, + { label: "DELETE", type: "keyword", apply: "DELETE /" }, +]; + +function httpCompletionSource( + context: CompletionContext, +): CompletionResult | null { + const { state, pos } = context; + const tree = syntaxTree(state); + const node = tree.resolveInner(pos, -1); + + const line = state.doc.lineAt(pos); + const beforeCursor = line.text.slice(0, pos - line.from); + + // Request method completion — first line, typing method + if ( + node.type.id === HttpRequestMethod || + (line.number === 1 && /^\s*[a-zA-Z]*$/.test(beforeCursor)) + ) { + const word = context.matchBefore(/[a-zA-Z]*/); + return { + from: word?.from ?? pos, + options: HTTP_METHODS, + validFor: /^[a-zA-Z]*$/i, + }; + } + + // Header value completion — after colon + const colonIdx = beforeCursor.indexOf(":"); + if (colonIdx >= 0) { + const inHeaderValue = node.type.id === HttpHeaderValue; + const parentIsHeaders = node.parent?.type.id === HttpHeaders; + if ( + !inHeaderValue && + !parentIsHeaders && + node.parent?.parent?.type.id !== HttpHeaders + ) + return null; + + const headerName = beforeCursor.slice(0, colonIdx).trim().toLowerCase(); + const values = HEADER_VALUES[headerName]; + if (!values) return null; + + const word = context.matchBefore(/\S*/); + return { + from: word?.from ?? pos, + options: values, + validFor: /^\S*$/, + }; + } + + // Header name completion — before colon + const inHeaderName = node.type.id === HttpHeaderName; + const inHeaders = node.type.id === HttpHeaders; + const parentIsHeaders = node.parent?.type.id === HttpHeaders; + + if (!inHeaderName && !inHeaders && !parentIsHeaders) return null; + + const word = context.matchBefore(/[\w-]*/); + return { + from: word?.from ?? pos, + options: COMMON_HEADERS, + validFor: /^[\w-]*$/, + }; +} + +export interface UrlSuggestion { + label: string; + value: string; + type?: string; + description?: string; + expression?: string; +} + +export type GetUrlSuggestions = ( + path: string, + method: string, +) => UrlSuggestion[] | Promise; + +function httpUrlCompletionSource( + getUrlSuggestions: GetUrlSuggestions, +): (context: CompletionContext) => Promise { + return async (context: CompletionContext): Promise => { + const { state, pos } = context; + const tree = syntaxTree(state); + const node = tree.resolveInner(pos, -1); + + if (node.type.id !== HttpRequestPath) return null; + + const line = state.doc.lineAt(pos); + const lineText = line.text; + // Extract method from the beginning of the line + const methodMatch = lineText.match(/^\s*(\w+)\s+/); + if (!methodMatch?.[1]) return null; + const method = methodMatch[1].toUpperCase(); + + const pathStart = line.from + methodMatch[0].length; + const currentPath = state.sliceDoc(pathStart, pos); + + const suggestions = await getUrlSuggestions(currentPath, method); + if (suggestions.length === 0) return null; + + const options: Completion[] = suggestions.map((s) => { + const c: Completion = { + label: s.label, + type: + s.type === "resource-type" + ? "type" + : s.type === "operation" + ? "function" + : s.type === "search-param" + ? "search-param" + : "text", + }; + if (s.type === "search-param") c.apply = `${s.label}=`; + if (s.description) c.detail = s.description.toUpperCase(); + if (s.expression) c.info = s.expression; + return c; + }); + + // Match from the last segment separator (/ or ? or &) + const hasQuery = currentPath.includes("?"); + let from: number; + if (hasQuery) { + const lastSep = Math.max(currentPath.lastIndexOf("?"), currentPath.lastIndexOf("&")); + from = pathStart + lastSep + 1; + } else { + const lastSlash = currentPath.lastIndexOf("/"); + from = pathStart + lastSlash + 1; + } + + return { + from, + options, + validFor: /^[^\s&=]*/, + }; + }; +} + +function http( + bodyLanguages: (contentType: string) => Language | null, + getUrlSuggestions?: GetUrlSuggestions, +) { const parser = makeParser(bodyLanguages); const language = LRLanguage.define({ parser: parser }); - return new LanguageSupport(language, [ + const extensions = [ httpMethodHighlighter, httpMethodTheme, - ]); + language.data.of({ autocomplete: httpCompletionSource }), + ]; + if (getUrlSuggestions) { + extensions.push( + language.data.of({ + autocomplete: httpUrlCompletionSource(getUrlSuggestions), + }), + ); + } + return new LanguageSupport(language, extensions); } export { http }; diff --git a/packages/react-components/src/components/code-editor/index.tsx b/packages/react-components/src/components/code-editor/index.tsx index 343e6810..4ddbd3b8 100644 --- a/packages/react-components/src/components/code-editor/index.tsx +++ b/packages/react-components/src/components/code-editor/index.tsx @@ -55,22 +55,35 @@ import { type ViewUpdate, } from "@codemirror/view"; import { tags } from "@lezer/highlight"; -import { ChevronDown, ChevronUp, ChevronsRight, Table2, Terminal, X } from "lucide-react"; +import { + ChevronDown, + ChevronsRight, + ChevronUp, + Heading, + Table2, + Terminal, + X, +} from "lucide-react"; import * as React from "react"; import { flushSync } from "react-dom"; import { createRoot } from "react-dom/client"; -import { ComplexTypeIcon, SquareFunctionIcon, TypCodeIcon } from "../../icons"; -import { http } from "./http"; import { - type SqlConfig, - buildSqlCompletionExtensions, - fetchSqlMetadata, -} from "./sql-completion"; + ComplexTypeIcon, + ResourceIcon, + SquareFunctionIcon, + TypCodeIcon, +} from "../../icons"; import { buildFhirCompletionExtension, - type GetStructureDefinition, + type GetStructureDefinitions, } from "./fhir-completion"; +import { type GetUrlSuggestions, http } from "./http"; +import { + buildSqlCompletionExtensions, + fetchSqlMetadata, + type SqlConfig, +} from "./sql-completion"; // --- Issue lines: gutter highlighting, line background, hover tooltip --- @@ -303,9 +316,10 @@ const completionTheme = EditorView.theme({ border: "1px solid var(--color-border-primary)", borderRadius: "var(--radius-md)", color: "var(--color-text-secondary)", - fontFamily: "var(--font-family-sans)", - fontSize: "12px", + fontFamily: "var(--font-family-mono)", + fontSize: "14px", padding: "8px 12px", + marginLeft: "8px", lineHeight: "1.4", whiteSpace: "normal", maxWidth: "300px", @@ -459,6 +473,7 @@ function createSearchPanel(view: EditorView) { alignItems: "center", gap: "2px", padding: "6px 8px", + marginTop: "4px", backgroundColor: "var(--color-bg-primary)", border: "1px solid var(--color-border-primary)", borderRadius: "var(--radius-md)", @@ -595,10 +610,10 @@ function createSearchPanel(view: EditorView) { }; } -const searchPanelTheme = EditorView.baseTheme({ - ".cm-panels-top": { +const searchPanelTheme = EditorView.theme({ + "& .cm-panels-top": { position: "absolute", - top: "4px", + top: "8px", right: "4px", left: "auto", zIndex: "10", @@ -606,10 +621,13 @@ const searchPanelTheme = EditorView.baseTheme({ border: "none", }, ".cm-searchMatch": { - backgroundColor: "var(--color-blue-200)", + backgroundColor: "var(--color-blue-200) !important", }, ".cm-searchMatch-selected": { - backgroundColor: "var(--color-blue-400)", + backgroundColor: "var(--color-blue-400) !important", + }, + ".cm-selectionMatch": { + backgroundColor: "var(--color-blue-100) !important", }, }); @@ -725,19 +743,25 @@ const customSQLDialect = SQLDialect.define({ type LanguageMode = "json" | "http" | "sql" | "yaml"; -function languageExtensions(mode: LanguageMode, sqlExtraBuiltins?: string[]) { +function languageExtensions( + mode: LanguageMode, + sqlExtraBuiltins?: string[], + getUrlSuggestions?: GetUrlSuggestions, +) { if (mode === "http") { const jsonLang = json(); const yamlLang = yaml(); return [ - http((ct) => - ct === "application/json" - ? jsonLang.language - : ct === "text/yaml" || - ct === "application/yaml" || - ct === "application/x-yaml" - ? yamlLang.language - : null, + http( + (ct) => + ct === "application/json" + ? jsonLang.language + : ct === "text/yaml" || + ct === "application/yaml" || + ct === "application/x-yaml" + ? yamlLang.language + : null, + getUrlSuggestions, ), syntaxHighlighting(customHighlightStyle), ]; @@ -751,7 +775,36 @@ function languageExtensions(mode: LanguageMode, sqlExtraBuiltins?: string[]) { } return [sql({ dialect }), syntaxHighlighting(customHighlightStyle)]; } else if (mode === "yaml") { - return [yaml(), syntaxHighlighting(customHighlightStyle)]; + return [ + yaml(), + syntaxHighlighting(customHighlightStyle), + keymap.of([{ + key: "Enter", + run: (view) => { + const { state } = view; + const line = state.doc.lineAt(state.selection.main.head); + const lineText = line.text; + const indent = lineText.match(/^(\s*)/)?.[1] ?? ""; + const trimmed = lineText.trimEnd(); + if (trimmed.endsWith(":")) { + // After "key:" — indent to key content level + 2 + const dashMatch = trimmed.match(/^(\s*-\s+)/); + const baseIndent = dashMatch?.[1] ? " ".repeat(dashMatch[1].length) : indent; + const newIndent = `${baseIndent} `; + view.dispatch({ + changes: { from: state.selection.main.head, insert: `\n${newIndent}` }, + selection: { anchor: state.selection.main.head + 1 + newIndent.length }, + }); + } else { + view.dispatch({ + changes: { from: state.selection.main.head, insert: `\n${indent}` }, + selection: { anchor: state.selection.main.head + 1 + indent.length }, + }); + } + return true; + }, + }]), + ]; } else { return [ json(), @@ -777,19 +830,20 @@ type CodeEditorProps = { lintGutter?: boolean; lineNumbers?: boolean; sql?: SqlConfig; - getStructureDefinition?: GetStructureDefinition; + getStructureDefinitions?: GetStructureDefinitions; + getUrlSuggestions?: GetUrlSuggestions; }; export type CodeEditorView = EditorView; +export type { GetStructureDefinitions } from "./fhir-completion"; +export type { GetUrlSuggestions } from "./http"; export type { SqlConfig, - SqlQueryType, SqlMetadata, + SqlQueryType, } from "./sql-completion"; -export type { GetStructureDefinition } from "./fhir-completion"; - export function CodeEditor({ defaultValue, currentValue, @@ -806,7 +860,8 @@ export function CodeEditor({ lintGutter: enableLintGutter = true, lineNumbers: enableLineNumbers = true, sql, - getStructureDefinition, + getStructureDefinitions, + getUrlSuggestions, }: CodeEditorProps) { const domRef = React.useRef(null); const [view, setView] = React.useState(null); @@ -937,9 +992,7 @@ export function CodeEditor({ executeSqlRef.current?.(query, type) ?? Promise.resolve([]), ); view.dispatch({ - effects: sqlCompletionCompartment.current.reconfigure( - extensions, - ), + effects: sqlCompletionCompartment.current.reconfigure(extensions), }); }) .catch(() => {}); @@ -951,10 +1004,10 @@ export function CodeEditor({ React.useEffect(() => { if (!view) return; - if (getStructureDefinition) { + if (getStructureDefinitions) { view.dispatch({ effects: fhirCompletionCompartment.current.reconfigure( - buildFhirCompletionExtension(getStructureDefinition), + buildFhirCompletionExtension(getStructureDefinitions), ), }); } else { @@ -962,7 +1015,7 @@ export function CodeEditor({ effects: fhirCompletionCompartment.current.reconfigure([]), }); } - }, [view, getStructureDefinition]); + }, [view, getStructureDefinitions]); React.useEffect(() => { if (viewCallback && view) { @@ -1012,16 +1065,25 @@ export function CodeEditor({ } }, [currentValue, view]); + const getUrlSuggestionsRef = React.useRef(getUrlSuggestions); + getUrlSuggestionsRef.current = getUrlSuggestions; + + const stableGetUrlSuggestions = React.useMemo(() => { + if (!getUrlSuggestions) return undefined; + return ((path: string, method: string) => + getUrlSuggestionsRef.current?.(path, method) ?? []) as GetUrlSuggestions; + }, [!!getUrlSuggestions]); + React.useEffect(() => { if (view === null) { return; } view.dispatch({ effects: languageCompartment.current.reconfigure( - languageExtensions(mode, sqlFunctions), + languageExtensions(mode, sqlFunctions, stableGetUrlSuggestions), ), }); - }, [mode, view, sqlFunctions]); + }, [mode, view, sqlFunctions, stableGetUrlSuggestions]); React.useEffect(() => { if (view === null) { @@ -1129,21 +1191,28 @@ const editorInputTheme = EditorView.theme({ const KeywordIcon = () => ; const OperatorIcon = () => ; const TableIcon = () => ; +const HeaderIcon = () => ; function getCompletionIcon(completion: Completion): React.FC | null { if (completion.type === "function") return SquareFunctionIcon; if (completion.type === "keyword") return KeywordIcon; if (completion.type === "operator") return OperatorIcon; if (completion.type === "table") return TableIcon; + if (completion.type === "header") return HeaderIcon; + if (completion.type === "text") return TypCodeIcon; + if (completion.type === "type") return ResourceIcon; + if (completion.type === "search-param") return null; const detail = completion.detail; if (!detail) { if (completion.type === "variable") return SquareFunctionIcon; - return null; + return TypCodeIcon; } const typeName = detail.replace(/\[\]$/, ""); - if (!typeName) return null; + if (!typeName) return TypCodeIcon; + // Search param types (TOKEN, REFERENCE) — no icon + if (typeName === typeName.toUpperCase()) return null; const firstChar = typeName[0]; - if (!firstChar) return null; + if (!firstChar) return TypCodeIcon; const isComplex = firstChar === firstChar.toUpperCase(); return isComplex ? ComplexTypeIcon : TypCodeIcon; } @@ -1156,6 +1225,8 @@ function renderCompletionIcon(completion: Completion): Node { flushSync(() => { createRoot(container).render(); }); + } else { + container.style.display = "none"; } return container; } diff --git a/packages/react-components/src/components/code-editor/sql-completion.ts b/packages/react-components/src/components/code-editor/sql-completion.ts index 74d349d1..b3fc4441 100644 --- a/packages/react-components/src/components/code-editor/sql-completion.ts +++ b/packages/react-components/src/components/code-editor/sql-completion.ts @@ -11,7 +11,12 @@ import { EditorView } from "@codemirror/view"; // ── Public types ── -export type SqlQueryType = "tables" | "columns" | "functions" | "jsonb_columns" | "structure_definition"; +export type SqlQueryType = + | "tables" + | "columns" + | "functions" + | "jsonb_columns" + | "structure_definition"; interface StructureDefinitionElementType { code: string; @@ -86,10 +91,7 @@ const COLUMNS_QUERY = `SELECT c.table_schema, c.table_name, c.column_name, c.dat // ── FHIR StructureDefinition processing ── -function isExpandedVariant( - path: string, - unionBases: Set, -): boolean { +function isExpandedVariant(path: string, unionBases: Set): boolean { const parts = path.split("."); if (parts.length < 2) return false; const parentPath = parts.slice(0, -1).join("."); @@ -394,12 +396,14 @@ function buildJsonbResult( return { from: context.pos - chain.partialInput.length, validFor: /^\w*$/, - options: filtered.map((f): Completion => ({ - label: f.name, - type: "property", - detail: f.datatype + (f.isArray ? "[]" : ""), - ...(f.description != null ? { info: f.description } : {}), - })), + options: filtered.map( + (f): Completion => ({ + label: f.name, + type: "property", + detail: f.datatype + (f.isArray ? "[]" : ""), + ...(f.description != null ? { info: f.description } : {}), + }), + ), }; } @@ -407,34 +411,43 @@ function buildJsonbResult( return { from: context.pos - chain.partialInput.length, validFor: /^\w*$/, - options: filtered.map((f): Completion => ({ - label: f.name, - type: "property", - detail: f.datatype + (f.isArray ? "[]" : ""), - ...(f.description != null ? { info: f.description } : {}), - apply: (view: EditorView, _completion: Completion, from: number, to: number) => { - const after = view.state.sliceDoc(to, to + 1); - const end = after === "'" ? to + 1 : to; - const insert = `${f.name}'`; - view.dispatch({ - changes: { from, to: end, insert }, - selection: { anchor: from + insert.length }, - }); - }, - })), + options: filtered.map( + (f): Completion => ({ + label: f.name, + type: "property", + detail: f.datatype + (f.isArray ? "[]" : ""), + ...(f.description != null ? { info: f.description } : {}), + apply: ( + view: EditorView, + _completion: Completion, + from: number, + to: number, + ) => { + const after = view.state.sliceDoc(to, to + 1); + const end = after === "'" ? to + 1 : to; + const insert = `${f.name}'`; + view.dispatch({ + changes: { from, to: end, insert }, + selection: { anchor: from + insert.length }, + }); + }, + }), + ), }; } return { from: context.pos - chain.partialInput.length, validFor: /^'?\w*'?$/, - options: filtered.map((f): Completion => ({ - label: `'${f.name}'`, - type: "property", - detail: f.datatype + (f.isArray ? "[]" : ""), - ...(f.description != null ? { info: f.description } : {}), - apply: `'${f.name}'`, - })), + options: filtered.map( + (f): Completion => ({ + label: `'${f.name}'`, + type: "property", + detail: f.datatype + (f.isArray ? "[]" : ""), + ...(f.description != null ? { info: f.description } : {}), + apply: `'${f.name}'`, + }), + ), }; } @@ -838,9 +851,7 @@ function jsonbCompletionExtension(ctx: { function sqlCompletionOverride(): Extension { return autocompletion({ override: [ - async ( - context: CompletionContext, - ): Promise => { + async (context: CompletionContext): Promise => { const line = context.state.doc.lineAt(context.pos); const textBefore = line.text.slice(0, context.pos - line.from); const inJsonb = isInJsonbContext(textBefore); @@ -878,9 +889,7 @@ function sqlCompletionOverride(): Extension { if (hasTableResults) { const tableOptions = results.flatMap((r) => - r.options.filter( - (o) => o.type === "table" || o.type === "keyword", - ), + r.options.filter((o) => o.type === "table" || o.type === "keyword"), ); const from = results.find((r) => r.options.some((o) => o.type === "table"), @@ -891,7 +900,10 @@ function sqlCompletionOverride(): Extension { if (results.length === 1) return results[0]!; - const groups = new Map(); + const groups = new Map< + number, + { from: number; options: Completion[] } + >(); for (const r of results) { const existing = groups.get(r.from); if (existing) { @@ -927,13 +939,14 @@ function sqlCompletionOverride(): Extension { export async function fetchSqlMetadata( executeSql: SqlConfig["executeSql"], ): Promise { - const [tablesRows, jsonbRows, functionsRows, columnsRows] = - await Promise.all([ + const [tablesRows, jsonbRows, functionsRows, columnsRows] = await Promise.all( + [ executeSql(TABLES_QUERY, "tables"), executeSql(JSONB_COLUMNS_QUERY, "jsonb_columns"), executeSql(FUNCTIONS_QUERY, "functions"), executeSql(COLUMNS_QUERY, "columns"), - ]); + ], + ); const schemas: SchemaMap = {}; for (const row of tablesRows) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8bdd7d82..f4fe14b8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -296,8 +296,8 @@ importers: specifier: ^7.71.2 version: 7.71.2(react@19.2.4) react-resizable-panels: - specifier: ^4.7.2 - version: 4.7.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: ^3.0.6 + version: 3.0.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) recharts: specifier: 3.8.0 version: 3.8.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react-is@18.3.1)(react@19.2.4)(redux@5.0.1) @@ -3639,11 +3639,11 @@ packages: '@types/react': optional: true - react-resizable-panels@4.7.2: - resolution: {integrity: sha512-1L2vyeBG96hp7N6x6rzYXJ8EjYiDiffMsqj3cd+T9aOKwscvuyCn2CuZ5q3PoUSTIJUM6Q5DgXH1bdDe6uvh2w==} + react-resizable-panels@3.0.6: + resolution: {integrity: sha512-b3qKHQ3MLqOgSS+FRYKapNkJZf5EQzuf6+RLiq1/IlTHw99YrZ2NJZLk4hQIzTnnIkRg2LUqyVinu6YWWpUYew==} peerDependencies: - react: ^18.0.0 || ^19.0.0 - react-dom: ^18.0.0 || ^19.0.0 + react: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc react-style-singleton@2.2.3: resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} @@ -7389,7 +7389,7 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 - react-resizable-panels@4.7.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + react-resizable-panels@3.0.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) From 735e73ce0643ede310ff129fc5bc058992469e77 Mon Sep 17 00:00:00 2001 From: Panthevm Date: Wed, 18 Mar 2026 18:45:35 +0300 Subject: [PATCH 07/55] Update code editor --- .../components/code-editor/fhir-completion.ts | 524 +++++++++++++++++- .../src/components/code-editor/http/index.ts | 1 + .../src/components/code-editor/index.tsx | 249 +++++++-- 3 files changed, 704 insertions(+), 70 deletions(-) diff --git a/packages/react-components/src/components/code-editor/fhir-completion.ts b/packages/react-components/src/components/code-editor/fhir-completion.ts index c6abde28..a367889c 100644 --- a/packages/react-components/src/components/code-editor/fhir-completion.ts +++ b/packages/react-components/src/components/code-editor/fhir-completion.ts @@ -6,8 +6,22 @@ import type { } from "@codemirror/autocomplete"; import { jsonLanguage } from "@codemirror/lang-json"; import { yamlLanguage } from "@codemirror/lang-yaml"; -import type { Extension } from "@codemirror/state"; -import type { EditorView } from "@codemirror/view"; +import { + type Extension, + RangeSet, + StateEffect, + StateField, +} from "@codemirror/state"; +import { + Decoration, + EditorView, + GutterMarker, + gutterLineClass, + ViewPlugin, + type ViewUpdate, +} from "@codemirror/view"; +import { ensureSyntaxTree, syntaxTree } from "@codemirror/language"; +import type { SyntaxNode } from "@lezer/common"; // ── Types ────────────────────────────────────────────────────────────── @@ -196,7 +210,8 @@ function getJsonPathAtCursor(doc: string, pos: number): string[] { function getYamlPathAtCursor(doc: string, pos: number): string[] { const lines = doc.slice(0, pos).split("\n"); const currentLine = lines[lines.length - 1] ?? ""; - const currentIndent = currentLine.search(/\S/); + let currentIndent = currentLine.search(/\S/); + if (currentIndent === -1) currentIndent = currentLine.length; // Walk backwards to build path from indentation const path: string[] = []; @@ -208,11 +223,18 @@ function getYamlPathAtCursor(doc: string, pos: number): string[] { if (!trimmed || trimmed.startsWith("#")) continue; const indent = line.search(/\S/); - // Strip leading "- " for array items - const content = trimmed.startsWith("- ") ? trimmed.slice(2) : trimmed; + const isArrayItem = trimmed.startsWith("- "); + const content = isArrayItem ? trimmed.slice(2) : trimmed; const colonIdx = content.indexOf(":"); if (indent < targetIndent && colonIdx > 0) { + // For array items like " - given:", dash is at indent 2 but + // content starts at indent 4. If cursor is at indent 4, it's a + // sibling of "given" (same array item), not nested under it. + if (isArrayItem && indent + 2 >= targetIndent) { + targetIndent = indent; + continue; + } const key = content.slice(0, colonIdx).trim(); path.unshift(key); targetIndent = indent; @@ -509,6 +531,18 @@ function toCompletion(element: FhirElement): Completion { if (actualFrom > 0 && doc[actualFrom - 1] === '"') actualFrom--; if (actualTo < doc.length && doc[actualTo] === '"') actualTo++; + // If replacing an existing property name (colon already follows), + // only replace the name, don't insert a snippet with value + const afterName = doc.slice(actualTo).match(/^\s*:/); + if (afterName) { + const insert = `"${name}"`; + view.dispatch({ + changes: { from: actualFrom, to: actualTo, insert }, + selection: { anchor: actualFrom + insert.length }, + }); + return; + } + // Detect current indentation const line = view.state.doc.lineAt(actualFrom); const lineText = line.text; @@ -537,6 +571,7 @@ function isValuePosition(beforeCursor: string): string | null { export function fhirCompletionSource( getSDs: GetStructureDefinitions, + resourceTypeHint?: string, ): CompletionSource { return async ( context: CompletionContext, @@ -581,19 +616,44 @@ export function fhirCompletionSource( const isPropertyPosition = beforeCursor === "" || beforeCursor === '"' || - /^"?[\w]*$/.test(beforeCursor); + /^"?[\w]*$/.test(beforeCursor) || + /[{,]\s*"?[\w]*$/.test(beforeCursor); if (!isPropertyPosition) return null; const path = getJsonPathAtCursor(doc, pos); const rtMatch = doc.match(/"resourceType"\s*:\s*"([^"]+)"/) ?? doc.match(/resourceType\s*:\s*"([^"]+)"/); - const resourceType = rtMatch?.[1]; + const resourceType = rtMatch?.[1] ?? resourceTypeHint; + + const hasExplicitResourceType = !!rtMatch?.[1]; let completions: Completion[]; if (resourceType) { const elements = await resolveElements(path, resourceType, getSDs); completions = elementsToCompletions(elements, toCompletion); + if (!hasExplicitResourceType && path.length === 0) { + const rtCompletion: Completion = { + label: "resourceType", + type: "property", + detail: "string", + boost: 10, + apply: (view, _completion, from, to) => { + const d = view.state.doc.toString(); + let actualFrom = from; + let actualTo = to; + if (actualFrom > 0 && d[actualFrom - 1] === '"') actualFrom--; + if (actualTo < d.length && d[actualTo] === '"') actualTo++; + const text = '"resourceType": ""'; + view.dispatch({ + changes: { from: actualFrom, to: actualTo, insert: text }, + selection: { anchor: actualFrom + text.length - 1 }, + }); + }, + }; + rtCompletion.info = "FHIR resource type"; + completions = [rtCompletion, ...completions]; + } } else if (path.length === 0) { const rtCompletion: Completion = { label: "resourceType", @@ -652,8 +712,19 @@ function toYamlFieldCompletion(element: FhirElement): Completion { detail: types, boost: element.min && element.min > 0 ? 2 : 0, apply: (view, _completion, from, to) => { + const doc = view.state.doc.toString(); + const afterTo = doc.slice(to); + + // If a colon already follows, just replace the property name + if (/^\s*:/.test(afterTo)) { + view.dispatch({ + changes: { from, to, insert: name }, + selection: { anchor: from + name.length }, + }); + return; + } + const line = view.state.doc.lineAt(from); - // Indent = everything before cursor position on this line const charsBeforeFrom = from - line.from; const indent = " ".repeat(charsBeforeFrom); const inner = `${indent} `; @@ -689,6 +760,7 @@ function toYamlFieldCompletion(element: FhirElement): Completion { export function yamlFhirCompletionSource( getSDs: GetStructureDefinitions, + resourceTypeHint?: string, ): CompletionSource { return async ( context: CompletionContext, @@ -723,12 +795,24 @@ export function yamlFhirCompletionSource( if (!isYamlPropertyPosition(beforeCursor)) return null; const path = getYamlPathAtCursor(doc, pos); - const resourceType = getYamlResourceType(doc); + const hasExplicitResourceType = !!getYamlResourceType(doc); + const resourceType = getYamlResourceType(doc) ?? resourceTypeHint; let completions: Completion[]; if (resourceType) { const elements = await resolveElements(path, resourceType, getSDs); completions = elementsToCompletions(elements, toYamlFieldCompletion); + if (!hasExplicitResourceType && path.length === 0) { + const rtCompletion: Completion = { + label: "resourceType", + type: "property", + detail: "string", + boost: 10, + apply: "resourceType: ", + }; + rtCompletion.info = "FHIR resource type"; + completions = [rtCompletion, ...completions]; + } } else if (path.length === 0) { const rtCompletion: Completion = { label: "resourceType", @@ -760,15 +844,433 @@ export function yamlFhirCompletionSource( }; } +// ── JSON FHIR linter ────────────────────────────────────────────────── + +type PropertyInfo = { + name: string; + path: string[]; + resourceType: string; + from: number; + to: number; +}; + +type EmptyStringInfo = { + from: number; + to: number; +}; + +function walkJsonObject( + node: SyntaxNode, + parentPath: string[], + parentResourceType: string | null, + doc: string, + result: PropertyInfo[], + emptyStrings?: EmptyStringInfo[], +): void { + // Detect if this object declares its own resourceType + let ownResourceType: string | null = null; + for (let child = node.firstChild; child; child = child.nextSibling) { + if (child.name !== "Property") continue; + const nameNode = child.getChild("PropertyName"); + if (!nameNode) continue; + const keyName = doc + .slice(nameNode.from, nameNode.to) + .replace(/^"|"$/g, ""); + if (keyName === "resourceType") { + for (let v = child.firstChild; v; v = v.nextSibling) { + if (v.name === "String") { + ownResourceType = doc + .slice(v.from, v.to) + .replace(/^"|"$/g, ""); + break; + } + } + break; + } + } + + const resourceType = ownResourceType ?? parentResourceType; + const path = ownResourceType ? [] : parentPath; + + if (!resourceType) return; + + for (let child = node.firstChild; child; child = child.nextSibling) { + if (child.name !== "Property") continue; + const nameNode = child.getChild("PropertyName"); + if (!nameNode) continue; + const name = doc + .slice(nameNode.from, nameNode.to) + .replace(/^"|"$/g, ""); + + result.push({ + name, + path: [...path], + resourceType, + from: nameNode.from, + to: nameNode.to, + }); + + for (let v = child.firstChild; v; v = v.nextSibling) { + if (v.name === "Object") { + walkJsonObject( + v, + [...path, name], + resourceType, + doc, + result, + emptyStrings, + ); + } else if (v.name === "Array") { + for ( + let item = v.firstChild; + item; + item = item.nextSibling + ) { + if (item.name === "Object") { + walkJsonObject( + item, + [...path, name], + resourceType, + doc, + result, + emptyStrings, + ); + } + } + } else if (v.name === "String" && emptyStrings) { + const raw = doc.slice(v.from, v.to); + if (raw === '""') { + emptyStrings.push({ from: v.from, to: v.to }); + } + } + } + } +} + +type FhirDiagnostic = { + from: number; + to: number; + message: string; +}; + +async function validateFhirProperties( + properties: PropertyInfo[], + getSDs: GetStructureDefinitions, +): Promise { + const groups = new Map< + string, + { resourceType: string; path: string[]; props: PropertyInfo[] } + >(); + for (const prop of properties) { + const key = `${prop.resourceType}|${prop.path.join(".")}`; + let group = groups.get(key); + if (!group) { + group = { + resourceType: prop.resourceType, + path: [...prop.path], + props: [], + }; + groups.set(key, group); + } + group.props.push(prop); + } + + const diagnostics: FhirDiagnostic[] = []; + + for (const { resourceType, path, props } of groups.values()) { + const elements = await resolveElements(path, resourceType, getSDs); + if (elements.length === 0) continue; + + const validNames = new Set(); + for (const el of elements) { + const name = fieldName(el); + validNames.add(name); + const typeCode = el.type?.[0]?.code; + if ( + el.type?.length === 1 && + typeCode && + PRIMITIVE_TYPES.has(typeCode) + ) { + validNames.add(`_${name}`); + } + } + if (path.length === 0) { + validNames.add("resourceType"); + } + + for (const prop of props) { + if (!validNames.has(prop.name)) { + diagnostics.push({ + from: prop.from, + to: prop.to, + message: `Unknown property "${prop.name}"`, + }); + } + } + } + + return diagnostics; +} + +function findRootJsonObject( + doc: string, + tree: ReturnType, +): SyntaxNode | null { + // Pure JSON mode: top node is JsonText with Object child + const direct = tree.topNode.getChild("Object"); + if (direct) return direct; + + // HTTP mode (mixed parsing): find body after blank line, + // then resolve into the mounted JSON subtree + const bodyStart = doc.indexOf("\n\n"); + if (bodyStart === -1) return null; + + const jsonStart = bodyStart + 2; + if (jsonStart >= doc.length) return null; + + // resolveInner enters mounted (mixed-parsed) subtrees + const innerNode = tree.resolveInner(jsonStart, 1); + if (!innerNode) return null; + + // Walk up to find the Object node + let node: SyntaxNode | null = innerNode; + while (node) { + if (node.name === "Object") return node; + if (node.name === "JsonText") { + return node.getChild("Object"); + } + node = node.parent; + } + + return null; +} + +// ── FHIR validation decorations ─────────────────────────────────────── + +type FhirDiagnosticWithLine = FhirDiagnostic & { line: number }; + +const setFhirDiagnosticsEffect = StateEffect.define(); + +const fhirUnderline = Decoration.mark({ class: "cm-fhir-error-underline" }); +const fhirErrorLineDecoration = Decoration.line({ class: "cm-errorLine" }); + +class FhirGutterMarker extends GutterMarker { + elementClass = "cm-errorLineGutter"; +} +const fhirGutterMarker = new FhirGutterMarker(); + +export const fhirDiagnosticsField = StateField.define<{ + marks: RangeSet; + lineDecos: RangeSet; + gutterMarkers: RangeSet; + messages: Map; +}>({ + create() { + return { + marks: Decoration.none, + lineDecos: Decoration.none, + gutterMarkers: RangeSet.empty, + messages: new Map(), + }; + }, + update(value, tr) { + for (const effect of tr.effects) { + if (effect.is(setFhirDiagnosticsEffect)) { + const diags = effect.value; + if (diags.length === 0) { + return { + marks: Decoration.none, + lineDecos: Decoration.none, + gutterMarkers: RangeSet.empty, + messages: new Map(), + }; + } + + const marks: { from: number; to: number; value: Decoration }[] = + []; + const lineDecos: { + from: number; + to: number; + value: Decoration; + }[] = []; + const gutter: { + from: number; + to: number; + value: GutterMarker; + }[] = []; + const messages = new Map(); + + for (const d of diags) { + marks.push(fhirUnderline.range(d.from, d.to)); + const existing = messages.get(d.line); + if (existing) { + messages.set(d.line, `${existing}\n${d.message}`); + } else { + messages.set(d.line, d.message); + const line = tr.state.doc.line(d.line); + lineDecos.push( + fhirErrorLineDecoration.range(line.from), + ); + gutter.push(fhirGutterMarker.range(line.from)); + } + } + + return { + marks: Decoration.set(marks, true), + lineDecos: Decoration.set(lineDecos, true), + gutterMarkers: RangeSet.of(gutter, true), + messages, + }; + } + } + if (tr.docChanged) { + try { + return { + marks: value.marks.map(tr.changes), + lineDecos: value.lineDecos.map(tr.changes), + gutterMarkers: value.gutterMarkers.map(tr.changes), + messages: value.messages, + }; + } catch { + return { + marks: Decoration.none, + lineDecos: Decoration.none, + gutterMarkers: RangeSet.empty, + messages: new Map(), + }; + } + } + return value; + }, + provide(field) { + return [ + EditorView.decorations.from(field, (v) => v.marks), + EditorView.decorations.from(field, (v) => v.lineDecos), + gutterLineClass.from(field, (v) => v.gutterMarkers), + ]; + }, +}); + +const fhirLinterTheme = EditorView.theme({ + ".cm-fhir-error-underline": { + textDecorationLine: "underline", + textDecorationStyle: "wavy", + textDecorationColor: "var(--color-text-error-primary)", + textUnderlineOffset: "3px", + }, + ".cm-lineNumbers .cm-gutterElement.cm-errorLineGutter": { + color: "var(--color-text-error-primary)", + backgroundColor: + "color-mix(in srgb, var(--color-text-error-primary) 7%, transparent)", + }, +}); + +function buildFhirValidationPlugin( + getSDs: GetStructureDefinitions, + resourceTypeHint?: string, +): Extension { + return ViewPlugin.define((view) => { + let timeout: ReturnType | null = null; + let destroyed = false; + + function scheduleCheck() { + if (timeout) clearTimeout(timeout); + timeout = setTimeout(() => check(), 500); + } + + async function check() { + if (destroyed) return; + const currentDoc = view.state.doc.toString(); + // Ensure syntax tree is fully parsed before checking + const tree = + ensureSyntaxTree(view.state, view.state.doc.length, 1000) ?? + syntaxTree(view.state); + + const rootObj = findRootJsonObject(currentDoc, tree); + if (!rootObj) { + // JSON truly has no object — clear diagnostics + try { + view.dispatch({ + effects: setFhirDiagnosticsEffect.of([]), + }); + } catch { + /* view destroyed */ + } + return; + } + + const properties: PropertyInfo[] = []; + const emptyStrings: EmptyStringInfo[] = []; + walkJsonObject(rootObj, [], resourceTypeHint ?? null, currentDoc, properties, emptyStrings); + if (properties.length === 0 && emptyStrings.length === 0) { + try { + view.dispatch({ + effects: setFhirDiagnosticsEffect.of([]), + }); + } catch { + /* view destroyed */ + } + return; + } + + const rawDiags = await validateFhirProperties( + properties, + getSDs, + ); + if (destroyed) return; + if (view.state.doc.toString() !== currentDoc) return; + + for (const es of emptyStrings) { + rawDiags.push({ + from: es.from, + to: es.to, + message: "Value must not be empty", + }); + } + + const diags: FhirDiagnosticWithLine[] = rawDiags.map((d) => ({ + ...d, + line: view.state.doc.lineAt(d.from).number, + })); + + try { + view.dispatch({ + effects: setFhirDiagnosticsEffect.of(diags), + }); + } catch { + /* view destroyed */ + } + } + + scheduleCheck(); + + return { + update(update: ViewUpdate) { + if (update.docChanged) { + scheduleCheck(); + } + }, + destroy() { + destroyed = true; + if (timeout) clearTimeout(timeout); + }, + }; + }); +} + // ── Public API ───────────────────────────────────────────────────────── export function buildFhirCompletionExtension( getSDs: GetStructureDefinitions, + resourceTypeHint?: string, ): Extension { - const jsonSource = fhirCompletionSource(getSDs); - const yamlSource = yamlFhirCompletionSource(getSDs); + const jsonSource = fhirCompletionSource(getSDs, resourceTypeHint); + const yamlSource = yamlFhirCompletionSource(getSDs, resourceTypeHint); return [ jsonLanguage.data.of({ autocomplete: jsonSource }), yamlLanguage.data.of({ autocomplete: yamlSource }), + fhirDiagnosticsField, + fhirLinterTheme, + buildFhirValidationPlugin(getSDs, resourceTypeHint), ]; } diff --git a/packages/react-components/src/components/code-editor/http/index.ts b/packages/react-components/src/components/code-editor/http/index.ts index 769046a6..41a084c4 100644 --- a/packages/react-components/src/components/code-editor/http/index.ts +++ b/packages/react-components/src/components/code-editor/http/index.ts @@ -353,6 +353,7 @@ function httpUrlCompletionSource( : "text", }; if (s.type === "search-param") c.apply = `${s.label}=`; + else if (s.type === "path" && s.label === "fhir") c.apply = `${s.label}/`; if (s.description) c.detail = s.description.toUpperCase(); if (s.expression) c.info = s.expression; return c; diff --git a/packages/react-components/src/components/code-editor/index.tsx b/packages/react-components/src/components/code-editor/index.tsx index 4ddbd3b8..9850dfc0 100644 --- a/packages/react-components/src/components/code-editor/index.tsx +++ b/packages/react-components/src/components/code-editor/index.tsx @@ -18,8 +18,9 @@ import { HighlightStyle, indentOnInput, syntaxHighlighting, + syntaxTree, } from "@codemirror/language"; -import { linter, lintGutter, lintKeymap } from "@codemirror/lint"; +import { linter, lintKeymap } from "@codemirror/lint"; import { closeSearchPanel, findNext, @@ -76,6 +77,7 @@ import { } from "../../icons"; import { buildFhirCompletionExtension, + fhirDiagnosticsField, type GetStructureDefinitions, } from "./fhir-completion"; import { type GetUrlSuggestions, http } from "./http"; @@ -99,7 +101,7 @@ const setIssueLinesEffect = StateEffect.define(); let errorTooltipEl: HTMLDivElement | null = null; -function showErrorTooltip(anchor: Element, message: string) { +function showErrorTooltip(message: string, x: number, y: number) { hideErrorTooltip(); const tooltip = document.createElement("div"); @@ -123,13 +125,9 @@ function showErrorTooltip(anchor: Element, message: string) { document.body.appendChild(tooltip); errorTooltipEl = tooltip; - const guttersEl = anchor.closest(".cm-gutters"); - const guttersRect = guttersEl - ? guttersEl.getBoundingClientRect() - : anchor.getBoundingClientRect(); - const anchorRect = anchor.getBoundingClientRect(); - tooltip.style.left = `${guttersRect.right + 4}px`; - tooltip.style.top = `${anchorRect.top}px`; + const tooltipHeight = tooltip.getBoundingClientRect().height; + tooltip.style.left = `${x}px`; + tooltip.style.top = `${y - tooltipHeight - 8}px`; } function hideErrorTooltip() { @@ -179,6 +177,21 @@ const issueLinesField = StateField.define<{ }; } } + if (tr.docChanged) { + try { + return { + gutterMarkers: state.gutterMarkers.map(tr.changes), + lineDecorations: state.lineDecorations.map(tr.changes), + messages: state.messages, + }; + } catch { + return { + gutterMarkers: RangeSet.empty, + lineDecorations: Decoration.none, + messages: new Map(), + }; + } + } return state; }, provide(field) { @@ -189,33 +202,67 @@ const issueLinesField = StateField.define<{ }, }); -const errorTooltipHandler = EditorView.domEventHandlers({ - mouseover(event, view) { - const target = event.target as HTMLElement; - const gutterEl = target.closest( - ".cm-lineNumbers .cm-gutterElement", - ) as HTMLElement | null; - if (!gutterEl) { - hideErrorTooltip(); - return false; - } +function getErrorMessageForLine( + view: EditorView, + lineNo: number, +): string | undefined { + const issueMsg = view.state.field(issueLinesField).messages.get(lineNo); + if (issueMsg) return issueMsg; + try { + return view.state.field(fhirDiagnosticsField).messages.get(lineNo); + } catch { + return undefined; + } +} + +function handleErrorTooltipMove(event: Event, view: EditorView) { + const target = event.target as HTMLElement; + const mouseEvent = event as MouseEvent; + // Check gutter line number + const gutterEl = target.closest( + ".cm-lineNumbers .cm-gutterElement", + ) as HTMLElement | null; + if (gutterEl) { const lineNo = Number.parseInt(gutterEl.textContent ?? "", 10); - if (Number.isNaN(lineNo)) { - hideErrorTooltip(); - return false; + if (!Number.isNaN(lineNo)) { + const message = getErrorMessageForLine(view, lineNo); + if (message) { + showErrorTooltip( + message, + mouseEvent.clientX, + mouseEvent.clientY, + ); + return false; + } } + hideErrorTooltip(); + return false; + } - const { messages } = view.state.field(issueLinesField); - const message = messages.get(lineNo); - if (!message) { - hideErrorTooltip(); + // Check content line (cm-line) — follow cursor + const lineEl = target.closest(".cm-line") as HTMLElement | null; + if (lineEl) { + const pos = view.posAtDOM(lineEl); + const lineNo = view.state.doc.lineAt(pos).number; + const message = getErrorMessageForLine(view, lineNo); + if (message) { + showErrorTooltip( + message, + mouseEvent.clientX, + mouseEvent.clientY, + ); return false; } + } - showErrorTooltip(gutterEl, message); - return false; - }, + hideErrorTooltip(); + return false; +} + +const errorTooltipHandler = EditorView.domEventHandlers({ + mouseover: handleErrorTooltipMove, + mousemove: handleErrorTooltipMove, mouseleave() { hideErrorTooltip(); return false; @@ -252,7 +299,7 @@ const baseTheme = EditorView.theme({ fontFamily: "var(--font-family-mono)", }, ".cm-gutters": { - backgroundColor: "var(--color-bg-primary)", + backgroundColor: "transparent", border: "none", }, ".cm-lineNumbers": { @@ -273,11 +320,19 @@ const baseTheme = EditorView.theme({ ".cm-activeLine": { backgroundColor: "transparent !important", }, - ".cm-errorLineGutter": { + ".cm-lineNumbers .cm-gutterElement.cm-errorLineGutter": { color: "var(--color-text-error-primary)", backgroundColor: "color-mix(in srgb, var(--color-text-error-primary) 7%, transparent)", }, + ".cm-foldGutter .cm-gutterElement.cm-errorLineGutter": { + color: "var(--color-text-error-primary)", + backgroundColor: + "color-mix(in srgb, var(--color-text-error-primary) 7%, transparent)", + display: "flex", + alignItems: "center", + justifyContent: "center", + }, ".cm-errorLine": { backgroundColor: "color-mix(in srgb, var(--color-text-error-primary) 7%, transparent)", @@ -382,11 +437,19 @@ const readOnlyTheme = EditorView.theme({ ".cm-activeLine": { backgroundColor: "transparent !important", }, - ".cm-errorLineGutter": { + ".cm-lineNumbers .cm-gutterElement.cm-errorLineGutter": { color: "var(--color-text-error-primary)", backgroundColor: "color-mix(in srgb, var(--color-text-error-primary) 7%, transparent)", }, + ".cm-foldGutter .cm-gutterElement.cm-errorLineGutter": { + color: "var(--color-text-error-primary)", + backgroundColor: + "color-mix(in srgb, var(--color-text-error-primary) 7%, transparent)", + display: "flex", + alignItems: "center", + justifyContent: "center", + }, ".cm-errorLine": { backgroundColor: "color-mix(in srgb, var(--color-text-error-primary) 7%, transparent)", @@ -764,6 +827,7 @@ function languageExtensions( getUrlSuggestions, ), syntaxHighlighting(customHighlightStyle), + jsonAutoExpandBraces(), ]; } else if (mode === "sql") { let dialect = customSQLDialect; @@ -810,10 +874,49 @@ function languageExtensions( json(), linter(jsonParseLinter(), { delay: 300 }), syntaxHighlighting(customHighlightStyle), + jsonAutoExpandBraces(), ]; } } +function jsonAutoExpandBraces(): Extension { + return EditorState.transactionFilter.of((tr) => { + if (!tr.docChanged) return tr; + + let braceFrom = -1; + let braceTo = -1; + let changeCount = 0; + + tr.changes.iterChanges((fromA, toA, _fromB, _toB, inserted) => { + changeCount++; + if (inserted.toString() === "{}") { + braceFrom = fromA; + braceTo = toA; + } + }); + + if (changeCount !== 1 || braceFrom === -1) return tr; + + const tree = syntaxTree(tr.startState); + const nodeBefore = tree.resolveInner(braceFrom, -1); + if ( + nodeBefore.name === "String" || + nodeBefore.parent?.name === "String" + ) { + return tr; + } + + const line = tr.startState.doc.lineAt(braceFrom); + const indent = line.text.match(/^(\s*)/)?.[1] ?? ""; + const inner = `${indent} `; + + return { + changes: { from: braceFrom, to: braceTo, insert: `{\n${inner}\n${indent}}` }, + selection: { anchor: braceFrom + 2 + inner.length }, + }; + }); +} + type CodeEditorProps = { readOnly?: boolean; isReadOnlyTheme?: boolean; @@ -827,10 +930,10 @@ type CodeEditorProps = { additionalExtensions?: Extension[]; issueLineNumbers?: { line: number; message?: string }[]; foldGutter?: boolean; - lintGutter?: boolean; lineNumbers?: boolean; sql?: SqlConfig; getStructureDefinitions?: GetStructureDefinitions; + resourceTypeHint?: string; getUrlSuggestions?: GetUrlSuggestions; }; @@ -857,15 +960,26 @@ export function CodeEditor({ additionalExtensions, issueLineNumbers, foldGutter: enableFoldGutter = true, - lintGutter: enableLintGutter = true, lineNumbers: enableLineNumbers = true, sql, getStructureDefinitions, + resourceTypeHint, getUrlSuggestions, }: CodeEditorProps) { const domRef = React.useRef(null); const [view, setView] = React.useState(null); + const safeDispatch = React.useCallback( + (spec: Parameters[0]) => { + try { + view?.dispatch(spec); + } catch { + // Ignore RangeError from stale decoration positions during reconfigure + } + }, + [view], + ); + const initialValue = React.useRef(defaultValue ?? ""); const onChangeComparment = React.useRef(new Compartment()); @@ -894,7 +1008,24 @@ export function CodeEditor({ EditorView.contentAttributes.of({ "data-gramm": "false" }), readOnlyCompartment.current.of(EditorState.readOnly.of(false)), ...(enableLineNumbers ? [lineNumbers()] : []), - ...(enableFoldGutter ? [foldGutter()] : []), + ...(enableFoldGutter + ? [ + foldGutter({ + markerDOM: (open) => { + const el = document.createElement("span"); + el.style.display = "flex"; + el.style.alignItems = "center"; + el.style.justifyContent = "center"; + el.style.width = "100%"; + el.style.height = "100%"; + el.innerHTML = open + ? '' + : ''; + return el; + }, + }), + ] + : []), highlightSpecialChars(), history(), drawSelection(), @@ -944,9 +1075,9 @@ export function CodeEditor({ ...completionKeymap, ...lintKeymap, ]), - ...(enableLintGutter ? [lintGutter()] : []), issueLinesField, errorTooltipHandler, + EditorView.exceptionSink.of(() => {}), ...customSearchExtension, onChangeComparment.current.of([]), onUpdateComparment.current.of([]), @@ -963,7 +1094,7 @@ export function CodeEditor({ view.destroy(); setView(() => null); }; - }, [enableFoldGutter, enableLineNumbers, enableLintGutter]); + }, [enableFoldGutter, enableLineNumbers]); React.useEffect(() => { executeSqlRef.current = sql?.executeSql; @@ -972,7 +1103,7 @@ export function CodeEditor({ React.useEffect(() => { if (!view || !sql) { if (view) { - view.dispatch({ + safeDispatch({ effects: sqlCompletionCompartment.current.reconfigure([]), }); } @@ -991,7 +1122,7 @@ export function CodeEditor({ (query, type) => executeSqlRef.current?.(query, type) ?? Promise.resolve([]), ); - view.dispatch({ + safeDispatch({ effects: sqlCompletionCompartment.current.reconfigure(extensions), }); }) @@ -1005,17 +1136,17 @@ export function CodeEditor({ React.useEffect(() => { if (!view) return; if (getStructureDefinitions) { - view.dispatch({ + safeDispatch({ effects: fhirCompletionCompartment.current.reconfigure( - buildFhirCompletionExtension(getStructureDefinitions), + buildFhirCompletionExtension(getStructureDefinitions, resourceTypeHint), ), }); } else { - view.dispatch({ + safeDispatch({ effects: fhirCompletionCompartment.current.reconfigure([]), }); } - }, [view, getStructureDefinitions]); + }, [view, getStructureDefinitions, resourceTypeHint, safeDispatch]); React.useEffect(() => { if (viewCallback && view) { @@ -1024,7 +1155,7 @@ export function CodeEditor({ }, [view, viewCallback]); React.useEffect(() => { - view?.dispatch({ + safeDispatch({ effects: onChangeComparment.current.reconfigure([ EditorView.updateListener.of((update) => { if (update.docChanged && onChange) { @@ -1033,10 +1164,10 @@ export function CodeEditor({ }), ]), }); - }, [view, onChange]); + }, [view, onChange, safeDispatch]); React.useEffect(() => { - view?.dispatch({ + safeDispatch({ effects: onUpdateComparment.current.reconfigure([ EditorView.updateListener.of((update) => { if (onUpdate) { @@ -1045,7 +1176,7 @@ export function CodeEditor({ }), ]), }); - }, [view, onUpdate]); + }, [view, onUpdate, safeDispatch]); // FIXME: it is probably better to have CM manage its state. React.useEffect(() => { @@ -1055,7 +1186,7 @@ export function CodeEditor({ const currentDoc = view.state.doc.toString(); if (currentDoc !== currentValue) { - view.dispatch({ + safeDispatch({ changes: { from: 0, to: currentDoc.length, @@ -1063,7 +1194,7 @@ export function CodeEditor({ }, }); } - }, [currentValue, view]); + }, [currentValue, view, safeDispatch]); const getUrlSuggestionsRef = React.useRef(getUrlSuggestions); getUrlSuggestionsRef.current = getUrlSuggestions; @@ -1078,60 +1209,60 @@ export function CodeEditor({ if (view === null) { return; } - view.dispatch({ + safeDispatch({ effects: languageCompartment.current.reconfigure( languageExtensions(mode, sqlFunctions, stableGetUrlSuggestions), ), }); - }, [mode, view, sqlFunctions, stableGetUrlSuggestions]); + }, [mode, view, sqlFunctions, stableGetUrlSuggestions, safeDispatch]); React.useEffect(() => { if (view === null) { return; } - view.dispatch({ + safeDispatch({ effects: [ readOnlyCompartment.current.reconfigure( EditorState.readOnly.of(readOnly), ), ], }); - }, [readOnly, view]); + }, [readOnly, view, safeDispatch]); React.useEffect(() => { if (view === null) { return; } - view.dispatch({ + safeDispatch({ effects: [ themeCompartment.current.reconfigure( isReadOnlyTheme ? readOnlyTheme : baseTheme, ), ], }); - }, [isReadOnlyTheme, view]); + }, [isReadOnlyTheme, view, safeDispatch]); React.useEffect(() => { if (view === null) { return; } - view.dispatch({ + safeDispatch({ effects: [ additionalExtensionsCompartment.current.reconfigure( additionalExtensions ?? [], ), ], }); - }, [additionalExtensions, view]); + }, [additionalExtensions, view, safeDispatch]); React.useEffect(() => { if (view === null) { return; } - view.dispatch({ + safeDispatch({ effects: setIssueLinesEffect.of(issueLineNumbers ?? []), }); - }, [issueLineNumbers, view]); + }, [issueLineNumbers, view, safeDispatch]); return
; } From 70706293b66d954d2782f019a7db150be001e748 Mon Sep 17 00:00:00 2001 From: Panthevm Date: Wed, 18 Mar 2026 20:01:49 +0300 Subject: [PATCH 08/55] Update Codemirror autocomplete --- .../components/code-editor/fhir-completion.ts | 338 ++++++++++++++++-- .../src/components/code-editor/index.tsx | 97 +++-- 2 files changed, 386 insertions(+), 49 deletions(-) diff --git a/packages/react-components/src/components/code-editor/fhir-completion.ts b/packages/react-components/src/components/code-editor/fhir-completion.ts index a367889c..96d193f6 100644 --- a/packages/react-components/src/components/code-editor/fhir-completion.ts +++ b/packages/react-components/src/components/code-editor/fhir-completion.ts @@ -38,6 +38,7 @@ interface FhirElement { max?: string; type?: FhirElementType[]; binding?: { valueSet: string; strength: string }; + contentReference?: string; } interface StructureDefinition { @@ -251,10 +252,12 @@ function getYamlResourceType(doc: string): string | null { function isYamlPropertyPosition(beforeCursor: string): boolean { const trimmed = beforeCursor.trimStart(); - // Empty line, or typing a key (no colon yet), or after "- " - if (trimmed === "" || trimmed === "-" || trimmed === "- ") return true; + // Empty line, or after "- " (dash with space) + if (trimmed === "" || trimmed === "- ") return true; + // Bare "-" without space — not ready for property yet + if (trimmed === "-") return false; if (trimmed.includes(":")) return false; - // Typing a word without colon = key position + // Typing a word without colon = key position (optionally after "- ") return /^(-\s+)?[\w]*$/.test(trimmed); } @@ -361,8 +364,16 @@ async function resolveElements( if (key === "resourceType") return []; const el = findElement(currentElements, currentPath, key); - if (!el?.type?.[0]) return []; + if (!el) return []; + // contentReference (e.g. "#Questionnaire.item") — resolve to referenced path + if (el.contentReference) { + const refPath = el.contentReference.replace(/^#/, ""); + currentPath = refPath; + continue; + } + + if (!el.type?.[0]) return []; const typeCode = el.type[0].code; if (typeCode === "BackboneElement") { @@ -452,6 +463,39 @@ const PRIMITIVE_TYPES = new Set([ "xhtml", ]); +function isPrimitiveType(typeCode: string): boolean { + return ( + PRIMITIVE_TYPES.has(typeCode) || + typeCode.startsWith("http://hl7.org/fhirpath/System.") + ); +} + +const FHIR_STRING_TYPES = new Set([ + "string", + "code", + "uri", + "url", + "canonical", + "id", + "markdown", + "oid", + "uuid", + "base64Binary", + "xhtml", + "http://hl7.org/fhirpath/System.String", +]); + +const FHIR_NUMBER_TYPES = new Set([ + "boolean", + "integer", + "decimal", + "positiveInt", + "unsignedInt", + "http://hl7.org/fhirpath/System.Boolean", + "http://hl7.org/fhirpath/System.Integer", + "http://hl7.org/fhirpath/System.Decimal", +]); + type SnippetKind = | "array-complex" | "array-primitive" @@ -463,18 +507,15 @@ type SnippetKind = function snippetKind(element: FhirElement): SnippetKind { const isArray = element.max === "*"; const typeCode = element.type?.[0]?.code; - if (!typeCode) return "bare"; + if (!typeCode) { + // contentReference elements have no type but are complex objects + if (element.contentReference) return isArray ? "array-complex" : "object"; + return "bare"; + } if (isArray) - return PRIMITIVE_TYPES.has(typeCode) ? "array-primitive" : "array-complex"; - if ( - typeCode === "boolean" || - typeCode === "integer" || - typeCode === "decimal" || - typeCode === "positiveInt" || - typeCode === "unsignedInt" - ) - return "number"; - if (PRIMITIVE_TYPES.has(typeCode)) return "string"; + return isPrimitiveType(typeCode) ? "array-primitive" : "array-complex"; + if (FHIR_NUMBER_TYPES.has(typeCode)) return "number"; + if (isPrimitiveType(typeCode)) return "string"; return "object"; } @@ -562,6 +603,66 @@ function toCompletion(element: FhirElement): Completion { // ── Completion source ────────────────────────────────────────────────── +// ── Reference target resolution ──────────────────────────────────────── + +async function resolveReferenceTargets( + path: string[], + resourceType: string, + getSDs: GetStructureDefinitions, +): Promise { + // path is the path TO the Reference element (e.g. ["managingOrganization"]) + // We need to find the element at this path and check if its type is Reference + const result = await collectAllElements(resourceType, getSDs); + if (!result) return null; + + let currentPath = resourceType; + let currentElements = result.elements; + + // Walk all segments except the last to resolve the parent context + for (let i = 0; i < path.length - 1; i++) { + const key = path[i]!; + if (key === "resourceType") return null; + + const el = findElement(currentElements, currentPath, key); + if (!el) return null; + + if (el.contentReference) { + currentPath = el.contentReference.replace(/^#/, ""); + continue; + } + if (!el.type?.[0]) return null; + const typeCode = el.type[0].code; + if (typeCode === "BackboneElement") { + currentPath = el.path; + continue; + } + const typeResult = await collectAllElements(typeCode, getSDs); + if (!typeResult) return null; + currentPath = typeResult.basePath; + currentElements = typeResult.elements; + } + + // Now find the last segment — this should be a Reference element + const lastKey = path[path.length - 1]; + if (!lastKey) return null; + + const el = findElement(currentElements, currentPath, lastKey); + if (!el?.type) return null; + + // Collect targetProfile from all Reference types + const targets: string[] = []; + for (const t of el.type) { + if (t.code === "Reference" && t.targetProfile) { + for (const profile of t.targetProfile) { + // Extract resource type from profile URL: "http://hl7.org/fhir/StructureDefinition/Organization" → "Organization" + const rt = profile.split("/").pop(); + if (rt) targets.push(rt); + } + } + } + return targets.length > 0 ? targets : null; +} + // Check if cursor is in a value position (after "key": or key: ) function isValuePosition(beforeCursor: string): string | null { const match = beforeCursor.match(/"?(\w+)"?\s*:\s*"?([^"]*)?$/); @@ -612,6 +713,30 @@ export function fhirCompletionSource( return { from: word?.from ?? pos, options, validFor: /^\w*$/ }; } + // Check if we're in a value position for "reference" inside a Reference type + if (valueKey === "reference") { + const path = getJsonPathAtCursor(doc, pos); + const rtMatch = doc.match(/"resourceType"\s*:\s*"([^"]+)"/); + const resourceType = rtMatch?.[1] ?? resourceTypeHint; + if (resourceType && path.length > 0) { + const targets = await resolveReferenceTargets(path, resourceType, getSDs); + if (targets) { + const options: Completion[] = targets.map((rt) => ({ + label: `${rt}/`, + type: "type", + apply: (view: EditorView, _c: Completion, from: number, to: number) => { + view.dispatch({ + changes: { from, to, insert: `${rt}/` }, + selection: { anchor: from + rt.length + 1 }, + }); + }, + })); + const word = context.matchBefore(/[\w/]*/); + return { from: word?.from ?? pos, options, validFor: /^[\w/]*$/ }; + } + } + } + // Property name position — with or without quotes const isPropertyPosition = beforeCursor === "" || @@ -704,7 +829,7 @@ function toYamlFieldCompletion(element: FhirElement): Completion { const types = element.type?.map((t) => t.code).join(" | ") ?? ""; const isArray = element.max === "*"; const typeCode = element.type?.[0]?.code; - const isPrimitive = typeCode ? PRIMITIVE_TYPES.has(typeCode) : false; + const isPrimitive = typeCode ? isPrimitiveType(typeCode) : false; const completion: Completion = { label: name, @@ -731,7 +856,7 @@ function toYamlFieldCompletion(element: FhirElement): Completion { let text: string; let cursorOffset: number; - const isString = typeCode === "string" || typeCode === "code" || typeCode === "uri" || typeCode === "url" || typeCode === "canonical" || typeCode === "id" || typeCode === "markdown" || typeCode === "oid" || typeCode === "uuid" || typeCode === "base64Binary" || typeCode === "xhtml"; + const isString = typeCode ? FHIR_STRING_TYPES.has(typeCode) : false; if (isArray) { text = `${name}:\n${inner}- `; cursorOffset = text.length; @@ -792,6 +917,29 @@ export function yamlFhirCompletionSource( return { from: word?.from ?? pos, options, validFor: /^\w*$/ }; } + // Value position for "reference" inside a Reference type + if (valueKey === "reference") { + const path = getYamlPathAtCursor(doc, pos); + // path includes keys up to cursor; "reference" is the current key, + // so the parent Reference element is at path (without "reference" in path since + // getYamlPathAtCursor gives parents). We need path + ["reference"] context. + // Actually path gives ancestor keys. The element containing "reference" is at path. + const resourceType = getYamlResourceType(doc) ?? resourceTypeHint; + if (resourceType && path.length > 0) { + // path = ["managingOrganization"] when cursor is on reference value + // We need to find the element at path[-1] from the grandparent + const targets = await resolveReferenceTargets(path, resourceType, getSDs); + if (targets) { + const options: Completion[] = targets.map((rt) => ({ + label: `${rt}/`, + type: "type", + })); + const word = context.matchBefore(/[\w/]*/); + return { from: word?.from ?? pos, options, validFor: /^[\w/]*$/ }; + } + } + } + if (!isYamlPropertyPosition(beforeCursor)) return null; const path = getYamlPathAtCursor(doc, pos); @@ -844,6 +992,140 @@ export function yamlFhirCompletionSource( }; } +// ── YAML FHIR linter ────────────────────────────────────────────────── + +const HTTP_METHOD_RE = /^(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s/; + +function findRootYamlDocument(doc: string): { start: number } | null { + const firstLine = doc.slice(0, doc.indexOf("\n") >>> 0).trimStart(); + const isHttpMode = HTTP_METHOD_RE.test(firstLine); + + if (isHttpMode) { + // HTTP mode: body starts after blank line + const bodyStart = doc.indexOf("\n\n"); + if (bodyStart === -1) return null; + const start = bodyStart + 2; + if (start >= doc.length) return null; + const bodyContent = doc.slice(start).trimStart(); + if (!bodyContent) return null; + // Check that what follows isn't JSON + if (bodyContent.startsWith("{") || bodyContent.startsWith("[")) return null; + return { start }; + } + + // Pure YAML: check first non-whitespace isn't JSON + const firstNonWs = doc.trimStart(); + if (firstNonWs.startsWith("{") || firstNonWs.startsWith("[")) return null; + return { start: 0 }; +} + +function walkYamlObject( + text: string, + startOffset: number, + parentPath: string[], + parentResourceType: string | null, + result: PropertyInfo[], + emptyStrings?: EmptyStringInfo[], +): void { + const yamlText = text.slice(startOffset); + const yamlLines = yamlText.split("\n"); + + // Detect resourceType + let ownResourceType: string | null = null; + for (const line of yamlLines) { + const trimmed = line.trimStart(); + if (!trimmed || trimmed.startsWith("#")) continue; + const m = trimmed.match(/^resourceType:\s*(\S+)/); + if (m) { + ownResourceType = m[1] ?? null; + break; + } + // Only check top-level lines (indent 0) + if (line.search(/\S/) === 0 && !m) continue; + if (line.search(/\S/) > 0) continue; + } + + const resourceType = ownResourceType ?? parentResourceType; + const basePath = ownResourceType ? [] : parentPath; + if (!resourceType) return; + + const stack: { indent: number; path: string[]; arrayChildIndent: number | null }[] = [ + { indent: -1, path: basePath, arrayChildIndent: null }, + ]; + + for (let i = 0; i < yamlLines.length; i++) { + const line = yamlLines[i]!; + const trimmed = line.trimStart(); + if (!trimmed || trimmed.startsWith("#")) continue; + + const indent = line.length - trimmed.length; + const isArrayItem = trimmed.startsWith("- "); + const content = isArrayItem ? trimmed.slice(2) : trimmed; + const colonIdx = content.indexOf(":"); + if (colonIdx <= 0) continue; + + const key = content.slice(0, colonIdx).trim(); + const valueAfterColon = content.slice(colonIdx + 1).trim(); + + // Pop stack to find parent + while (stack.length > 1 && stack[stack.length - 1]!.indent >= indent) { + stack.pop(); + } + let parentEntry = stack[stack.length - 1]!; + + // Track where array items appear under this parent + if (isArrayItem && parentEntry.arrayChildIndent === null) { + parentEntry.arrayChildIndent = indent; + } + + // If parent has array children and this non-array line is at the array item + // level (not deeper inside an item), it's invalid YAML — treat as grandparent's child. + if ( + !isArrayItem && + parentEntry.arrayChildIndent !== null && + indent <= parentEntry.arrayChildIndent && + stack.length > 1 + ) { + stack.pop(); + parentEntry = stack[stack.length - 1]!; + } + + // Calculate character offset for this key + const keyIndent = isArrayItem ? indent + 2 : indent; + let charOffset = startOffset; + for (let j = 0; j < i; j++) { + charOffset += yamlLines[j]!.length + 1; + } + const keyFrom = charOffset + keyIndent; + const keyTo = keyFrom + key.length; + + result.push({ + name: key, + path: [...parentEntry.path], + resourceType, + from: keyFrom, + to: keyTo, + }); + + // Check for empty strings + if (emptyStrings && (valueAfterColon === "''" || valueAfterColon === '""')) { + const afterColonStr = content.slice(colonIdx + 1); + const wsLen = afterColonStr.length - afterColonStr.trimStart().length; + const emptyFrom = charOffset + keyIndent + colonIdx + 1 + wsLen; + const emptyTo = emptyFrom + 2; + emptyStrings.push({ from: emptyFrom, to: emptyTo }); + } + + // Push to stack if this key has nested content (no inline value, or value is empty) + // For array items (- key:), use effective indent (indent + 2) so that + // sibling properties at the same level correctly pop this entry. + if (!valueAfterColon || valueAfterColon === "" || valueAfterColon.startsWith("#")) { + const effectiveIndent = isArrayItem ? indent + 2 : indent; + stack.push({ indent: effectiveIndent, path: [...parentEntry.path, key], arrayChildIndent: null }); + } + } +} + // ── JSON FHIR linter ────────────────────────────────────────────────── type PropertyInfo = { @@ -989,7 +1271,7 @@ async function validateFhirProperties( if ( el.type?.length === 1 && typeCode && - PRIMITIVE_TYPES.has(typeCode) + isPrimitiveType(typeCode) ) { validNames.add(`_${name}`); } @@ -1186,9 +1468,22 @@ function buildFhirValidationPlugin( ensureSyntaxTree(view.state, view.state.doc.length, 1000) ?? syntaxTree(view.state); + const properties: PropertyInfo[] = []; + const emptyStrings: EmptyStringInfo[] = []; + + // Try JSON first const rootObj = findRootJsonObject(currentDoc, tree); - if (!rootObj) { - // JSON truly has no object — clear diagnostics + if (rootObj) { + walkJsonObject(rootObj, [], resourceTypeHint ?? null, currentDoc, properties, emptyStrings); + } else { + // Try YAML + const yamlDoc = findRootYamlDocument(currentDoc); + if (yamlDoc) { + walkYamlObject(currentDoc, yamlDoc.start, [], resourceTypeHint ?? null, properties, emptyStrings); + } + } + + if (!rootObj && !findRootYamlDocument(currentDoc)) { try { view.dispatch({ effects: setFhirDiagnosticsEffect.of([]), @@ -1199,9 +1494,6 @@ function buildFhirValidationPlugin( return; } - const properties: PropertyInfo[] = []; - const emptyStrings: EmptyStringInfo[] = []; - walkJsonObject(rootObj, [], resourceTypeHint ?? null, currentDoc, properties, emptyStrings); if (properties.length === 0 && emptyStrings.length === 0) { try { view.dispatch({ diff --git a/packages/react-components/src/components/code-editor/index.tsx b/packages/react-components/src/components/code-editor/index.tsx index 9850dfc0..25967ecb 100644 --- a/packages/react-components/src/components/code-editor/index.tsx +++ b/packages/react-components/src/components/code-editor/index.tsx @@ -804,6 +804,75 @@ const customSQLDialect = SQLDialect.define({ builtin: SQL_BUILTIN.join(" "), }); +function computeYamlNewlineIndent(lineText: string): string { + const indent = lineText.match(/^(\s*)/)?.[1] ?? ""; + const trimmed = lineText.trimEnd(); + + if (trimmed.endsWith(":")) { + // After "key:" with no value — increase indent + // For " - key:", base indent is at the dash content level + const dashMatch = trimmed.match(/^(\s*-\s+)/); + const baseIndent = dashMatch?.[1] ? " ".repeat(dashMatch[1].length) : indent; + return `${baseIndent} `; + } + if (/^\s*-\s*$/.test(trimmed)) { + // After bare "- " (array item marker only) — align to content after dash + const dashMatch = trimmed.match(/^(\s*-\s*)/); + return dashMatch?.[1] ? " ".repeat(dashMatch[1].length) : indent; + } + // Preserve current indent; for " - key: val" align to key level + const dashKeyMatch = trimmed.match(/^(\s*-\s+)\S/); + return dashKeyMatch?.[1] ? " ".repeat(dashKeyMatch[1].length) : indent; +} + +function yamlEnterKeymap(): Extension { + return keymap.of([{ + key: "Enter", + run: (view) => { + const { state } = view; + const pos = state.selection.main.head; + const line = state.doc.lineAt(pos); + const newIndent = computeYamlNewlineIndent(line.text); + + view.dispatch({ + changes: { from: pos, insert: `\n${newIndent}` }, + selection: { anchor: pos + 1 + newIndent.length }, + }); + return true; + }, + }]); +} + +function httpYamlEnterKeymap(): Extension { + return keymap.of([{ + key: "Enter", + run: (view) => { + const { state } = view; + const pos = state.selection.main.head; + const doc = state.doc.toString(); + + // Only handle if cursor is in YAML body (after blank line separator) + const textBeforeCursor = doc.slice(0, pos); + const blankLineIdx = textBeforeCursor.indexOf("\n\n"); + if (blankLineIdx === -1 || pos <= blankLineIdx + 1) return false; + + // Check if the body looks like YAML (not JSON) + const bodyStart = blankLineIdx + 2; + const bodyPrefix = doc.slice(bodyStart, bodyStart + 20).trimStart(); + if (bodyPrefix.startsWith("{") || bodyPrefix.startsWith("[")) return false; + + const line = state.doc.lineAt(pos); + const newIndent = computeYamlNewlineIndent(line.text); + + view.dispatch({ + changes: { from: pos, insert: `\n${newIndent}` }, + selection: { anchor: pos + 1 + newIndent.length }, + }); + return true; + }, + }]); +} + type LanguageMode = "json" | "http" | "sql" | "yaml"; function languageExtensions( @@ -828,6 +897,7 @@ function languageExtensions( ), syntaxHighlighting(customHighlightStyle), jsonAutoExpandBraces(), + httpYamlEnterKeymap(), ]; } else if (mode === "sql") { let dialect = customSQLDialect; @@ -842,32 +912,7 @@ function languageExtensions( return [ yaml(), syntaxHighlighting(customHighlightStyle), - keymap.of([{ - key: "Enter", - run: (view) => { - const { state } = view; - const line = state.doc.lineAt(state.selection.main.head); - const lineText = line.text; - const indent = lineText.match(/^(\s*)/)?.[1] ?? ""; - const trimmed = lineText.trimEnd(); - if (trimmed.endsWith(":")) { - // After "key:" — indent to key content level + 2 - const dashMatch = trimmed.match(/^(\s*-\s+)/); - const baseIndent = dashMatch?.[1] ? " ".repeat(dashMatch[1].length) : indent; - const newIndent = `${baseIndent} `; - view.dispatch({ - changes: { from: state.selection.main.head, insert: `\n${newIndent}` }, - selection: { anchor: state.selection.main.head + 1 + newIndent.length }, - }); - } else { - view.dispatch({ - changes: { from: state.selection.main.head, insert: `\n${indent}` }, - selection: { anchor: state.selection.main.head + 1 + indent.length }, - }); - } - return true; - }, - }]), + yamlEnterKeymap(), ]; } else { return [ From 256781c6d2149c70c3f938015a269233fd0fd892 Mon Sep 17 00:00:00 2001 From: Panthevm Date: Wed, 18 Mar 2026 20:23:51 +0300 Subject: [PATCH 09/55] Fix code editor --- .../components/code-editor/fhir-completion.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/packages/react-components/src/components/code-editor/fhir-completion.ts b/packages/react-components/src/components/code-editor/fhir-completion.ts index 96d193f6..6237b08f 100644 --- a/packages/react-components/src/components/code-editor/fhir-completion.ts +++ b/packages/react-components/src/components/code-editor/fhir-completion.ts @@ -663,6 +663,39 @@ async function resolveReferenceTargets( return targets.length > 0 ? targets : null; } +// Check if cursor is directly inside a JSON array (not inside an object within the array) +function isInsideJsonArray(doc: string, pos: number): boolean { + let depth = 0; + let inString = false; + let isEscaped = false; + for (let i = pos - 1; i >= 0; i--) { + const ch = doc[i]; + if (isEscaped) { + isEscaped = false; + continue; + } + if (ch === "\\") { + isEscaped = true; + continue; + } + if (ch === '"') { + inString = !inString; + continue; + } + if (inString) continue; + if (ch === "}" || ch === "]") { + depth++; + } else if (ch === "{") { + if (depth === 0) return false; + depth--; + } else if (ch === "[") { + if (depth === 0) return true; + depth--; + } + } + return false; +} + // Check if cursor is in a value position (after "key": or key: ) function isValuePosition(beforeCursor: string): string | null { const match = beforeCursor.match(/"?(\w+)"?\s*:\s*"?([^"]*)?$/); @@ -746,6 +779,9 @@ export function fhirCompletionSource( if (!isPropertyPosition) return null; + // Don't offer property completions inside arrays (e.g. "profile": [""]) + if (isInsideJsonArray(doc, pos)) return null; + const path = getJsonPathAtCursor(doc, pos); const rtMatch = doc.match(/"resourceType"\s*:\s*"([^"]+)"/) ?? doc.match(/resourceType\s*:\s*"([^"]+)"/); From ebdcccd17b57aa1f73d8a37092097dea4fbfd313 Mon Sep 17 00:00:00 2001 From: Panthevm Date: Thu, 19 Mar 2026 19:59:57 +0300 Subject: [PATCH 10/55] Update CodeMirror autocomplete --- packages/react-components/package.json | 1 + .../components/code-editor/fhir-completion.ts | 872 ++++++++++++++++-- .../src/components/code-editor/http/index.ts | 26 +- .../src/components/code-editor/index.tsx | 107 ++- pnpm-lock.yaml | 20 + 5 files changed, 937 insertions(+), 89 deletions(-) diff --git a/packages/react-components/package.json b/packages/react-components/package.json index fd2b0c49..2a38c6fb 100644 --- a/packages/react-components/package.json +++ b/packages/react-components/package.json @@ -75,6 +75,7 @@ "@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-toggle-group": "^1.1.11", "@radix-ui/react-tooltip": "^1.2.8", + "@replit/codemirror-vim": "^6.3.0", "@tanstack/react-table": "^8.21.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/packages/react-components/src/components/code-editor/fhir-completion.ts b/packages/react-components/src/components/code-editor/fhir-completion.ts index 6237b08f..86698ce3 100644 --- a/packages/react-components/src/components/code-editor/fhir-completion.ts +++ b/packages/react-components/src/components/code-editor/fhir-completion.ts @@ -1,8 +1,10 @@ -import type { - Completion, - CompletionContext, - CompletionResult, - CompletionSource, +import { + type Completion, + type CompletionContext, + type CompletionResult, + type CompletionSource, + completionStatus, + startCompletion, } from "@codemirror/autocomplete"; import { jsonLanguage } from "@codemirror/lang-json"; import { yamlLanguage } from "@codemirror/lang-yaml"; @@ -27,6 +29,7 @@ import type { SyntaxNode } from "@lezer/common"; interface FhirElementType { code: string; + profile?: string[]; targetProfile?: string[]; } @@ -39,12 +42,16 @@ interface FhirElement { type?: FhirElementType[]; binding?: { valueSet: string; strength: string }; contentReference?: string; + sliceName?: string; + fixedUri?: string; } interface StructureDefinition { type: string; + url?: string; name?: string; baseDefinition?: string; + context?: { expression: string; type: string }[]; differential?: { element: FhirElement[] }; } @@ -56,12 +63,18 @@ export interface StructureDefinitionSearchParams { kind?: string; _count?: string; _elements?: string; + _ilike?: string; } export type GetStructureDefinitions = ( params: StructureDefinitionSearchParams, ) => Promise; +export type ExpandValueSet = ( + url: string, + filter: string, +) => Promise<{ code: string; display?: string; system?: string }[]>; + // ── Cache ────────────────────────────────────────────────────────────── const sdCache = new Map(); @@ -69,7 +82,7 @@ const pendingRequests = new Map>(); const listCache = new Map(); const pendingListRequests = new Map>(); -const SD_ELEMENTS = "differential,type,name,baseDefinition"; +const SD_ELEMENTS = "differential,type,name,baseDefinition,url,context"; function cacheKey(params: StructureDefinitionSearchParams): string { return JSON.stringify(params); @@ -300,14 +313,19 @@ function findElement( if (direct) return direct; // Choice type match: key "deceasedBoolean" → element "deceased[x]" with type boolean - return elements.find((el) => { - if (!el.path.endsWith("[x]")) return false; - if (!el.path.startsWith(`${parentPath}.`)) return false; + // Return element with only the matched type so resolveElements picks the right one + for (const el of elements) { + if (!el.path.endsWith("[x]")) continue; + if (!el.path.startsWith(`${parentPath}.`)) continue; const baseName = fieldName(el); - if (!key.toLowerCase().startsWith(baseName.toLowerCase())) return false; + if (!key.toLowerCase().startsWith(baseName.toLowerCase())) continue; const typeSuffix = key.slice(baseName.length).toLowerCase(); - return el.type?.some((t) => t.code.toLowerCase() === typeSuffix) ?? false; - }); + const matchedType = el.type?.find((t) => t.code.toLowerCase() === typeSuffix); + if (matchedType) { + return { ...el, type: [matchedType] }; + } + } + return undefined; } // ── Resolve completions at path ──────────────────────────────────────── @@ -318,14 +336,6 @@ async function collectAllElements( getSDs: GetStructureDefinitions, ): Promise<{ elements: FhirElement[]; basePath: string } | null> { const sd = await getCachedSD(type, getSDs); - console.log( - "[fhir] collectAllElements:", - type, - "sd:", - sd?.type, - "elements:", - sd?.differential?.element?.length, - ); if (!sd?.differential?.element) return null; const elements = [...sd.differential.element]; @@ -512,6 +522,8 @@ function snippetKind(element: FhirElement): SnippetKind { if (element.contentReference) return isArray ? "array-complex" : "object"; return "bare"; } + // Extension arrays get special treatment — show extension URL picker + if (typeCode === "Extension" && isArray) return "array-primitive"; if (isArray) return isPrimitiveType(typeCode) ? "array-primitive" : "array-complex"; if (FHIR_NUMBER_TYPES.has(typeCode)) return "number"; @@ -535,8 +547,8 @@ function buildSnippet( }; } case "array-primitive": { - const text = `"${name}": []`; - return { text, cursorOffset: text.length - 1 }; + const text = `"${name}": [\n${inner}\n${indent}]`; + return { text, cursorOffset: text.indexOf(inner + "\n") + inner.length }; } case "object": { const text = `"${name}": {\n${inner}\n${indent}}`; @@ -555,6 +567,86 @@ function buildSnippet( } } +// ── Extension helpers ────────────────────────────────────────────────── + +interface ExtensionInfo { + url: string; + name?: string | undefined; + isNested: boolean; + valueTypes: string[]; + slices: { sliceName: string; fixedUri: string; short?: string | undefined }[]; +} + +function analyzeExtensionSD(sd: StructureDefinition): ExtensionInfo | null { + if (!sd.differential?.element) return null; + const elements = sd.differential.element; + + const valueEl = elements.find((e) => e.path === "Extension.value[x]"); + const isNested = valueEl?.max === "0"; + const valueTypes = isNested ? [] : (valueEl?.type?.map((t) => t.code) ?? []); + + const slices: ExtensionInfo["slices"] = []; + for (const el of elements) { + if (el.path === "Extension.extension" && el.sliceName) { + const sliceName = el.sliceName; + const urlEl = elements.find( + (e) => + e.path === "Extension.extension.url" && + e.fixedUri && + elements.indexOf(e) > elements.indexOf(el), + ); + const fixedUri = urlEl?.fixedUri ?? sliceName; + slices.push({ sliceName, fixedUri, short: el.short }); + } + } + + return { + url: sd.url ?? sd.type, + name: sd.name, + isNested, + valueTypes, + slices, + }; +} + +function extensionValueFieldName(typeCode: string): string { + return `value${typeCode.charAt(0).toUpperCase()}${typeCode.slice(1)}`; +} + +function buildExtensionSnippet( + extInfo: ExtensionInfo, + indent: string, +): { text: string; cursorOffset: number } { + const inner = indent + " "; + + if (extInfo.isNested) { + const innerInner = inner + " "; + const text = `{\n${inner}"url": "${extInfo.url}",\n${inner}"extension": [\n${innerInner}\n${inner}]\n${indent}}`; + return { text, cursorOffset: text.indexOf(innerInner + "\n" + inner) + innerInner.length }; + } + + if (extInfo.valueTypes.length === 1) { + const vField = extensionValueFieldName(extInfo.valueTypes[0]!); + const typeCode = extInfo.valueTypes[0]!; + if (FHIR_STRING_TYPES.has(typeCode) || typeCode === "code") { + const text = `{\n${inner}"url": "${extInfo.url}",\n${inner}"${vField}": ""\n${indent}}`; + return { text, cursorOffset: text.lastIndexOf('""') + 1 }; + } + if (FHIR_NUMBER_TYPES.has(typeCode)) { + const text = `{\n${inner}"url": "${extInfo.url}",\n${inner}"${vField}": \n${indent}}`; + return { text, cursorOffset: text.lastIndexOf(": ") + 2 }; + } + // Complex type (Coding, CodeableConcept, etc.) + const innerInner = inner + " "; + const text = `{\n${inner}"url": "${extInfo.url}",\n${inner}"${vField}": {\n${innerInner}\n${inner}}\n${indent}}`; + return { text, cursorOffset: text.indexOf(innerInner + "\n" + inner) + innerInner.length }; + } + + // Multiple types or no types — just url, user picks value field + const text = `{\n${inner}"url": "${extInfo.url}",\n${inner}\n${indent}}`; + return { text, cursorOffset: text.indexOf(inner + "\n" + indent) + inner.length }; +} + function toCompletion(element: FhirElement): Completion { const name = fieldName(element); const types = element.type?.map((t) => t.code).join(" | ") ?? ""; @@ -595,6 +687,11 @@ function toCompletion(element: FhirElement): Completion { changes: { from: actualFrom, to: actualTo, insert: text }, selection: { anchor: actualFrom + cursorOffset }, }); + + // Trigger value autocomplete after inserting a snippet with cursor in value position + if (kind === "string" || kind === "array-primitive") { + setTimeout(() => startCompletion(view), 0); + } }, }; if (element.short) completion.info = element.short; @@ -603,6 +700,186 @@ function toCompletion(element: FhirElement): Completion { // ── Completion source ────────────────────────────────────────────────── +// ── Terminology binding resolution ───────────────────────────────────── + +// Build the FHIR element path for a given JSON path + valueKey +// e.g. resourceType="Patient", path=["address"], valueKey="state" → "Patient.address.state" +// For "code" inside Coding: path=["maritalStatus","coding"], valueKey="code" → walk up to find bound parent +function buildFhirElementPath( + resourceType: string, + path: string[], + valueKey: string, +): string { + return `${resourceType}.${[...path, valueKey].join(".")}`; +} + +// Check if a profile's differential overrides the binding for a given element path +async function findProfileBinding( + profileUrls: string[], + resourceType: string, + path: string[], + valueKey: string, + getSDs: GetStructureDefinitions, +): Promise { + if (profileUrls.length === 0) return null; + + for (const profileUrl of profileUrls) { + const sd = await getCachedSD(profileUrl, getSDs); + if (!sd?.differential?.element) continue; + + // Direct match: e.g. Patient.gender + const directPath = buildFhirElementPath(resourceType, path, valueKey); + for (const el of sd.differential.element) { + if (el.path === directPath && el.binding?.valueSet) { + return el.binding.valueSet; + } + } + + // For "code" inside Coding/CodeableConcept — check parent paths + if (valueKey === "code") { + for (let i = path.length; i > 0; i--) { + const parentFhirPath = buildFhirElementPath(resourceType, path.slice(0, i - 1), path[i - 1]!); + for (const el of sd.differential.element) { + if (el.path === parentFhirPath && el.binding?.valueSet) { + return el.binding.valueSet; + } + } + } + } + } + return null; +} + +// Find binding from extension SD differential for a value inside extension object +async function findExtensionBinding( + doc: string, + pos: number, + getSDs: GetStructureDefinitions, +): Promise { + const textBefore = doc.slice(0, pos); + + // Find the nearest extension "url" in current or parent object + // Match "url": "value" scanning backwards through the text + const urlMatches = [...textBefore.matchAll(/"url"\s*:\s*"([^"]+)"/g)]; + if (urlMatches.length === 0) return null; + + // Work backwards from the most recent url match + for (let i = urlMatches.length - 1; i >= 0; i--) { + const extUrl = urlMatches[i]![1]!; + // Skip non-extension URLs (like profile URLs) + if (!extUrl.includes("StructureDefinition/") && !extUrl.includes("Extension")) { + // Could be a slice name like "ombCategory" — find the parent extension URL + const parentUrlMatches = [...textBefore.slice(0, urlMatches[i]!.index).matchAll(/"url"\s*:\s*"([^"]+)"/g)]; + for (let j = parentUrlMatches.length - 1; j >= 0; j--) { + const parentUrl = parentUrlMatches[j]![1]!; + if (!parentUrl.includes("/")) continue; + const parentSD = await getCachedSD(parentUrl, getSDs); + if (!parentSD?.differential?.element) continue; + // Find the slice matching extUrl and check its value[x] binding + let inSlice = false; + for (const el of parentSD.differential.element) { + if (el.path === "Extension.extension" && el.sliceName) { + const urlEl = parentSD.differential.element.find( + (e) => e.path === "Extension.extension.url" && e.fixedUri && + parentSD.differential!.element.indexOf(e) > parentSD.differential!.element.indexOf(el), + ); + if ((urlEl?.fixedUri ?? el.sliceName) === extUrl) { + inSlice = true; + continue; + } + if (inSlice) break; + } + if (inSlice && el.path === "Extension.extension.value[x]" && el.binding?.valueSet) { + return el.binding.valueSet; + } + } + if (inSlice) break; + } + continue; + } + // Direct extension URL — check its value[x] binding + const sd = await getCachedSD(extUrl, getSDs); + if (!sd?.differential?.element) continue; + for (const el of sd.differential.element) { + if (el.path === "Extension.value[x]" && el.binding?.valueSet) { + return el.binding.valueSet; + } + } + } + return null; +} + +async function findBindingForValue( + path: string[], + valueKey: string, + resourceType: string, + getSDs: GetStructureDefinitions, + profileUrls: string[] = [], + doc?: string, + pos?: number, +): Promise { + // Check extension binding first (for values inside extension objects) + if (doc != null && pos != null) { + // Check if we're inside an extension context (path contains "extension") + const inExtension = path.some((p) => p === "extension" || p === "modifierExtension"); + if (inExtension) { + const extBinding = await findExtensionBinding(doc, pos, getSDs); + if (extBinding) return extBinding; + } + } + + // Check profile overrides first + const profileBinding = await findProfileBinding(profileUrls, resourceType, path, valueKey, getSDs); + if (profileBinding) return profileBinding; + + // Case 1: Direct binding on the field (e.g. code type like Patient.gender) + const elements = await resolveElements(path, resourceType, getSDs); + for (const el of elements) { + if (fieldName(el) === valueKey && el.binding?.valueSet) { + return el.binding.valueSet; + } + } + + // Case 2: valueKey is "code" — walk up to find Coding/CodeableConcept with binding + // Handles: Coding.code, CodeableConcept.coding[].code + if (valueKey === "code") { + for (let i = path.length; i > 0; i--) { + const parentElements = await resolveElements( + path.slice(0, i - 1), + resourceType, + getSDs, + ); + for (const el of parentElements) { + if (fieldName(el) === path[i - 1] && el.binding?.valueSet) { + return el.binding.valueSet; + } + } + } + } + + return null; +} + +// Find canonical targetProfile for an array element (e.g. Meta.profile → StructureDefinition) +// Returns the FHIR resource type name from the targetProfile URL, or null +async function findCanonicalTargetType( + path: string[], + arrayKey: string, + resourceType: string, + getSDs: GetStructureDefinitions, +): Promise { + const elements = await resolveElements(path, resourceType, getSDs); + for (const el of elements) { + if (fieldName(el) !== arrayKey) continue; + if (el.max !== "*") continue; + const t = el.type?.[0]; + if (t?.code !== "canonical" || !t.targetProfile?.length) continue; + // Extract resource type from targetProfile URL + return t.targetProfile[0]?.split("/").pop() ?? null; + } + return null; +} + // ── Reference target resolution ──────────────────────────────────────── async function resolveReferenceTargets( @@ -663,39 +940,6 @@ async function resolveReferenceTargets( return targets.length > 0 ? targets : null; } -// Check if cursor is directly inside a JSON array (not inside an object within the array) -function isInsideJsonArray(doc: string, pos: number): boolean { - let depth = 0; - let inString = false; - let isEscaped = false; - for (let i = pos - 1; i >= 0; i--) { - const ch = doc[i]; - if (isEscaped) { - isEscaped = false; - continue; - } - if (ch === "\\") { - isEscaped = true; - continue; - } - if (ch === '"') { - inString = !inString; - continue; - } - if (inString) continue; - if (ch === "}" || ch === "]") { - depth++; - } else if (ch === "{") { - if (depth === 0) return false; - depth--; - } else if (ch === "[") { - if (depth === 0) return true; - depth--; - } - } - return false; -} - // Check if cursor is in a value position (after "key": or key: ) function isValuePosition(beforeCursor: string): string | null { const match = beforeCursor.match(/"?(\w+)"?\s*:\s*"?([^"]*)?$/); @@ -703,9 +947,36 @@ function isValuePosition(beforeCursor: string): string | null { return null; } +// Extract meta.profile URLs from JSON document +function getJsonProfileUrls(doc: string): string[] { + const match = doc.match(/"profile"\s*:\s*\[([\s\S]*?)\]/); + if (!match?.[1]) return []; + const urls: string[] = []; + const re = /"([^"]+)"/g; + let m: RegExpExecArray | null; + while ((m = re.exec(match[1])) !== null) { + if (m[1]) urls.push(m[1]); + } + return urls; +} + +// Extract meta.profile URLs from YAML document +function getYamlProfileUrls(doc: string): string[] { + const profileSection = doc.match(/profile:\s*\n((?:\s+-\s+.+\n?)*)/); + if (!profileSection?.[1]) return []; + const urls: string[] = []; + const re = /-\s+['"]?([^'"\n]+)['"]?/g; + let m: RegExpExecArray | null; + while ((m = re.exec(profileSection[1])) !== null) { + if (m[1]) urls.push(m[1].trim()); + } + return urls; +} + export function fhirCompletionSource( getSDs: GetStructureDefinitions, resourceTypeHint?: string, + expandValueSet?: ExpandValueSet, ): CompletionSource { return async ( context: CompletionContext, @@ -770,6 +1041,363 @@ export function fhirCompletionSource( } } + // Terminology binding value completion + if (valueKey && valueKey !== "resourceType" && valueKey !== "reference" && expandValueSet) { + const path = getJsonPathAtCursor(doc, pos); + const rtMatch = doc.match(/"resourceType"\s*:\s*"([^"]+)"/); + const resourceType = rtMatch?.[1] ?? resourceTypeHint; + if (resourceType) { + const profileUrls = getJsonProfileUrls(doc); + const valueSetUrl = await findBindingForValue(path, valueKey, resourceType, getSDs, profileUrls, doc, pos); + if (valueSetUrl) { + // Include opening quote in match so from < pos (triggers auto-show) + const quoteWord = context.matchBefore(/"[\w-]*/); + const from = quoteWord?.from ?? pos; + const filter = quoteWord ? quoteWord.text.replace(/^"/, "") : ""; + try { + const codes = await expandValueSet(valueSetUrl, filter); + if (codes.length > 0) { + const options: Completion[] = codes.map((c) => ({ + label: c.code, + ...(c.display ? { info: c.display } : {}), + type: "text", + apply: (view: EditorView, _c: Completion, applyFrom: number, applyTo: number) => { + const d = view.state.doc.toString(); + let actualFrom = applyFrom; + let actualTo = applyTo; + if (d[actualFrom] === '"') actualFrom++; + if (actualTo < d.length && d[actualTo] === '"') actualTo++; + view.dispatch({ + changes: { from: actualFrom, to: actualTo, insert: `${c.code}"` }, + selection: { anchor: actualFrom + c.code.length + 1 }, + }); + }, + })); + return { from, options, filter: false }; + } + } catch { + // expand failed — fall through + } + } + } + } + + // Canonical array completion (e.g. meta.profile → StructureDefinition profiles) + // Use regex to detect array context — isInsideJsonArray fails inside "" + { + const textBefore = doc.slice(0, pos); + // Match "key": [ ... with cursor inside the array (possibly inside "") + const arrayMatch = textBefore.match(/"(\w+)"\s*:\s*\[\s*(?:"[^"]*"\s*,\s*)*"?[^"]*$/s); + if (arrayMatch) { + const arrayKey = arrayMatch[1]!; + // Find JSON body start for correct path resolution + const bodyStart = doc.indexOf("\n\n"); + const jsonStart = bodyStart !== -1 ? bodyStart + 2 : 0; + const jsonBody = doc.slice(jsonStart); + const posInBody = pos - jsonStart; + // Get path excluding the array key itself (parent path) + const fullPath = getJsonPathAtCursor(jsonBody, posInBody); + // The array key is the last segment pushed by { before [ + // Remove it to get parentPath + const parentPath = fullPath.length > 0 && fullPath[fullPath.length - 1] === arrayKey + ? fullPath.slice(0, -1) + : fullPath; + const rtMatch = doc.match(/"resourceType"\s*:\s*"([^"]+)"/); + const resourceType = rtMatch?.[1] ?? resourceTypeHint; + if (resourceType) { + const targetType = await findCanonicalTargetType(parentPath, arrayKey, resourceType, getSDs); + if (targetType === "StructureDefinition") { + const allSDs = await getCachedSDList( + { type: `${resourceType},DomainResource,Resource`, derivation: "constraint", _elements: "url,name", _count: "50" }, + getSDs, + ); + const seen = new Set(); + const uniqueSDs = allSDs.filter((sd) => { + const u = sd.url ?? sd.type; + if (seen.has(u)) return false; + seen.add(u); + return true; + }); + if (uniqueSDs.length > 0) { + const quoteWord = context.matchBefore(/"[^"]*/); + const bareWord = context.matchBefore(/[\w.:/-]*/); + const from = quoteWord?.from ?? bareWord?.from ?? pos; + const filter = quoteWord + ? quoteWord.text.replace(/^"/, "").toLowerCase() + : (bareWord?.text.toLowerCase() ?? ""); + const filtered = filter + ? uniqueSDs.filter((sd) => sd.name?.toLowerCase().includes(filter) || sd.url?.toLowerCase().includes(filter)) + : uniqueSDs; + const options: Completion[] = filtered.map((sd) => { + const url = sd.url ?? sd.type; + return { + label: url, + ...(sd.name ? { info: sd.name } : {}), + type: "text", + apply: (view: EditorView, _c: Completion, applyFrom: number, applyTo: number) => { + const d = view.state.doc.toString(); + let actualTo = applyTo; + if (actualTo < d.length && d[actualTo] === '"') actualTo++; + view.dispatch({ + changes: { from: applyFrom, to: actualTo, insert: `"${url}"` }, + selection: { anchor: applyFrom + url.length + 2 }, + }); + }, + }; + }); + if (options.length > 0) { + return { from, options, filter: false }; + } + } + } + if (targetType) return null; + } + } + } + + // Extension array completion — offer extension URLs with full snippets + { + const textBefore = doc.slice(0, pos); + const extArrayMatch = textBefore.match(/"(?:extension|modifierExtension)"\s*:\s*\[\s*(?:\{[\s\S]*?\}\s*,?\s*)*[\w.:/-]*$/s); + if (extArrayMatch) { + const rtMatch = doc.match(/"resourceType"\s*:\s*"([^"]+)"/); + const resourceType = rtMatch?.[1] ?? resourceTypeHint; + { + // Also check if we're inside a nested extension (parent has url) + const parentUrlMatch = textBefore.match(/"url"\s*:\s*"([^"]+)"[\s\S]*?"extension"\s*:\s*\[\s*(?:\{[\s\S]*?\}\s*,?\s*)*[\w.:/-]*$/s); + const parentExtUrl = parentUrlMatch?.[1]; + + if (parentExtUrl) { + // Nested: offer slice URLs from parent extension + const parentSD = await getCachedSD(parentExtUrl, getSDs); + if (parentSD) { + const parentInfo = analyzeExtensionSD(parentSD); + if (parentInfo?.slices.length) { + const line = state.doc.lineAt(pos); + const indent = line.text.match(/^(\s*)/)?.[1] ?? ""; + const bareWord = context.matchBefore(/[\w.:/-]*/); + const filter = bareWord?.text.toLowerCase() ?? ""; + const matchingSlices = filter + ? parentInfo.slices.filter((s) => s.fixedUri.toLowerCase().includes(filter) || (s.short?.toLowerCase().includes(filter) ?? false)) + : parentInfo.slices; + const options: Completion[] = matchingSlices.map((slice) => { + // Find value type for this slice + const sliceElements = parentSD.differential?.element ?? []; + let sliceValueTypes: string[] = []; + let inSlice = false; + for (const el of sliceElements) { + if (el.path === "Extension.extension" && el.sliceName === slice.sliceName) { + inSlice = true; + continue; + } + if (inSlice && el.path === "Extension.extension.value[x]") { + sliceValueTypes = el.type?.map((t) => t.code) ?? []; + break; + } + if (inSlice && el.path === "Extension.extension" && el.sliceName) { + break; // next slice + } + } + const sliceInfo: ExtensionInfo = { + url: slice.fixedUri, + name: slice.short, + isNested: false, + valueTypes: sliceValueTypes, + slices: [], + }; + const { text: snippet, cursorOffset } = buildExtensionSnippet(sliceInfo, indent); + return { + label: slice.fixedUri, + ...(slice.short ? { info: slice.short } : {}), + type: "text", + apply: (view: EditorView, _c: Completion, applyFrom: number, applyTo: number) => { + view.dispatch({ + changes: { from: applyFrom, to: applyTo, insert: snippet }, + selection: { anchor: applyFrom + cursorOffset }, + }); + setTimeout(() => startCompletion(view), 0); + }, + }; + }); + if (options.length > 0) { + return { from: bareWord?.from ?? pos, options, filter: false }; + } + } + } + } else { + // Collect extension URLs from profile SD (priority) + const profileUrls = getJsonProfileUrls(doc); + const profileExtUrls: string[] = []; + for (const pUrl of profileUrls) { + const profileSD = await getCachedSD(pUrl, getSDs); + if (!profileSD?.differential?.element) continue; + for (const el of profileSD.differential.element) { + if (el.type?.[0]?.code !== "Extension") continue; + for (const t of el.type ?? []) { + for (const p of t.profile ?? []) { + // Strip version from profile URL + const clean = p.includes("|") ? p.slice(0, p.indexOf("|")) : p; + if (!profileExtUrls.includes(clean)) profileExtUrls.push(clean); + } + } + } + } + + // Determine container type for context filtering + // e.g. inside address[].extension → context = [Address, Element] + // Root extension → context = [Patient, DomainResource, Resource, Element] + const bodyStart = doc.indexOf("\n\n"); + const jsonStart = bodyStart !== -1 ? bodyStart + 2 : 0; + const jsonBody = doc.slice(jsonStart); + const posInBody = pos - jsonStart; + const fullPath = getJsonPathAtCursor(jsonBody, posInBody); + + let containerType: string | null = null; + if (fullPath.length > 0 && resourceType) { + let currentRT = resourceType; + for (const seg of fullPath) { + const elements = await resolveElements([], currentRT, getSDs); + const el = elements.find((e) => fieldName(e) === seg); + if (el?.type?.[0]?.code) { + currentRT = el.type[0].code; + } else { + break; + } + } + if (currentRT !== resourceType) { + containerType = currentRT; + } + } + + // Build FHIR path for context matching (e.g. "Patient.address") + const fhirPath = containerType && resourceType + ? `${resourceType}.${fullPath.join(".")}` + : null; + const contextMatchers: string[] | null = containerType + ? [containerType, "Element", ...(fhirPath ? [fhirPath] : [])] + : resourceType + ? [resourceType, "DomainResource", "Resource", "Element"] + : null; + const bareWord = context.matchBefore(/[\w.:/-]*/); + const filter = bareWord?.text ?? ""; + const searchParams: StructureDefinitionSearchParams = { + type: "Extension", + derivation: "constraint", + _elements: "url,context", + _count: "500", + }; + if (filter) searchParams._ilike = filter; + const results = await getCachedSDList(searchParams, getSDs); + const contextExts = contextMatchers + ? results.filter((sd) => + sd.context?.some((c) => c.type === "element" && contextMatchers.includes(c.expression)), + ) + : results; + + // Merge: profile extensions first (only at root level), then context-matched + const seen = new Set(); + const allUrls: { url: string; name?: string | undefined; boost: number }[] = []; + if (!containerType) { + for (const u of profileExtUrls) { + if (seen.has(u)) continue; + seen.add(u); + allUrls.push({ url: u, boost: 10 }); + } + } + for (const sd of contextExts) { + const u = sd.url ?? sd.type; + if (seen.has(u)) continue; + seen.add(u); + // Boost extensions whose context matches the container type specifically + const isSpecific = containerType && sd.context?.some( + (c) => c.type === "element" && (c.expression === containerType || c.expression === fhirPath), + ); + allUrls.push({ url: u, name: sd.name, boost: isSpecific ? 5 : 0 }); + } + + const lowerFilter = filter.toLowerCase(); + const filtered = lowerFilter + ? allUrls.filter((e) => e.url.toLowerCase().includes(lowerFilter) || (e.name?.toLowerCase().includes(lowerFilter) ?? false)) + : allUrls; + if (filtered.length > 0) { + const line = state.doc.lineAt(pos); + const indent = line.text.match(/^(\s*)/)?.[1] ?? ""; + const options: Completion[] = filtered.map((ext) => ({ + label: ext.url, + ...(ext.name ? { info: ext.name } : {}), + type: "text", + boost: ext.boost, + apply: (view: EditorView, _c: Completion, applyFrom: number, applyTo: number) => { + getCachedSD(ext.url, getSDs).then((fullSD) => { + const info = fullSD ? analyzeExtensionSD(fullSD) : null; + const snippet = info + ? buildExtensionSnippet(info, indent) + : { text: `{\n${indent}"url": "${ext.url}"\n${indent.slice(2)}}`, cursorOffset: 0 }; + view.dispatch({ + changes: { from: applyFrom, to: applyTo, insert: snippet.text }, + selection: { anchor: applyFrom + snippet.cursorOffset }, + }); + setTimeout(() => startCompletion(view), 0); + }); + }, + })); + const from = bareWord?.from ?? pos; + return { from, options, filter: false }; + } + } + } + } + } + + // Extension object field completion — offer valueXxx fields inside {"url": "...", |} + { + const textBefore = doc.slice(0, pos); + // Check if inside an object that has a "url" key (extension object) + const urlInObjMatch = textBefore.match(/"url"\s*:\s*"([^"]+)"[^{}]*$/s); + if (urlInObjMatch && (beforeCursor === "" || beforeCursor === '"' || /^"?[\w]*$/.test(beforeCursor))) { + const extUrl = urlInObjMatch[1]!; + const sd = await getCachedSD(extUrl, getSDs); + if (sd) { + const info = analyzeExtensionSD(sd); + if (info && info.valueTypes.length > 1) { + // Multiple value types — offer valueXxx fields + const options: Completion[] = info.valueTypes.map((typeCode) => { + const vField = extensionValueFieldName(typeCode); + const vKind = FHIR_STRING_TYPES.has(typeCode) || typeCode === "code" ? "string" + : FHIR_NUMBER_TYPES.has(typeCode) ? "number" + : "object"; + return { + label: vField, + detail: typeCode, + type: "property", + apply: (view: EditorView, _c: Completion, applyFrom: number, applyTo: number) => { + const d = view.state.doc.toString(); + let actualFrom = applyFrom; + let actualTo = applyTo; + if (actualFrom > 0 && d[actualFrom - 1] === '"') actualFrom--; + if (actualTo < d.length && d[actualTo] === '"') actualTo++; + const lineObj = view.state.doc.lineAt(actualFrom); + const indentStr = lineObj.text.match(/^(\s*)/)?.[1] ?? ""; + const { text, cursorOffset } = buildSnippet(vField, vKind as SnippetKind, indentStr); + view.dispatch({ + changes: { from: actualFrom, to: actualTo, insert: text }, + selection: { anchor: actualFrom + cursorOffset }, + }); + if (vKind === "string") { + setTimeout(() => startCompletion(view), 0); + } + }, + }; + }); + const word = context.matchBefore(/"?\w*/); + let from = word?.from ?? pos; + if (from < doc.length && doc[from] === '"') from++; + return { from, options, validFor: /^\w*$/ }; + } + } + } + } + // Property name position — with or without quotes const isPropertyPosition = beforeCursor === "" || @@ -779,8 +1407,9 @@ export function fhirCompletionSource( if (!isPropertyPosition) return null; - // Don't offer property completions inside arrays (e.g. "profile": [""]) - if (isInsideJsonArray(doc, pos)) return null; + // Only auto-trigger property completions when the user has started typing a name + // Don't auto-trigger after a comma without typing (e.g. "Patient", |) + if (!context.explicit && /,\s*"?\s*$/.test(beforeCursor) && !context.matchBefore(/\w+/)) return null; const path = getJsonPathAtCursor(doc, pos); const rtMatch = doc.match(/"resourceType"\s*:\s*"([^"]+)"/) ?? @@ -911,6 +1540,10 @@ function toYamlFieldCompletion(element: FhirElement): Completion { changes: { from, to, insert: text }, selection: { anchor: from + cursorOffset }, }); + + if (isArray || isString) { + setTimeout(() => startCompletion(view), 0); + } }, }; if (element.short) completion.info = element.short; @@ -922,6 +1555,7 @@ function toYamlFieldCompletion(element: FhirElement): Completion { export function yamlFhirCompletionSource( getSDs: GetStructureDefinitions, resourceTypeHint?: string, + expandValueSet?: ExpandValueSet, ): CompletionSource { return async ( context: CompletionContext, @@ -976,8 +1610,86 @@ export function yamlFhirCompletionSource( } } + // Terminology binding value completion + if (valueKey && valueKey !== "resourceType" && valueKey !== "reference" && expandValueSet) { + const path = getYamlPathAtCursor(doc, pos); + const resourceType = getYamlResourceType(doc) ?? resourceTypeHint; + if (resourceType) { + const profileUrls = getYamlProfileUrls(doc); + const valueSetUrl = await findBindingForValue(path, valueKey, resourceType, getSDs, profileUrls, doc, pos); + if (valueSetUrl) { + const word = context.matchBefore(/[\w-]*/); + const filter = word ? doc.slice(word.from, pos) : ""; + try { + const codes = await expandValueSet(valueSetUrl, filter); + if (codes.length > 0) { + const options: Completion[] = codes.map((c) => ({ + label: c.code, + ...(c.display ? { info: c.display } : {}), + type: "text", + })); + return { from: word?.from ?? pos, options, validFor: /^[\w-]*$/ }; + } + } catch { + // expand failed — fall through + } + } + } + } + + // Canonical array completion in YAML (e.g. meta.profile → StructureDefinition profiles) + { + const trimmed = beforeCursor.trimStart(); + const isArrayItem = trimmed === "-" || trimmed === "- " || trimmed.startsWith("- "); + if (isArrayItem) { + const path = getYamlPathAtCursor(doc, pos); + const arrayKey = path[path.length - 1]; + if (path.length > 0 && arrayKey) { + const parentPath = path.slice(0, -1); + const resourceType = getYamlResourceType(doc) ?? resourceTypeHint; + if (resourceType) { + const targetType = await findCanonicalTargetType(parentPath, arrayKey, resourceType, getSDs); + if (targetType === "StructureDefinition") { + const allSDs = await getCachedSDList( + { type: `${resourceType},DomainResource,Resource`, derivation: "constraint", _elements: "url,name", _count: "50" }, + getSDs, + ); + const seen = new Set(); + const uniqueSDs = allSDs.filter((sd) => { + const u = sd.url ?? sd.type; + if (seen.has(u)) return false; + seen.add(u); + return true; + }); + if (uniqueSDs.length > 0) { + const word = context.matchBefore(/[\w.:/-]*/); + const from = word?.from ?? pos; + const filter = word?.text.toLowerCase() ?? ""; + const filtered = filter + ? uniqueSDs.filter((sd) => sd.name?.toLowerCase().includes(filter) || sd.url?.toLowerCase().includes(filter)) + : uniqueSDs; + const options: Completion[] = filtered.map((sd) => ({ + label: sd.url ?? sd.type, + ...(sd.name ? { info: sd.name } : {}), + type: "text", + })); + if (options.length > 0) { + return { from, options, filter: false }; + } + } + } + if (targetType) return null; + } + } + } + } + if (!isYamlPropertyPosition(beforeCursor)) return null; + // Only auto-trigger property completions when the user has started typing a name + // Don't auto-trigger after a comma without typing (e.g. "Patient", |) + if (!context.explicit && /,\s*"?\s*$/.test(beforeCursor) && !context.matchBefore(/\w+/)) return null; + const path = getYamlPathAtCursor(doc, pos); const hasExplicitResourceType = !!getYamlResourceType(doc); const resourceType = getYamlResourceType(doc) ?? resourceTypeHint; @@ -1097,11 +1809,13 @@ function walkYamlObject( const indent = line.length - trimmed.length; const isArrayItem = trimmed.startsWith("- "); const content = isArrayItem ? trimmed.slice(2) : trimmed; - const colonIdx = content.indexOf(":"); + // Skip array scalar values (quoted strings, URLs, or bare values) + if (isArrayItem && (content.startsWith("'") || content.startsWith('"') || !content.includes(": "))) continue; + const colonIdx = content.indexOf(": "); if (colonIdx <= 0) continue; const key = content.slice(0, colonIdx).trim(); - const valueAfterColon = content.slice(colonIdx + 1).trim(); + const valueAfterColon = content.slice(colonIdx + 2).trim(); // Pop stack to find parent while (stack.length > 1 && stack[stack.length - 1]!.indent >= indent) { @@ -1145,9 +1859,9 @@ function walkYamlObject( // Check for empty strings if (emptyStrings && (valueAfterColon === "''" || valueAfterColon === '""')) { - const afterColonStr = content.slice(colonIdx + 1); + const afterColonStr = content.slice(colonIdx + 2); const wsLen = afterColonStr.length - afterColonStr.trimStart().length; - const emptyFrom = charOffset + keyIndent + colonIdx + 1 + wsLen; + const emptyFrom = charOffset + keyIndent + colonIdx + 2 + wsLen; const emptyTo = emptyFrom + 2; emptyStrings.push({ from: emptyFrom, to: emptyTo }); } @@ -1491,9 +2205,18 @@ function buildFhirValidationPlugin( let timeout: ReturnType | null = null; let destroyed = false; + function hasActiveDiagnostics() { + try { + return view.state.field(fhirDiagnosticsField).messages.size > 0; + } catch { + return false; + } + } + function scheduleCheck() { if (timeout) clearTimeout(timeout); - timeout = setTimeout(() => check(), 500); + const delay = hasActiveDiagnostics() ? 0 : 1500; + timeout = setTimeout(() => check(), delay); } async function check() { @@ -1591,12 +2314,33 @@ function buildFhirValidationPlugin( export function buildFhirCompletionExtension( getSDs: GetStructureDefinitions, resourceTypeHint?: string, + expandValueSet?: ExpandValueSet, ): Extension { - const jsonSource = fhirCompletionSource(getSDs, resourceTypeHint); - const yamlSource = yamlFhirCompletionSource(getSDs, resourceTypeHint); + const jsonSource = fhirCompletionSource(getSDs, resourceTypeHint, expandValueSet); + const yamlSource = yamlFhirCompletionSource(getSDs, resourceTypeHint, expandValueSet); + // Trigger completion on empty lines inside objects (where from === pos + // would cause CodeMirror to suppress auto-triggered results) + const autoTrigger = EditorView.updateListener.of((update) => { + if (!update.docChanged) return; + if (completionStatus(update.view.state)) return; + const { state } = update.view; + const pos = state.selection.main.head; + const doc = state.doc.toString(); + const line = state.doc.lineAt(pos); + const beforeCursor = line.text.slice(0, pos - line.from).trimStart(); + // Empty line, inside [] or inside "" + const shouldTrigger = + beforeCursor === "" || + (pos > 0 && doc[pos - 1] === "[") || + (pos > 0 && doc[pos - 1] === '"' && pos > 1 && doc[pos - 2] !== "\\"); + if (!shouldTrigger) return; + setTimeout(() => startCompletion(update.view), 0); + }); + return [ jsonLanguage.data.of({ autocomplete: jsonSource }), yamlLanguage.data.of({ autocomplete: yamlSource }), + autoTrigger, fhirDiagnosticsField, fhirLinterTheme, buildFhirValidationPlugin(getSDs, resourceTypeHint), diff --git a/packages/react-components/src/components/code-editor/http/index.ts b/packages/react-components/src/components/code-editor/http/index.ts index 41a084c4..a18d087a 100644 --- a/packages/react-components/src/components/code-editor/http/index.ts +++ b/packages/react-components/src/components/code-editor/http/index.ts @@ -234,13 +234,25 @@ const HEADER_VALUES: Record = { ], }; -const HTTP_METHODS: Completion[] = [ - { label: "GET", type: "keyword", apply: "GET /" }, - { label: "POST", type: "keyword", apply: "POST /" }, - { label: "PUT", type: "keyword", apply: "PUT /" }, - { label: "PATCH", type: "keyword", apply: "PATCH /" }, - { label: "DELETE", type: "keyword", apply: "DELETE /" }, -]; +const HTTP_METHODS: Completion[] = ["GET", "POST", "PUT", "PATCH", "DELETE"].map( + (method) => ({ + label: method, + type: "keyword" as const, + apply: (view: EditorView, _c: Completion, from: number, to: number) => { + const line = view.state.doc.lineAt(from); + const afterTo = line.text.slice(to - line.from); + // Skip whitespace after the method word to avoid double spaces + const wsMatch = afterTo.match(/^(\s*)/); + const actualTo = to + (wsMatch?.[1]?.length ?? 0); + const rest = line.text.slice(actualTo - line.from); + const insert = rest.startsWith("/") ? `${method} ` : `${method} /`; + view.dispatch({ + changes: { from, to: actualTo, insert }, + selection: { anchor: from + insert.length }, + }); + }, + }), +); function httpCompletionSource( context: CompletionContext, diff --git a/packages/react-components/src/components/code-editor/index.tsx b/packages/react-components/src/components/code-editor/index.tsx index 25967ecb..b03bbe55 100644 --- a/packages/react-components/src/components/code-editor/index.tsx +++ b/packages/react-components/src/components/code-editor/index.tsx @@ -5,7 +5,6 @@ import { closeBrackets, closeBracketsKeymap, completionKeymap, - completionStatus, } from "@codemirror/autocomplete"; import { defaultKeymap, history, historyKeymap } from "@codemirror/commands"; import { json, jsonParseLinter } from "@codemirror/lang-json"; @@ -69,6 +68,7 @@ import * as React from "react"; import { flushSync } from "react-dom"; import { createRoot } from "react-dom/client"; +import { vim } from "@replit/codemirror-vim"; import { ComplexTypeIcon, ResourceIcon, @@ -77,6 +77,7 @@ import { } from "../../icons"; import { buildFhirCompletionExtension, + type ExpandValueSet, fhirDiagnosticsField, type GetStructureDefinitions, } from "./fhir-completion"; @@ -101,33 +102,89 @@ const setIssueLinesEffect = StateEffect.define(); let errorTooltipEl: HTMLDivElement | null = null; +function formatErrorTypeTitle(code: string): string { + return code + .split("-") + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(" "); +} + +function renderErrorCard(msg: string): HTMLElement { + const card = document.createElement("div"); + Object.assign(card.style, { + backgroundColor: "var(--color-bg-primary)", + border: "1px solid var(--color-border-primary)", + borderRadius: "var(--radius-md)", + padding: "6px 10px", + boxShadow: "0 2px 6px rgba(0, 0, 0, 0.08)", + }); + const newlineIdx = msg.indexOf("\n"); + if (newlineIdx !== -1) { + const title = msg.slice(0, newlineIdx); + const body = msg.slice(newlineIdx + 1); + + const titleEl = document.createElement("div"); + titleEl.textContent = formatErrorTypeTitle(title); + Object.assign(titleEl.style, { fontWeight: "600" }); + + const hr = document.createElement("div"); + Object.assign(hr.style, { + borderTop: "1px solid var(--color-border-primary)", + margin: "4px 0", + }); + + const bodyEl = document.createElement("div"); + bodyEl.textContent = body; + Object.assign(bodyEl.style, { whiteSpace: "pre-wrap" }); + + card.append(titleEl, hr, bodyEl); + } else { + card.textContent = msg; + card.style.whiteSpace = "pre-wrap"; + } + return card; +} + function showErrorTooltip(message: string, x: number, y: number) { hideErrorTooltip(); const tooltip = document.createElement("div"); - tooltip.textContent = message; Object.assign(tooltip.style, { position: "fixed", - backgroundColor: "var(--color-bg-primary)", - border: "1px solid var(--color-border-primary)", - borderRadius: "var(--radius-md)", - padding: "6px 10px", fontSize: "12px", lineHeight: "1.4", color: "var(--color-text-error-primary)", fontFamily: "var(--font-family-sans)", - boxShadow: "0 4px 12px rgba(0, 0, 0, 0.1)", zIndex: "1000", pointerEvents: "none", maxWidth: "400px", - whiteSpace: "pre-wrap", + display: "flex", + flexDirection: "column", + gap: "6px", }); + + const parts = message.split("\n\x00\n"); + for (const part of parts) { + tooltip.append(renderErrorCard(part ?? "")); + } + document.body.appendChild(tooltip); errorTooltipEl = tooltip; - const tooltipHeight = tooltip.getBoundingClientRect().height; + const tooltipRect = tooltip.getBoundingClientRect(); + let top = y - tooltipRect.height - 8; + // If tooltip goes above viewport, show below cursor instead + if (top < 4) { + top = y + 20; + } + // If it still goes below viewport, clamp to bottom + if (top + tooltipRect.height > window.innerHeight - 4) { + top = window.innerHeight - tooltipRect.height - 4; + } + // Final clamp to top + if (top < 4) top = 4; tooltip.style.left = `${x}px`; - tooltip.style.top = `${y - tooltipHeight - 8}px`; + tooltip.style.top = `${top}px`; } function hideErrorTooltip() { @@ -979,12 +1036,14 @@ type CodeEditorProps = { sql?: SqlConfig; getStructureDefinitions?: GetStructureDefinitions; resourceTypeHint?: string; + expandValueSet?: ExpandValueSet; getUrlSuggestions?: GetUrlSuggestions; + vimMode?: boolean; }; export type CodeEditorView = EditorView; -export type { GetStructureDefinitions } from "./fhir-completion"; +export type { ExpandValueSet, GetStructureDefinitions } from "./fhir-completion"; export type { GetUrlSuggestions } from "./http"; export type { SqlConfig, @@ -1009,7 +1068,9 @@ export function CodeEditor({ sql, getStructureDefinitions, resourceTypeHint, + expandValueSet, getUrlSuggestions, + vimMode = false, }: CodeEditorProps) { const domRef = React.useRef(null); const [view, setView] = React.useState(null); @@ -1035,6 +1096,7 @@ export function CodeEditor({ const additionalExtensionsCompartment = React.useRef(new Compartment()); const sqlCompletionCompartment = React.useRef(new Compartment()); const fhirCompletionCompartment = React.useRef(new Compartment()); + const vimCompartment = React.useRef(new Compartment()); const [sqlFunctions, setSqlFunctions] = React.useState< string[] | undefined >(); @@ -1050,6 +1112,7 @@ export function CodeEditor({ state: EditorState.create({ doc: initialValue.current, extensions: [ + vimCompartment.current.of(vimMode ? vim() : []), EditorView.contentAttributes.of({ "data-gramm": "false" }), readOnlyCompartment.current.of(EditorState.readOnly.of(false)), ...(enableLineNumbers ? [lineNumbers()] : []), @@ -1083,6 +1146,7 @@ export function CodeEditor({ autocompletion({ icons: false, maxRenderedOptions: 1000, + defaultKeymap: false, addToOptions: [{ render: renderCompletionIcon, position: 20 }], optionClass: (_completion) => "!px-2 !py-1 rounded-md aria-selected:!bg-bg-quaternary aria-selected:!text-text-primary hover:!bg-bg-secondary flex items-center gap-2", @@ -1103,21 +1167,17 @@ export function CodeEditor({ key: "Tab", run: acceptCompletion, }, - { - key: "Enter", - run: (v) => completionStatus(v.state) === "active", - }, ]), ), themeCompartment.current.of(baseTheme), completionTheme, keymap.of([ ...closeBracketsKeymap, + ...completionKeymap.filter((b) => b.key !== "Enter"), ...defaultKeymap, ...searchKeymap, ...historyKeymap, ...foldKeymap, - ...completionKeymap, ...lintKeymap, ]), issueLinesField, @@ -1183,7 +1243,7 @@ export function CodeEditor({ if (getStructureDefinitions) { safeDispatch({ effects: fhirCompletionCompartment.current.reconfigure( - buildFhirCompletionExtension(getStructureDefinitions, resourceTypeHint), + buildFhirCompletionExtension(getStructureDefinitions, resourceTypeHint, expandValueSet), ), }); } else { @@ -1191,7 +1251,7 @@ export function CodeEditor({ effects: fhirCompletionCompartment.current.reconfigure([]), }); } - }, [view, getStructureDefinitions, resourceTypeHint, safeDispatch]); + }, [view, getStructureDefinitions, resourceTypeHint, expandValueSet, safeDispatch]); React.useEffect(() => { if (viewCallback && view) { @@ -1287,6 +1347,17 @@ export function CodeEditor({ }); }, [isReadOnlyTheme, view, safeDispatch]); + React.useEffect(() => { + if (view === null) { + return; + } + safeDispatch({ + effects: [ + vimCompartment.current.reconfigure(vimMode ? vim() : []), + ], + }); + }, [vimMode, view, safeDispatch]); + React.useEffect(() => { if (view === null) { return; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f4fe14b8..9bcefee2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -253,6 +253,9 @@ importers: '@radix-ui/react-tooltip': specifier: ^1.2.8 version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@replit/codemirror-vim': + specifier: ^6.3.0 + version: 6.3.0(@codemirror/commands@6.10.3)(@codemirror/language@6.12.2)(@codemirror/search@6.6.0)(@codemirror/state@6.6.0)(@codemirror/view@6.40.0) '@tanstack/react-table': specifier: ^8.21.3 version: 8.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -1869,6 +1872,15 @@ packages: react-redux: optional: true + '@replit/codemirror-vim@6.3.0': + resolution: {integrity: sha512-aTx931ULAMuJx6xLf7KQDOL7CxD+Sa05FktTDrtLaSy53uj01ll3Zf17JdKsriER248oS55GBzg0CfCTjEneAQ==} + peerDependencies: + '@codemirror/commands': 6.x.x + '@codemirror/language': 6.x.x + '@codemirror/search': 6.x.x + '@codemirror/state': 6.x.x + '@codemirror/view': 6.x.x + '@rolldown/binding-android-arm64@1.0.0-rc.9': resolution: {integrity: sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5721,6 +5733,14 @@ snapshots: react: 19.2.4 react-redux: 9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1) + '@replit/codemirror-vim@6.3.0(@codemirror/commands@6.10.3)(@codemirror/language@6.12.2)(@codemirror/search@6.6.0)(@codemirror/state@6.6.0)(@codemirror/view@6.40.0)': + dependencies: + '@codemirror/commands': 6.10.3 + '@codemirror/language': 6.12.2 + '@codemirror/search': 6.6.0 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.40.0 + '@rolldown/binding-android-arm64@1.0.0-rc.9': optional: true From 39cb047db4218ab8f624fd33dbdf19e0e8a2c609 Mon Sep 17 00:00:00 2001 From: Panthevm Date: Thu, 19 Mar 2026 21:56:38 +0300 Subject: [PATCH 11/55] Update CM --- .../components/code-editor/fhir-completion.ts | 682 ++++++++++-------- .../src/components/code-editor/index.tsx | 39 +- 2 files changed, 438 insertions(+), 283 deletions(-) diff --git a/packages/react-components/src/components/code-editor/fhir-completion.ts b/packages/react-components/src/components/code-editor/fhir-completion.ts index 86698ce3..5b3242c7 100644 --- a/packages/react-components/src/components/code-editor/fhir-completion.ts +++ b/packages/react-components/src/components/code-editor/fhir-completion.ts @@ -509,6 +509,7 @@ const FHIR_NUMBER_TYPES = new Set([ type SnippetKind = | "array-complex" | "array-primitive" + | "array-extension" | "object" | "string" | "number" @@ -522,8 +523,8 @@ function snippetKind(element: FhirElement): SnippetKind { if (element.contentReference) return isArray ? "array-complex" : "object"; return "bare"; } - // Extension arrays get special treatment — show extension URL picker - if (typeCode === "Extension" && isArray) return "array-primitive"; + // Extension arrays get special snippet with {"url": ""} + if (typeCode === "Extension" && isArray) return "array-extension"; if (isArray) return isPrimitiveType(typeCode) ? "array-primitive" : "array-complex"; if (FHIR_NUMBER_TYPES.has(typeCode)) return "number"; @@ -546,6 +547,10 @@ function buildSnippet( cursorOffset: text.indexOf(innerInner) + innerInner.length, }; } + case "array-extension": { + const text = `"${name}": [\n${inner}{\n${innerInner}"url": ""\n${inner}}\n${indent}]`; + return { text, cursorOffset: text.lastIndexOf('""') + 1 }; + } case "array-primitive": { const text = `"${name}": [\n${inner}\n${indent}]`; return { text, cursorOffset: text.indexOf(inner + "\n") + inner.length }; @@ -609,44 +614,6 @@ function analyzeExtensionSD(sd: StructureDefinition): ExtensionInfo | null { }; } -function extensionValueFieldName(typeCode: string): string { - return `value${typeCode.charAt(0).toUpperCase()}${typeCode.slice(1)}`; -} - -function buildExtensionSnippet( - extInfo: ExtensionInfo, - indent: string, -): { text: string; cursorOffset: number } { - const inner = indent + " "; - - if (extInfo.isNested) { - const innerInner = inner + " "; - const text = `{\n${inner}"url": "${extInfo.url}",\n${inner}"extension": [\n${innerInner}\n${inner}]\n${indent}}`; - return { text, cursorOffset: text.indexOf(innerInner + "\n" + inner) + innerInner.length }; - } - - if (extInfo.valueTypes.length === 1) { - const vField = extensionValueFieldName(extInfo.valueTypes[0]!); - const typeCode = extInfo.valueTypes[0]!; - if (FHIR_STRING_TYPES.has(typeCode) || typeCode === "code") { - const text = `{\n${inner}"url": "${extInfo.url}",\n${inner}"${vField}": ""\n${indent}}`; - return { text, cursorOffset: text.lastIndexOf('""') + 1 }; - } - if (FHIR_NUMBER_TYPES.has(typeCode)) { - const text = `{\n${inner}"url": "${extInfo.url}",\n${inner}"${vField}": \n${indent}}`; - return { text, cursorOffset: text.lastIndexOf(": ") + 2 }; - } - // Complex type (Coding, CodeableConcept, etc.) - const innerInner = inner + " "; - const text = `{\n${inner}"url": "${extInfo.url}",\n${inner}"${vField}": {\n${innerInner}\n${inner}}\n${indent}}`; - return { text, cursorOffset: text.indexOf(innerInner + "\n" + inner) + innerInner.length }; - } - - // Multiple types or no types — just url, user picks value field - const text = `{\n${inner}"url": "${extInfo.url}",\n${inner}\n${indent}}`; - return { text, cursorOffset: text.indexOf(inner + "\n" + indent) + inner.length }; -} - function toCompletion(element: FhirElement): Completion { const name = fieldName(element); const types = element.type?.map((t) => t.code).join(" | ") ?? ""; @@ -689,7 +656,7 @@ function toCompletion(element: FhirElement): Completion { }); // Trigger value autocomplete after inserting a snippet with cursor in value position - if (kind === "string" || kind === "array-primitive") { + if (kind === "string" || kind === "array-primitive" || kind === "array-extension") { setTimeout(() => startCompletion(view), 0); } }, @@ -1041,8 +1008,265 @@ export function fhirCompletionSource( } } + // Extension URL value completion — "url": "|" inside extension object + if (valueKey === "url") { + const bodyStart = doc.indexOf("\n\n"); + const jsonStart = bodyStart !== -1 ? bodyStart + 2 : 0; + const jsonBody = doc.slice(jsonStart); + const posInBody = pos - jsonStart; + const path = getJsonPathAtCursor(jsonBody, posInBody); + const lastSeg = path[path.length - 1]; + if (lastSeg === "extension" || lastSeg === "modifierExtension") { + const rtMatch = doc.match(/"resourceType"\s*:\s*"([^"]+)"/); + const resourceType = rtMatch?.[1] ?? resourceTypeHint; + // Determine if nested by scanning backwards for parent extension URL + // Find the "extension": [ that contains our cursor, then check if + // the object containing that array has a "url" field + // Find the { opening current extension object by finding "url" key position + // then scanning backwards from there (avoids string-tracking issues) + const urlKeyPos = doc.lastIndexOf('"url"', pos); + let scanEnd = urlKeyPos !== -1 ? urlKeyPos : pos; + // From "url" position, find the opening { + for (let i = scanEnd - 1; i >= 0; i--) { + const c = doc[i]; + if (c === "{") { scanEnd = i; break; } + if (c === "}" || c === "]" || c === "[") break; + } + const textBefore = doc.slice(0, scanEnd); + let parentExtUrl: string | null = null; + let depth = 0; + let inStr = false; + let esc = false; + let foundExtArray = false; + for (let i = textBefore.length - 1; i >= 0; i--) { + const ch = textBefore[i]; + if (esc) { esc = false; continue; } + if (ch === "\\") { esc = true; continue; } + if (ch === '"') { inStr = !inStr; continue; } + if (inStr) continue; + if (ch === "}" || ch === "]") { depth++; } + else if (ch === "[") { + if (depth === 0) { foundExtArray = true; continue; } + depth--; + } else if (ch === "{") { + if (depth === 0 && foundExtArray) { + // Found the parent object of the extension array + // Check if it has "url": "..." inside + const objText = textBefore.slice(i); + const urlMatch = objText.match(/^\{[\s\S]*?"url"\s*:\s*"([^"]+)"/); + if (urlMatch?.[1]?.includes("/")) { + parentExtUrl = urlMatch[1]; + } + break; + } + if (depth === 0) break; + depth--; + } + } + if (parentExtUrl) { + const parentSD = await getCachedSD(parentExtUrl, getSDs); + if (parentSD) { + const info = analyzeExtensionSD(parentSD); + if (info?.slices.length) { + const word = context.matchBefore(/[\w.:/-]*/); + const filter = word?.text.toLowerCase() ?? ""; + const matching = filter + ? info.slices.filter((s) => s.fixedUri.toLowerCase().includes(filter) || (s.short?.toLowerCase().includes(filter) ?? false)) + : info.slices; + const options: Completion[] = matching.map((slice) => { + // Find value type for this slice + const sliceElements = parentSD.differential?.element ?? []; + let sliceValueTypes: string[] = []; + let inSl = false; + for (const el of sliceElements) { + if (el.path === "Extension.extension" && el.sliceName === slice.sliceName) { inSl = true; continue; } + if (inSl && el.path === "Extension.extension.value[x]") { sliceValueTypes = el.type?.map((t) => t.code) ?? []; break; } + if (inSl && el.path === "Extension.extension" && el.sliceName) break; + } + return { + label: slice.fixedUri, + ...(slice.short ? { info: slice.short } : {}), + type: "text", + apply: (view: EditorView, _c: Completion, from: number, to: number) => { + const d = view.state.doc.toString(); + let actualTo = to; + if (actualTo < d.length && d[actualTo] === '"') actualTo++; + view.dispatch({ + changes: { from, to: actualTo, insert: `${slice.fixedUri}"` }, + selection: { anchor: from + slice.fixedUri.length + 1 }, + }); + if (sliceValueTypes.length === 1) { + setTimeout(() => { + const cp = view.state.selection.main.head; + const cd = view.state.doc.toString(); + const af = cd.slice(cp); + if (!/^\s*\n\s*\}/.test(af)) return; + const lo = view.state.doc.lineAt(cp); + const ind = lo.text.match(/^(\s*)/)?.[1] ?? ""; + const tc = sliceValueTypes[0]!; + const vf = `value${tc.charAt(0).toUpperCase()}${tc.slice(1)}`; + let ins: string; + let cOff: number; + if (FHIR_STRING_TYPES.has(tc) || tc === "code") { + ins = `,\n${ind}"${vf}": ""`; + cOff = ins.length - 1; + } else if (FHIR_NUMBER_TYPES.has(tc)) { + ins = `,\n${ind}"${vf}": `; + cOff = ins.length; + } else { + const inner = ind + " "; + ins = `,\n${ind}"${vf}": {\n${inner}\n${ind}}`; + cOff = ins.indexOf(inner + "\n" + ind) + inner.length; + } + view.dispatch({ + changes: { from: cp, insert: ins }, + selection: { anchor: cp + cOff }, + }); + setTimeout(() => startCompletion(view), 0); + }, 10); + } + }, + }; + }); + if (options.length > 0) return { from: word?.from ?? pos, options, filter: false }; + } + } + } else if (resourceType) { + // Top-level or nested-in-type: resolve context + const contextTypes: string[] = [resourceType, "DomainResource", "Resource", "Element"]; + const extIdx = path.lastIndexOf("extension"); + if (extIdx > 0) { + let currentRT = resourceType; + for (let i = 0; i < extIdx; i++) { + const seg = path[i]; + if (!seg) break; + const elements = await resolveElements([], currentRT, getSDs); + const el = elements.find((e) => fieldName(e) === seg); + if (el?.type?.[0]?.code && !isPrimitiveType(el.type[0].code)) currentRT = el.type[0].code; + else break; + } + if (currentRT !== resourceType) { + contextTypes.length = 0; + contextTypes.push(currentRT, "Element", `${resourceType}.${path.slice(0, extIdx).join(".")}`); + } + } + // Profile extensions + const profileExtUrls: string[] = []; + if (contextTypes.includes(resourceType)) { + for (const pUrl of getJsonProfileUrls(doc)) { + const profileSD = await getCachedSD(pUrl, getSDs); + if (!profileSD?.differential?.element) continue; + for (const el of profileSD.differential.element) { + for (const t of el.type ?? []) { + if (t.code === "Extension") { + for (const p of t.profile ?? []) { + const clean = p.includes("|") ? p.slice(0, p.indexOf("|")) : p; + if (!profileExtUrls.includes(clean)) profileExtUrls.push(clean); + } + } + } + } + } + } + const bareWord = context.matchBefore(/[\w.:/-]*/); + const filter = bareWord?.text ?? ""; + const searchParams: StructureDefinitionSearchParams = { type: "Extension", derivation: "constraint", _elements: "url,context", _count: "500" }; + if (filter) searchParams._ilike = filter; + const results = await getCachedSDList(searchParams, getSDs); + // Build context hierarchy for boost scoring + const containerType = contextTypes[0]; // e.g. "Address" or "Patient" + const fhirPath = contextTypes.find((c) => c.includes(".")); // e.g. "Patient.address" + const contextExts = results.filter((sd) => sd.context?.some((c) => c.type === "element" && contextTypes.includes(c.expression))); + const seen = new Set(); + const allExts: { url: string; boost: number }[] = []; + if (contextTypes.includes(resourceType)) { + for (const u of profileExtUrls) { if (!seen.has(u)) { seen.add(u); allExts.push({ url: u, boost: 20 }); } } + } + for (const sd of contextExts) { + const u = sd.url ?? sd.type; + if (seen.has(u)) continue; + seen.add(u); + const ctxExprs = sd.context?.filter((c) => c.type === "element").map((c) => c.expression) ?? []; + let boost = 0; + // Exact FHIR path match (e.g. "Patient.address") — highest + if (fhirPath && ctxExprs.includes(fhirPath)) boost = 15; + // Exact container type match (e.g. "Address") + else if (containerType && ctxExprs.includes(containerType)) boost = 10; + // Resource type match (e.g. "Patient") + else if (ctxExprs.includes(resourceType)) boost = 5; + // DomainResource/Resource + else if (ctxExprs.some((e) => e === "DomainResource" || e === "Resource")) boost = 2; + // Element (generic) + else if (ctxExprs.includes("Element")) boost = 1; + allExts.push({ url: u, boost }); + } + const lf = filter.toLowerCase(); + const filtered = (lf ? allExts.filter((e) => e.url.toLowerCase().includes(lf)) : allExts) + .sort((a, b) => b.boost - a.boost); + if (filtered.length > 0) { + const options: Completion[] = filtered.map((ext) => ({ + label: ext.url, type: "text", boost: ext.boost, + apply: (view: EditorView, _c: Completion, applyFrom: number, applyTo: number) => { + const d = view.state.doc.toString(); + let actualTo = applyTo; + if (actualTo < d.length && d[actualTo] === '"') actualTo++; + view.dispatch({ + changes: { from: applyFrom, to: actualTo, insert: `${ext.url}"` }, + selection: { anchor: applyFrom + ext.url.length + 1 }, + }); + // After inserting URL, fetch SD and append value/extension field + setTimeout(async () => { + const fullSD = await getCachedSD(ext.url, getSDs); + if (!fullSD) return; + const extInfo = analyzeExtensionSD(fullSD); + if (!extInfo) return; + const cursorPos = view.state.selection.main.head; + const curDoc = view.state.doc.toString(); + const after = curDoc.slice(cursorPos); + if (!/^\s*\n\s*\}/.test(after)) return; + const lineObj = view.state.doc.lineAt(cursorPos); + const ind = lineObj.text.match(/^(\s*)/)?.[1] ?? ""; + let ins: string; + let cOff: number; + if (extInfo.isNested) { + const inner = ind + " "; + const innerInner = inner + " "; + ins = `,\n${ind}"extension": [\n${inner}{\n${innerInner}"url": ""\n${inner}}\n${ind}]`; + cOff = ins.lastIndexOf('""') + 1; + } else if (extInfo.valueTypes.length === 1) { + const tc = extInfo.valueTypes[0]!; + const vf = `value${tc.charAt(0).toUpperCase()}${tc.slice(1)}`; + if (FHIR_STRING_TYPES.has(tc) || tc === "code") { + ins = `,\n${ind}"${vf}": ""`; + cOff = ins.length - 1; + } else if (FHIR_NUMBER_TYPES.has(tc)) { + ins = `,\n${ind}"${vf}": `; + cOff = ins.length; + } else { + const inner = ind + " "; + ins = `,\n${ind}"${vf}": {\n${inner}\n${ind}}`; + cOff = ins.indexOf(inner + "\n" + ind) + inner.length; + } + } else { + return; + } + view.dispatch({ + changes: { from: cursorPos, insert: ins }, + selection: { anchor: cursorPos + cOff }, + }); + setTimeout(() => startCompletion(view), 0); + }, 10); + }, + })); + return { from: bareWord?.from ?? pos, options, filter: false }; + } + } + return null; + } + } + // Terminology binding value completion - if (valueKey && valueKey !== "resourceType" && valueKey !== "reference" && expandValueSet) { + if (valueKey && valueKey !== "resourceType" && valueKey !== "reference" && valueKey !== "url" && expandValueSet) { const path = getJsonPathAtCursor(doc, pos); const rtMatch = doc.match(/"resourceType"\s*:\s*"([^"]+)"/); const resourceType = rtMatch?.[1] ?? resourceTypeHint; @@ -1155,247 +1379,28 @@ export function fhirCompletionSource( } } - // Extension array completion — offer extension URLs with full snippets - { - const textBefore = doc.slice(0, pos); - const extArrayMatch = textBefore.match(/"(?:extension|modifierExtension)"\s*:\s*\[\s*(?:\{[\s\S]*?\}\s*,?\s*)*[\w.:/-]*$/s); - if (extArrayMatch) { - const rtMatch = doc.match(/"resourceType"\s*:\s*"([^"]+)"/); - const resourceType = rtMatch?.[1] ?? resourceTypeHint; - { - // Also check if we're inside a nested extension (parent has url) - const parentUrlMatch = textBefore.match(/"url"\s*:\s*"([^"]+)"[\s\S]*?"extension"\s*:\s*\[\s*(?:\{[\s\S]*?\}\s*,?\s*)*[\w.:/-]*$/s); - const parentExtUrl = parentUrlMatch?.[1]; - - if (parentExtUrl) { - // Nested: offer slice URLs from parent extension - const parentSD = await getCachedSD(parentExtUrl, getSDs); - if (parentSD) { - const parentInfo = analyzeExtensionSD(parentSD); - if (parentInfo?.slices.length) { - const line = state.doc.lineAt(pos); - const indent = line.text.match(/^(\s*)/)?.[1] ?? ""; - const bareWord = context.matchBefore(/[\w.:/-]*/); - const filter = bareWord?.text.toLowerCase() ?? ""; - const matchingSlices = filter - ? parentInfo.slices.filter((s) => s.fixedUri.toLowerCase().includes(filter) || (s.short?.toLowerCase().includes(filter) ?? false)) - : parentInfo.slices; - const options: Completion[] = matchingSlices.map((slice) => { - // Find value type for this slice - const sliceElements = parentSD.differential?.element ?? []; - let sliceValueTypes: string[] = []; - let inSlice = false; - for (const el of sliceElements) { - if (el.path === "Extension.extension" && el.sliceName === slice.sliceName) { - inSlice = true; - continue; - } - if (inSlice && el.path === "Extension.extension.value[x]") { - sliceValueTypes = el.type?.map((t) => t.code) ?? []; - break; - } - if (inSlice && el.path === "Extension.extension" && el.sliceName) { - break; // next slice - } - } - const sliceInfo: ExtensionInfo = { - url: slice.fixedUri, - name: slice.short, - isNested: false, - valueTypes: sliceValueTypes, - slices: [], - }; - const { text: snippet, cursorOffset } = buildExtensionSnippet(sliceInfo, indent); - return { - label: slice.fixedUri, - ...(slice.short ? { info: slice.short } : {}), - type: "text", - apply: (view: EditorView, _c: Completion, applyFrom: number, applyTo: number) => { - view.dispatch({ - changes: { from: applyFrom, to: applyTo, insert: snippet }, - selection: { anchor: applyFrom + cursorOffset }, - }); - setTimeout(() => startCompletion(view), 0); - }, - }; - }); - if (options.length > 0) { - return { from: bareWord?.from ?? pos, options, filter: false }; - } - } - } - } else { - // Collect extension URLs from profile SD (priority) - const profileUrls = getJsonProfileUrls(doc); - const profileExtUrls: string[] = []; - for (const pUrl of profileUrls) { - const profileSD = await getCachedSD(pUrl, getSDs); - if (!profileSD?.differential?.element) continue; - for (const el of profileSD.differential.element) { - if (el.type?.[0]?.code !== "Extension") continue; - for (const t of el.type ?? []) { - for (const p of t.profile ?? []) { - // Strip version from profile URL - const clean = p.includes("|") ? p.slice(0, p.indexOf("|")) : p; - if (!profileExtUrls.includes(clean)) profileExtUrls.push(clean); - } - } - } - } - - // Determine container type for context filtering - // e.g. inside address[].extension → context = [Address, Element] - // Root extension → context = [Patient, DomainResource, Resource, Element] - const bodyStart = doc.indexOf("\n\n"); - const jsonStart = bodyStart !== -1 ? bodyStart + 2 : 0; - const jsonBody = doc.slice(jsonStart); - const posInBody = pos - jsonStart; - const fullPath = getJsonPathAtCursor(jsonBody, posInBody); - - let containerType: string | null = null; - if (fullPath.length > 0 && resourceType) { - let currentRT = resourceType; - for (const seg of fullPath) { - const elements = await resolveElements([], currentRT, getSDs); - const el = elements.find((e) => fieldName(e) === seg); - if (el?.type?.[0]?.code) { - currentRT = el.type[0].code; - } else { - break; - } - } - if (currentRT !== resourceType) { - containerType = currentRT; - } - } - - // Build FHIR path for context matching (e.g. "Patient.address") - const fhirPath = containerType && resourceType - ? `${resourceType}.${fullPath.join(".")}` - : null; - const contextMatchers: string[] | null = containerType - ? [containerType, "Element", ...(fhirPath ? [fhirPath] : [])] - : resourceType - ? [resourceType, "DomainResource", "Resource", "Element"] - : null; - const bareWord = context.matchBefore(/[\w.:/-]*/); - const filter = bareWord?.text ?? ""; - const searchParams: StructureDefinitionSearchParams = { - type: "Extension", - derivation: "constraint", - _elements: "url,context", - _count: "500", - }; - if (filter) searchParams._ilike = filter; - const results = await getCachedSDList(searchParams, getSDs); - const contextExts = contextMatchers - ? results.filter((sd) => - sd.context?.some((c) => c.type === "element" && contextMatchers.includes(c.expression)), - ) - : results; - - // Merge: profile extensions first (only at root level), then context-matched - const seen = new Set(); - const allUrls: { url: string; name?: string | undefined; boost: number }[] = []; - if (!containerType) { - for (const u of profileExtUrls) { - if (seen.has(u)) continue; - seen.add(u); - allUrls.push({ url: u, boost: 10 }); - } - } - for (const sd of contextExts) { - const u = sd.url ?? sd.type; - if (seen.has(u)) continue; - seen.add(u); - // Boost extensions whose context matches the container type specifically - const isSpecific = containerType && sd.context?.some( - (c) => c.type === "element" && (c.expression === containerType || c.expression === fhirPath), - ); - allUrls.push({ url: u, name: sd.name, boost: isSpecific ? 5 : 0 }); - } - - const lowerFilter = filter.toLowerCase(); - const filtered = lowerFilter - ? allUrls.filter((e) => e.url.toLowerCase().includes(lowerFilter) || (e.name?.toLowerCase().includes(lowerFilter) ?? false)) - : allUrls; - if (filtered.length > 0) { - const line = state.doc.lineAt(pos); - const indent = line.text.match(/^(\s*)/)?.[1] ?? ""; - const options: Completion[] = filtered.map((ext) => ({ - label: ext.url, - ...(ext.name ? { info: ext.name } : {}), - type: "text", - boost: ext.boost, - apply: (view: EditorView, _c: Completion, applyFrom: number, applyTo: number) => { - getCachedSD(ext.url, getSDs).then((fullSD) => { - const info = fullSD ? analyzeExtensionSD(fullSD) : null; - const snippet = info - ? buildExtensionSnippet(info, indent) - : { text: `{\n${indent}"url": "${ext.url}"\n${indent.slice(2)}}`, cursorOffset: 0 }; - view.dispatch({ - changes: { from: applyFrom, to: applyTo, insert: snippet.text }, - selection: { anchor: applyFrom + snippet.cursorOffset }, - }); - setTimeout(() => startCompletion(view), 0); - }); - }, - })); - const from = bareWord?.from ?? pos; - return { from, options, filter: false }; - } - } - } - } - } - - // Extension object field completion — offer valueXxx fields inside {"url": "...", |} + // Don't offer property completions inside arrays + // Scan backwards to find nearest unmatched [ or { { - const textBefore = doc.slice(0, pos); - // Check if inside an object that has a "url" key (extension object) - const urlInObjMatch = textBefore.match(/"url"\s*:\s*"([^"]+)"[^{}]*$/s); - if (urlInObjMatch && (beforeCursor === "" || beforeCursor === '"' || /^"?[\w]*$/.test(beforeCursor))) { - const extUrl = urlInObjMatch[1]!; - const sd = await getCachedSD(extUrl, getSDs); - if (sd) { - const info = analyzeExtensionSD(sd); - if (info && info.valueTypes.length > 1) { - // Multiple value types — offer valueXxx fields - const options: Completion[] = info.valueTypes.map((typeCode) => { - const vField = extensionValueFieldName(typeCode); - const vKind = FHIR_STRING_TYPES.has(typeCode) || typeCode === "code" ? "string" - : FHIR_NUMBER_TYPES.has(typeCode) ? "number" - : "object"; - return { - label: vField, - detail: typeCode, - type: "property", - apply: (view: EditorView, _c: Completion, applyFrom: number, applyTo: number) => { - const d = view.state.doc.toString(); - let actualFrom = applyFrom; - let actualTo = applyTo; - if (actualFrom > 0 && d[actualFrom - 1] === '"') actualFrom--; - if (actualTo < d.length && d[actualTo] === '"') actualTo++; - const lineObj = view.state.doc.lineAt(actualFrom); - const indentStr = lineObj.text.match(/^(\s*)/)?.[1] ?? ""; - const { text, cursorOffset } = buildSnippet(vField, vKind as SnippetKind, indentStr); - view.dispatch({ - changes: { from: actualFrom, to: actualTo, insert: text }, - selection: { anchor: actualFrom + cursorOffset }, - }); - if (vKind === "string") { - setTimeout(() => startCompletion(view), 0); - } - }, - }; - }); - const word = context.matchBefore(/"?\w*/); - let from = word?.from ?? pos; - if (from < doc.length && doc[from] === '"') from++; - return { from, options, validFor: /^\w*$/ }; - } - } + const bodyStart = doc.indexOf("\n\n"); + const jsonStart = bodyStart !== -1 ? bodyStart + 2 : 0; + const jsonBody = doc.slice(jsonStart); + const posInBody = pos - jsonStart; + let depth = 0; + let inStr = false; + let escaped = false; + let insideArray = false; + for (let i = posInBody - 1; i >= 0; i--) { + const ch = jsonBody[i]; + if (escaped) { escaped = false; continue; } + if (ch === "\\") { escaped = true; continue; } + if (ch === '"') { inStr = !inStr; continue; } + if (inStr) continue; + if (ch === "}" || ch === "]") { depth++; } + else if (ch === "{") { if (depth === 0) { insideArray = false; break; } depth--; } + else if (ch === "[") { if (depth === 0) { insideArray = true; break; } depth--; } } + if (insideArray) return null; } // Property name position — with or without quotes @@ -1610,8 +1615,117 @@ export function yamlFhirCompletionSource( } } + // Extension URL value completion in YAML — url: "|" inside extension + if (valueKey === "url") { + const path = getYamlPathAtCursor(doc, pos); + const lastSeg = path[path.length - 1]; + if (lastSeg === "extension" || lastSeg === "modifierExtension") { + const resourceType = getYamlResourceType(doc) ?? resourceTypeHint; + // Check for nested extension — find parent url in YAML + let parentExtUrl: string | null = null; + const urlMatches = [...doc.slice(0, pos).matchAll(/url:\s*['"]?([^\s'"]+)['"]?/g)]; + // If path has multiple extension segments, find parent + const extCount = path.filter((p) => p === "extension" || p === "modifierExtension").length; + if (extCount >= 2) { + for (let i = urlMatches.length - 1; i >= 0; i--) { + const u = urlMatches[i]![1]!; + if (u.includes("/")) { parentExtUrl = u; break; } + } + } + if (parentExtUrl) { + const parentSD = await getCachedSD(parentExtUrl, getSDs); + if (parentSD) { + const info = analyzeExtensionSD(parentSD); + if (info?.slices.length) { + const word = context.matchBefore(/[\w.:/-]*/); + const filter = word?.text.toLowerCase() ?? ""; + const matching = filter + ? info.slices.filter((s) => s.fixedUri.toLowerCase().includes(filter) || (s.short?.toLowerCase().includes(filter) ?? false)) + : info.slices; + const options: Completion[] = matching.map((slice) => ({ + label: slice.fixedUri, + ...(slice.short ? { info: slice.short } : {}), + type: "text", + })); + if (options.length > 0) return { from: word?.from ?? pos, options, filter: false }; + } + } + } else if (resourceType) { + const contextTypes: string[] = [resourceType, "DomainResource", "Resource", "Element"]; + // Resolve container type from path + const extIdx = path.lastIndexOf("extension"); + if (extIdx > 0) { + let currentRT = resourceType; + for (let i = 0; i < extIdx; i++) { + const seg = path[i]; + if (!seg) break; + const elements = await resolveElements([], currentRT, getSDs); + const el = elements.find((e) => fieldName(e) === seg); + if (el?.type?.[0]?.code && !isPrimitiveType(el.type[0].code)) currentRT = el.type[0].code; + else break; + } + if (currentRT !== resourceType) { + contextTypes.length = 0; + contextTypes.push(currentRT, "Element", `${resourceType}.${path.slice(0, extIdx).join(".")}`); + } + } + // Profile extensions + const profileExtUrls: string[] = []; + if (contextTypes.includes(resourceType)) { + for (const pUrl of getYamlProfileUrls(doc)) { + const profileSD = await getCachedSD(pUrl, getSDs); + if (!profileSD?.differential?.element) continue; + for (const el of profileSD.differential.element) { + for (const t of el.type ?? []) { + if (t.code === "Extension") { + for (const p of t.profile ?? []) { + const clean = p.includes("|") ? p.slice(0, p.indexOf("|")) : p; + if (!profileExtUrls.includes(clean)) profileExtUrls.push(clean); + } + } + } + } + } + } + const bareWord = context.matchBefore(/[\w.:/-]*/); + const filter = bareWord?.text ?? ""; + const searchParams: StructureDefinitionSearchParams = { type: "Extension", derivation: "constraint", _elements: "url,context", _count: "500" }; + if (filter) searchParams._ilike = filter; + const results = await getCachedSDList(searchParams, getSDs); + const containerType = contextTypes[0]; + const fhirPath = contextTypes.find((c) => c.includes(".")); + const contextExts = results.filter((sd) => sd.context?.some((c) => c.type === "element" && contextTypes.includes(c.expression))); + const seen = new Set(); + const allExts: { url: string; boost: number }[] = []; + if (contextTypes.includes(resourceType)) { + for (const u of profileExtUrls) { if (!seen.has(u)) { seen.add(u); allExts.push({ url: u, boost: 20 }); } } + } + for (const sd of contextExts) { + const u = sd.url ?? sd.type; + if (seen.has(u)) continue; + seen.add(u); + const ctxExprs = sd.context?.filter((c) => c.type === "element").map((c) => c.expression) ?? []; + let boost = 0; + if (fhirPath && ctxExprs.includes(fhirPath)) boost = 15; + else if (containerType && ctxExprs.includes(containerType)) boost = 10; + else if (ctxExprs.includes(resourceType)) boost = 5; + else if (ctxExprs.some((e) => e === "DomainResource" || e === "Resource")) boost = 2; + else if (ctxExprs.includes("Element")) boost = 1; + allExts.push({ url: u, boost }); + } + const lf = filter.toLowerCase(); + const sorted = (lf ? allExts.filter((e) => e.url.toLowerCase().includes(lf)) : allExts).sort((a, b) => b.boost - a.boost); + if (sorted.length > 0) { + const options: Completion[] = sorted.map((ext) => ({ label: ext.url, type: "text", boost: ext.boost })); + return { from: bareWord?.from ?? pos, options, filter: false }; + } + } + return null; + } + } + // Terminology binding value completion - if (valueKey && valueKey !== "resourceType" && valueKey !== "reference" && expandValueSet) { + if (valueKey && valueKey !== "resourceType" && valueKey !== "reference" && valueKey !== "url" && expandValueSet) { const path = getYamlPathAtCursor(doc, pos); const resourceType = getYamlResourceType(doc) ?? resourceTypeHint; if (resourceType) { @@ -2323,6 +2437,10 @@ export function buildFhirCompletionExtension( const autoTrigger = EditorView.updateListener.of((update) => { if (!update.docChanged) return; if (completionStatus(update.view.state)) return; + // Ignore bulk replacements (e.g. tab switch, currentValue update) + let changeSize = 0; + update.changes.iterChanges((_fA, _tA, _fB, _tB, ins) => { changeSize += ins.length; }); + if (changeSize > 50) return; const { state } = update.view; const pos = state.selection.main.head; const doc = state.doc.toString(); diff --git a/packages/react-components/src/components/code-editor/index.tsx b/packages/react-components/src/components/code-editor/index.tsx index b03bbe55..914ff697 100644 --- a/packages/react-components/src/components/code-editor/index.tsx +++ b/packages/react-components/src/components/code-editor/index.tsx @@ -5,6 +5,8 @@ import { closeBrackets, closeBracketsKeymap, completionKeymap, + completionStatus, + moveCompletionSelection, } from "@codemirror/autocomplete"; import { defaultKeymap, history, historyKeymap } from "@codemirror/commands"; import { json, jsonParseLinter } from "@codemirror/lang-json"; @@ -1012,6 +1014,18 @@ function jsonAutoExpandBraces(): Extension { const indent = line.text.match(/^(\s*)/)?.[1] ?? ""; const inner = `${indent} `; + // Check if { is inside an extension array — insert {"url": ""} snippet + const docText = tr.startState.doc.toString(); + const textBefore = docText.slice(0, braceFrom); + const isInExtArray = /"(?:extension|modifierExtension)"\s*:\s*\[\s*(?:\{[\s\S]*?\}\s*,?\s*)*$/s.test(textBefore); + if (isInExtArray) { + const insert = `{\n${inner}"url": ""\n${indent}}`; + return { + changes: { from: braceFrom, to: braceTo, insert }, + selection: { anchor: braceFrom + insert.lastIndexOf('""') + 1 }, + }; + } + return { changes: { from: braceFrom, to: braceTo, insert: `{\n${inner}\n${indent}}` }, selection: { anchor: braceFrom + 2 + inner.length }, @@ -1165,7 +1179,30 @@ export function CodeEditor({ keymap.of([ { key: "Tab", - run: acceptCompletion, + run: (v) => { + if (completionStatus(v.state) === "active") { + return moveCompletionSelection(true)(v); + } + return false; + }, + }, + { + key: "Shift-Tab", + run: (v) => { + if (completionStatus(v.state) === "active") { + return moveCompletionSelection(false)(v); + } + return false; + }, + }, + { + key: "Enter", + run: (v) => { + if (completionStatus(v.state) === "active") { + return acceptCompletion(v); + } + return false; + }, }, ]), ), From c467bf62a2fdcd6761e1076c91930caaf343b267 Mon Sep 17 00:00:00 2001 From: Panthevm Date: Fri, 20 Mar 2026 20:01:53 +0300 Subject: [PATCH 12/55] Refactor FHIR autocomplete --- .../code-editor/fhir-autocomplete.test.ts | 955 +++++++ .../code-editor/fhir-autocomplete.ts | 2347 ++++++++++++++++ .../components/code-editor/fhir-completion.ts | 2466 ----------------- .../src/components/code-editor/index.tsx | 4 +- .../components/code-editor/json-ast.test.ts | 222 ++ .../src/components/code-editor/json-ast.ts | 587 ++++ packages/react-components/tsconfig.app.json | 2 +- 7 files changed, 4114 insertions(+), 2469 deletions(-) create mode 100644 packages/react-components/src/components/code-editor/fhir-autocomplete.test.ts create mode 100644 packages/react-components/src/components/code-editor/fhir-autocomplete.ts delete mode 100644 packages/react-components/src/components/code-editor/fhir-completion.ts create mode 100644 packages/react-components/src/components/code-editor/json-ast.test.ts create mode 100644 packages/react-components/src/components/code-editor/json-ast.ts diff --git a/packages/react-components/src/components/code-editor/fhir-autocomplete.test.ts b/packages/react-components/src/components/code-editor/fhir-autocomplete.test.ts new file mode 100644 index 00000000..703a228a --- /dev/null +++ b/packages/react-components/src/components/code-editor/fhir-autocomplete.test.ts @@ -0,0 +1,955 @@ +import { CompletionContext } from "@codemirror/autocomplete"; +import { json } from "@codemirror/lang-json"; +import { EditorState } from "@codemirror/state"; +import { beforeEach, describe, expect, it } from "vitest"; +import { + buildParameterSnippet, + type ExpandValueSet, + type GetStructureDefinitions, + jsonCompletionSource, +} from "./fhir-autocomplete"; + +// ── Minimal mock SDs ─────────────────────────────────────────────────── + +const PATIENT_SD = { + type: "Patient", + url: "http://hl7.org/fhir/StructureDefinition/Patient", + baseDefinition: "http://hl7.org/fhir/StructureDefinition/DomainResource", + differential: { + element: [ + { path: "Patient", min: 0, max: "*" }, + { path: "Patient.name", min: 0, max: "*", type: [{ code: "HumanName" }] }, + { + path: "Patient.gender", + min: 0, + max: "1", + type: [{ code: "code" }], + binding: { + valueSet: "http://hl7.org/fhir/ValueSet/administrative-gender", + strength: "required", + }, + }, + { path: "Patient.birthDate", min: 0, max: "1", type: [{ code: "date" }] }, + { path: "Patient.active", min: 0, max: "1", type: [{ code: "boolean" }] }, + { + path: "Patient.managingOrganization", + min: 0, + max: "1", + type: [ + { + code: "Reference", + targetProfile: [ + "http://hl7.org/fhir/StructureDefinition/Organization", + ], + }, + ], + }, + { + path: "Patient.contained", + min: 0, + max: "*", + type: [{ code: "Resource" }], + }, + { + path: "Patient.meta", + min: 0, + max: "1", + type: [{ code: "Meta" }], + }, + ], + }, +}; + +const OBSERVATION_SD = { + type: "Observation", + url: "http://hl7.org/fhir/StructureDefinition/Observation", + baseDefinition: "http://hl7.org/fhir/StructureDefinition/DomainResource", + differential: { + element: [ + { path: "Observation", min: 0, max: "*" }, + { + path: "Observation.status", + min: 1, + max: "1", + type: [{ code: "code" }], + }, + { + path: "Observation.code", + min: 1, + max: "1", + type: [{ code: "CodeableConcept" }], + }, + { + path: "Observation.subject", + min: 0, + max: "1", + type: [ + { + code: "Reference", + targetProfile: [ + "http://hl7.org/fhir/StructureDefinition/Patient", + "http://hl7.org/fhir/StructureDefinition/Group", + ], + }, + ], + }, + ], + }, +}; + +const DOMAIN_RESOURCE_SD = { + type: "DomainResource", + url: "http://hl7.org/fhir/StructureDefinition/DomainResource", + baseDefinition: "http://hl7.org/fhir/StructureDefinition/Resource", + differential: { + element: [ + { path: "DomainResource", min: 0, max: "*" }, + { + path: "DomainResource.text", + min: 0, + max: "1", + type: [{ code: "Narrative" }], + }, + { + path: "DomainResource.contained", + min: 0, + max: "*", + type: [{ code: "Resource" }], + }, + { + path: "DomainResource.extension", + min: 0, + max: "*", + type: [{ code: "Extension" }], + }, + { + path: "DomainResource.modifierExtension", + min: 0, + max: "*", + type: [{ code: "Extension" }], + }, + ], + }, +}; + +const RESOURCE_SD = { + type: "Resource", + url: "http://hl7.org/fhir/StructureDefinition/Resource", + differential: { + element: [ + { path: "Resource", min: 0, max: "*" }, + { path: "Resource.id", min: 0, max: "1", type: [{ code: "id" }] }, + { path: "Resource.meta", min: 0, max: "1", type: [{ code: "Meta" }] }, + ], + }, +}; + +const HUMAN_NAME_SD = { + type: "HumanName", + url: "http://hl7.org/fhir/StructureDefinition/HumanName", + differential: { + element: [ + { path: "HumanName", min: 0, max: "*" }, + { + path: "HumanName.family", + min: 0, + max: "1", + type: [{ code: "string" }], + }, + { path: "HumanName.given", min: 0, max: "*", type: [{ code: "string" }] }, + ], + }, +}; + +const REFERENCE_SD = { + type: "Reference", + url: "http://hl7.org/fhir/StructureDefinition/Reference", + differential: { + element: [ + { path: "Reference", min: 0, max: "*" }, + { + path: "Reference.reference", + min: 0, + max: "1", + type: [{ code: "string" }], + }, + { + path: "Reference.display", + min: 0, + max: "1", + type: [{ code: "string" }], + }, + ], + }, +}; + +const META_SD = { + type: "Meta", + url: "http://hl7.org/fhir/StructureDefinition/Meta", + differential: { + element: [ + { path: "Meta", min: 0, max: "*" }, + { + path: "Meta.profile", + min: 0, + max: "*", + type: [ + { + code: "canonical", + targetProfile: [ + "http://hl7.org/fhir/StructureDefinition/StructureDefinition", + ], + }, + ], + }, + ], + }, +}; + +const BUNDLE_SD = { + type: "Bundle", + url: "http://hl7.org/fhir/StructureDefinition/Bundle", + baseDefinition: "http://hl7.org/fhir/StructureDefinition/Resource", + differential: { + element: [ + { path: "Bundle", min: 0, max: "*" }, + { path: "Bundle.type", min: 1, max: "1", type: [{ code: "code" }] }, + { path: "Bundle.entry", min: 0, max: "*", type: [{ code: "BackboneElement" }] }, + { path: "Bundle.entry.resource", min: 0, max: "1", type: [{ code: "Resource" }] }, + ], + }, +}; + +const PARAMETERS_SD = { + type: "Parameters", + url: "http://hl7.org/fhir/StructureDefinition/Parameters", + baseDefinition: "http://hl7.org/fhir/StructureDefinition/Resource", + differential: { + element: [ + { path: "Parameters", min: 0, max: "*" }, + { + path: "Parameters.parameter", + min: 0, + max: "*", + type: [{ code: "BackboneElement" }], + }, + { + path: "Parameters.parameter.name", + min: 1, + max: "1", + type: [{ code: "string" }], + }, + { + path: "Parameters.parameter.value[x]", + min: 0, + max: "1", + type: [ + { code: "string" }, + { code: "boolean" }, + { code: "integer" }, + { code: "code" }, + { code: "Reference" }, + { code: "CodeableConcept" }, + ], + }, + { + path: "Parameters.parameter.resource", + min: 0, + max: "1", + type: [{ code: "Resource" }], + }, + { + path: "Parameters.parameter.part", + min: 0, + max: "*", + contentReference: "#Parameters.parameter", + }, + ], + }, +}; + +const INSTALL_PARAMS_PROFILE = { + type: "Parameters", + url: "http://health-samurai.io/fhir/core/StructureDefinition/fhir-package-install-parameters", + baseDefinition: "http://hl7.org/fhir/StructureDefinition/Parameters", + differential: { + element: [ + { path: "Parameters.parameter", min: 1 }, + { + path: "Parameters.parameter", + sliceName: "package", + min: 1, + max: "*", + }, + { + path: "Parameters.parameter.name", + fixedString: "package", + }, + { + path: "Parameters.parameter", + sliceName: "registry", + min: 0, + max: "1", + }, + { + path: "Parameters.parameter.name", + fixedString: "registry", + }, + ], + }, +}; + +const TYPED_PARAMS_PROFILE = { + type: "Parameters", + url: "http://example.com/StructureDefinition/typed-params", + baseDefinition: "http://hl7.org/fhir/StructureDefinition/Parameters", + differential: { + element: [ + { + path: "Parameters.parameter", + sliceName: "count", + min: 1, + max: "1", + }, + { + path: "Parameters.parameter.name", + fixedString: "count", + }, + { + path: "Parameters.parameter.value[x]", + type: [{ code: "integer" }], + }, + { + path: "Parameters.parameter", + sliceName: "label", + min: 0, + max: "1", + }, + { + path: "Parameters.parameter.name", + fixedString: "label", + }, + { + path: "Parameters.parameter.value[x]", + type: [{ code: "string" }], + }, + ], + }, +}; + +const TOPIC_DEST_SD = { + type: "AidboxTopicDestination", + url: "http://aidbox.app/StructureDefinition/AidboxTopicDestination", + baseDefinition: "http://hl7.org/fhir/StructureDefinition/Parameters", + differential: { + element: [ + { path: "AidboxTopicDestination", min: 0, max: "*" }, + { + path: "AidboxTopicDestination.kind", + min: 0, + max: "1", + type: [{ code: "string" }], + }, + ], + }, +}; + +const TOPIC_DEST_KAFKA_PROFILE = { + type: "AidboxTopicDestination", + url: "http://aidbox.app/StructureDefinition/aidboxtopicdestination-kafka-best-effort", + baseDefinition: + "http://aidbox.app/StructureDefinition/AidboxTopicDestination", + differential: { + element: [ + { + path: "AidboxTopicDestination.kind", + fixedString: "kafka-best-effort", + }, + { + path: "AidboxTopicDestination.parameter", + sliceName: "kafkaTopic", + min: 1, + max: "1", + }, + { + path: "AidboxTopicDestination.parameter.name", + fixedString: "kafkaTopic", + }, + { + path: "AidboxTopicDestination.parameter.value[x]", + type: [{ code: "string" }], + }, + { + path: "AidboxTopicDestination.parameter", + sliceName: "bootstrapServers", + min: 1, + max: "1", + }, + { + path: "AidboxTopicDestination.parameter.name", + fixedString: "bootstrapServers", + }, + { + path: "AidboxTopicDestination.parameter.value[x]", + type: [{ code: "string" }], + }, + { + path: "AidboxTopicDestination.parameter", + sliceName: "batchSize", + min: 0, + max: "1", + }, + { + path: "AidboxTopicDestination.parameter.name", + fixedString: "batchSize", + }, + { + path: "AidboxTopicDestination.parameter.value[x]", + type: [{ code: "integer" }], + }, + ], + }, +}; + +const RESOURCE_TYPE_LIST = [ + { type: "Patient" }, + { type: "Observation" }, + { type: "Organization" }, + { type: "Bundle" }, + { type: "Parameters" }, + { type: "AidboxTopicDestination" }, +]; + +const ALL_SDS: Record = { + Patient: PATIENT_SD, + Observation: OBSERVATION_SD, + DomainResource: DOMAIN_RESOURCE_SD, + Resource: RESOURCE_SD, + HumanName: HUMAN_NAME_SD, + Reference: REFERENCE_SD, + Meta: META_SD, + Bundle: BUNDLE_SD, + Parameters: PARAMETERS_SD, + "http://hl7.org/fhir/StructureDefinition/Patient": PATIENT_SD, + "http://hl7.org/fhir/StructureDefinition/Observation": OBSERVATION_SD, + "http://hl7.org/fhir/StructureDefinition/DomainResource": DOMAIN_RESOURCE_SD, + "http://hl7.org/fhir/StructureDefinition/Resource": RESOURCE_SD, + "http://hl7.org/fhir/StructureDefinition/HumanName": HUMAN_NAME_SD, + "http://hl7.org/fhir/StructureDefinition/Reference": REFERENCE_SD, + "http://hl7.org/fhir/StructureDefinition/Meta": META_SD, + "http://hl7.org/fhir/StructureDefinition/Bundle": BUNDLE_SD, + "http://hl7.org/fhir/StructureDefinition/Parameters": PARAMETERS_SD, + "http://health-samurai.io/fhir/core/StructureDefinition/fhir-package-install-parameters": + INSTALL_PARAMS_PROFILE, + "http://example.com/StructureDefinition/typed-params": TYPED_PARAMS_PROFILE, + AidboxTopicDestination: TOPIC_DEST_SD, + "http://aidbox.app/StructureDefinition/AidboxTopicDestination": TOPIC_DEST_SD, + "http://aidbox.app/StructureDefinition/aidboxtopicdestination-kafka-best-effort": + TOPIC_DEST_KAFKA_PROFILE, +}; + +// ── Mock getSDs ──────────────────────────────────────────────────────── + +const mockGetSDs: GetStructureDefinitions = async (params) => { + if (params.kind === "resource" && params.derivation === "specialization") { + return RESOURCE_TYPE_LIST as (typeof PATIENT_SD)[]; + } + if (params.url) { + const sd = ALL_SDS[params.url]; + return sd ? [sd] : []; + } + if (params.type && params.derivation === "specialization") { + const sd = ALL_SDS[params.type]; + return sd ? [sd] : []; + } + if (params.type && params["derivation:missing"] === "true") { + const sd = ALL_SDS[params.type]; + return sd ? [sd] : []; + } + if (params.type === "Extension") { + return []; + } + return []; +}; + +const mockExpandValueSet: ExpandValueSet = async (url, _filter) => { + if (url === "http://hl7.org/fhir/ValueSet/administrative-gender") { + return [ + { code: "male", display: "Male" }, + { code: "female", display: "Female" }, + { code: "other", display: "Other" }, + { code: "unknown", display: "Unknown" }, + ]; + } + return []; +}; + +// ── Test helpers ─────────────────────────────────────────────────────── + +function completionAt(doc: string, marker = "|") { + const pos = doc.indexOf(marker); + const text = doc.slice(0, pos) + doc.slice(pos + 1); + const state = EditorState.create({ doc: text, extensions: [json()] }); + const cc = new CompletionContext(state, pos, true); + return { state, cc, pos }; +} + +function labels(result: { options: { label: string }[] } | null): string[] { + return result?.options.map((o) => o.label) ?? []; +} + +// ── Caches persist across tests — clear between describes ────────────── + +// The SD cache is module-level in fhir-autocomplete.ts. +// Since we use consistent mock data, the cache doesn't cause issues. + +// ── Tests ────────────────────────────────────────────────────────────── + +describe("fhir-autocomplete: jsonCompletionSource", () => { + const source = jsonCompletionSource( + mockGetSDs, + undefined, + mockExpandValueSet, + ); + + describe("resourceType value completions", () => { + it("offers resource types in empty resourceType value", async () => { + const { cc } = completionAt('{\n "resourceType": "|\n}'); + const result = await source(cc); + const l = labels(result); + expect(l).toContain("Patient"); + expect(l).toContain("Observation"); + expect(l).toContain("Organization"); + }); + }); + + describe("property completions", () => { + it("offers Patient fields when resourceType is set", async () => { + const { cc } = completionAt('{\n "resourceType": "Patient",\n |\n}'); + const result = await source(cc); + const l = labels(result); + expect(l).toContain("name"); + expect(l).toContain("gender"); + expect(l).toContain("birthDate"); + expect(l).toContain("managingOrganization"); + }); + + it("offers resourceType when no resourceType is set", async () => { + const { cc } = completionAt("{\n |\n}"); + const result = await source(cc); + const l = labels(result); + expect(l).toContain("resourceType"); + }); + + it("offers primitive extensions (_birthDate)", async () => { + const { cc } = completionAt('{\n "resourceType": "Patient",\n |\n}'); + const result = await source(cc); + const l = labels(result); + expect(l).toContain("_birthDate"); + expect(l).toContain("_gender"); + }); + + it("excludes properties already present in object", async () => { + const { cc } = completionAt( + '{\n "resourceType": "Patient",\n "gender": "male",\n |\n}', + ); + const result = await source(cc); + const l = labels(result); + expect(l).toContain("name"); + expect(l).toContain("birthDate"); + expect(l).not.toContain("gender"); + expect(l).not.toContain("resourceType"); + }); + + it("does not offer property completions inside arrays", async () => { + const { cc } = completionAt( + '{\n "resourceType": "Patient",\n "name": [\n |\n ]\n}', + ); + const result = await source(cc); + expect(result).toBe(null); + }); + }); + + describe("property completions with resourceTypeHint", () => { + const hintSource = jsonCompletionSource( + mockGetSDs, + "Patient", + mockExpandValueSet, + ); + + it("uses hint when resourceType is not in document", async () => { + const { cc } = completionAt("{\n |\n}"); + const result = await hintSource(cc); + const l = labels(result); + expect(l).toContain("name"); + expect(l).toContain("gender"); + }); + }); + + describe("terminology binding completions", () => { + it("offers gender codes for Patient.gender", async () => { + const { cc } = completionAt( + '{\n "resourceType": "Patient",\n "gender": "|\n}', + ); + const result = await source(cc); + const l = labels(result); + expect(l).toContain("male"); + expect(l).toContain("female"); + expect(l).toContain("other"); + expect(l).toContain("unknown"); + }); + }); + + describe("boolean value completions", () => { + it("offers true and false for boolean fields", async () => { + const { cc } = completionAt( + '{\n "resourceType": "Patient",\n "active": |\n}', + ); + const result = await source(cc); + const l = labels(result); + expect(l).toContain("true"); + expect(l).toContain("false"); + expect(l).toHaveLength(2); + }); + }); + + describe("reference target completions", () => { + it("offers Organization/ for Patient.managingOrganization.reference", async () => { + const { cc } = completionAt( + '{\n "resourceType": "Patient",\n "managingOrganization": {\n "reference": "|\n }\n}', + ); + const result = await source(cc); + const l = labels(result); + expect(l).toContain("Organization/"); + }); + + it("offers Patient/ and Group/ for Observation.subject.reference", async () => { + const { cc } = completionAt( + '{\n "resourceType": "Observation",\n "subject": {\n "reference": "|\n }\n}', + ); + const result = await source(cc); + const l = labels(result); + expect(l).toContain("Patient/"); + expect(l).toContain("Group/"); + }); + }); + + describe("contained resource completions", () => { + it("offers resourceType inside contained array item", async () => { + const { cc } = completionAt( + '{\n "resourceType": "Patient",\n "contained": [\n {\n |\n }\n ]\n}', + ); + const result = await source(cc); + const l = labels(result); + expect(l).toContain("resourceType"); + }); + + it("offers inner resource fields when contained has resourceType", async () => { + const { cc } = completionAt( + '{\n "resourceType": "Patient",\n "contained": [\n {\n "resourceType": "Observation",\n |\n }\n ]\n}', + ); + const result = await source(cc); + const l = labels(result); + expect(l).toContain("status"); + expect(l).toContain("code"); + expect(l).toContain("subject"); + // Should NOT contain Patient fields + expect(l).not.toContain("gender"); + expect(l).not.toContain("birthDate"); + }); + + it("offers correct reference targets for contained Observation.subject", async () => { + const { cc } = completionAt( + '{\n "resourceType": "Patient",\n "contained": [\n {\n "resourceType": "Observation",\n "subject": {\n "reference": "|\n }\n }\n ]\n}', + ); + const result = await source(cc); + const l = labels(result); + expect(l).toContain("Patient/"); + expect(l).toContain("Group/"); + // Should NOT contain Organization/ (that's from Patient.managingOrganization) + expect(l).not.toContain("Organization/"); + }); + }); + + describe("Bundle.entry.resource completions", () => { + it("offers Observation fields inside entry.resource with explicit Bundle resourceType", async () => { + const { cc } = completionAt( + '{\n "resourceType": "Bundle",\n "entry": [\n {\n "resource": {\n "resourceType": "Observation",\n |\n }\n }\n ]\n}', + ); + const result = await source(cc); + const l = labels(result); + expect(l).toContain("status"); + expect(l).toContain("code"); + expect(l).toContain("subject"); + expect(l).not.toContain("type"); + }); + + it("offers Observation fields when Bundle resourceType comes from hint (URL)", async () => { + const hintSource = jsonCompletionSource(mockGetSDs, "Bundle", mockExpandValueSet); + const { cc } = completionAt( + '{\n "entry": [\n {\n "resource": {\n "resourceType": "Observation",\n |\n }\n }\n ]\n}', + ); + const result = await hintSource(cc); + const l = labels(result); + expect(l).toContain("status"); + expect(l).toContain("code"); + expect(l).toContain("subject"); + expect(l).not.toContain("type"); + }); + }); + + describe("nested object completions", () => { + it("offers HumanName fields inside name array item", async () => { + const { cc } = completionAt( + '{\n "resourceType": "Patient",\n "name": [\n {\n |\n }\n ]\n}', + ); + const result = await source(cc); + const l = labels(result); + expect(l).toContain("family"); + expect(l).toContain("given"); + }); + + it("offers Reference fields inside managingOrganization", async () => { + const { cc } = completionAt( + '{\n "resourceType": "Patient",\n "managingOrganization": {\n |\n }\n}', + ); + const result = await source(cc); + const l = labels(result); + expect(l).toContain("reference"); + expect(l).toContain("display"); + }); + }); + + describe("Parameters completions", () => { + it("offers parameter fields inside parameter array item object", async () => { + const { cc } = completionAt( + '{\n "resourceType": "Parameters",\n "parameter": [\n {\n |\n }\n ]\n}', + ); + const result = await source(cc); + const l = labels(result); + expect(l).toContain("name"); + expect(l).toContain("valueString"); + expect(l).toContain("valueBoolean"); + expect(l).toContain("resource"); + expect(l).toContain("part"); + }); + + it("offers slice names for parameter.name from profile", async () => { + const { cc } = completionAt( + '{\n "resourceType": "Parameters",\n "meta": {\n "profile": ["http://health-samurai.io/fhir/core/StructureDefinition/fhir-package-install-parameters"]\n },\n "parameter": [\n {\n "name": "|\n }\n ]\n}', + ); + const result = await source(cc); + const l = labels(result); + expect(l).toContain("package"); + expect(l).toContain("registry"); + }); + + it("offers parameter snippets in array-item position from profile", async () => { + const { cc } = completionAt( + '{\n "resourceType": "Parameters",\n "meta": {\n "profile": ["http://health-samurai.io/fhir/core/StructureDefinition/fhir-package-install-parameters"]\n },\n "parameter": [\n |\n ]\n}', + ); + const result = await source(cc); + const l = labels(result); + expect(l).toContain("package"); + expect(l).toContain("registry"); + expect(l).toContain("parameter"); + }); + + it("offers generic parameter template without profile", async () => { + const { cc } = completionAt( + '{\n "resourceType": "Parameters",\n "parameter": [\n |\n ]\n}', + ); + const result = await source(cc); + expect(result).not.toBe(null); + const l = labels(result); + expect(l).toContain("parameter"); + }); + + it("offers parameter fields for second array item", async () => { + const { cc } = completionAt( + '{\n "resourceType": "Parameters",\n "parameter": [\n {"name": "a", "valueString": "1"},\n {\n |\n }\n ]\n}', + ); + const result = await source(cc); + const l = labels(result); + expect(l).toContain("name"); + expect(l).toContain("valueString"); + expect(l).not.toContain("parameter"); + }); + + it("offers slice names for second array item name value", async () => { + const { cc } = completionAt( + '{\n "resourceType": "AidboxTopicDestination",\n "meta": {\n "profile": ["http://aidbox.app/StructureDefinition/aidboxtopicdestination-kafka-best-effort"]\n },\n "parameter": [\n {"name": "kafkaTopic", "valueString": "1"},\n {\n "name": "|\n }\n ]\n}', + ); + const result = await source(cc); + const l = labels(result); + expect(l).toContain("bootstrapServers"); + expect(l).toContain("batchSize"); + }); + + it("offers part fields via contentReference", async () => { + const { cc } = completionAt( + '{\n "resourceType": "Parameters",\n "parameter": [\n {\n "name": "result",\n "part": [\n {\n |\n }\n ]\n }\n ]\n}', + ); + const result = await source(cc); + const l = labels(result); + expect(l).toContain("name"); + expect(l).toContain("valueString"); + expect(l).toContain("part"); + }); + + it("offers typed snippet for profile with value[x] constraint", async () => { + const typedSource = jsonCompletionSource(mockGetSDs, undefined, mockExpandValueSet); + const { cc } = completionAt( + '{\n "resourceType": "Parameters",\n "meta": {\n "profile": ["http://example.com/StructureDefinition/typed-params"]\n },\n "parameter": [\n |\n ]\n}', + ); + const result = await typedSource(cc); + const l = labels(result); + expect(l).toContain("count"); + expect(l).toContain("label"); + }); + + it("works for Parameters-derived types (AidboxTopicDestination)", async () => { + const { cc } = completionAt( + '{\n "resourceType": "AidboxTopicDestination",\n "meta": {\n "profile": ["http://aidbox.app/StructureDefinition/aidboxtopicdestination-kafka-best-effort"]\n },\n "parameter": [\n |\n ]\n}', + ); + const result = await source(cc); + const l = labels(result); + expect(l).toContain("kafkaTopic"); + expect(l).toContain("bootstrapServers"); + expect(l).toContain("batchSize"); + }); + + it("offers slice names for derived type parameter.name", async () => { + const { cc } = completionAt( + '{\n "resourceType": "AidboxTopicDestination",\n "meta": {\n "profile": ["http://aidbox.app/StructureDefinition/aidboxtopicdestination-kafka-best-effort"]\n },\n "parameter": [\n {\n "name": "|\n }\n ]\n}', + ); + const result = await source(cc); + const l = labels(result); + expect(l).toContain("kafkaTopic"); + expect(l).toContain("bootstrapServers"); + expect(l).toContain("batchSize"); + }); + + it("offers fixed value for profiled field", async () => { + const { cc } = completionAt( + '{\n "resourceType": "AidboxTopicDestination",\n "meta": {\n "profile": ["http://aidbox.app/StructureDefinition/aidboxtopicdestination-kafka-best-effort"]\n },\n "kind": "|\n}', + ); + const result = await source(cc); + const l = labels(result); + expect(l).toContain("kafka-best-effort"); + }); + + it("generic parameter snippet is offered with lowest boost", async () => { + const doc = + '{\n "resourceType": "Parameters",\n "parameter": [\n |\n ]\n}'; + const { cc } = completionAt(doc); + const result = await source(cc); + const option = result?.options.find((o) => o.label === "parameter"); + expect(option).toBeDefined(); + expect(option!.boost).toBe(-1); + expect(option!.info).toBe("Custom parameter"); + }); + }); + + describe("HTTP mode", () => { + it("offers Patient fields in HTTP mode body", async () => { + const doc = + 'POST /fhir/Patient\nContent-Type: application/json\n\n{\n "resourceType": "Patient",\n |\n}'; + const { cc } = completionAt(doc); + const result = await source(cc); + const l = labels(result); + expect(l).toContain("name"); + expect(l).toContain("gender"); + }); + + it("offers gender codes in HTTP mode body", async () => { + const doc = + 'PUT /fhir/Patient/1\n\n{\n "resourceType": "Patient",\n "gender": "|\n}'; + const { cc } = completionAt(doc); + const result = await source(cc); + const l = labels(result); + expect(l).toContain("male"); + expect(l).toContain("female"); + }); + + it("offers Observation fields inside Bundle entry.resource via hint", async () => { + const hintSource = jsonCompletionSource(mockGetSDs, "Bundle", mockExpandValueSet); + const doc = + 'POST /fhir/Bundle\nContent-Type: application/json\n\n{\n "entry": [\n {\n "resource": {\n "resourceType": "Observation",\n |\n }\n }\n ]\n}'; + const { cc } = completionAt(doc); + const result = await hintSource(cc); + const l = labels(result); + expect(l).toContain("status"); + expect(l).toContain("code"); + expect(l).toContain("subject"); + expect(l).not.toContain("type"); + }); + }); +}); + +describe("buildParameterSnippet", () => { + it("inserts valueString by default when no value types", () => { + const { text, cursorOffset } = buildParameterSnippet("package", [], " "); + expect(text).toContain('"name": "package"'); + expect(text).toContain('"valueString": ""'); + // Cursor should be inside the empty valueString quotes + expect(text[cursorOffset - 1]).toBe('"'); + expect(text[cursorOffset]).toBe('"'); + }); + + it("inserts valueString for string-constrained type", () => { + const { text } = buildParameterSnippet("label", ["string"], " "); + expect(text).toContain('"name": "label"'); + expect(text).toContain('"valueString": ""'); + }); + + it("inserts valueCode for code-constrained type", () => { + const { text } = buildParameterSnippet("status", ["code"], " "); + expect(text).toContain('"name": "status"'); + expect(text).toContain('"valueCode": ""'); + }); + + it("inserts valueInteger for integer-constrained type", () => { + const { text, cursorOffset } = buildParameterSnippet("count", ["integer"], " "); + expect(text).toContain('"name": "count"'); + expect(text).toContain('"valueInteger": '); + expect(text).not.toContain('"valueString"'); + // Cursor should be after ": " + expect(text.slice(cursorOffset - 2, cursorOffset)).toBe(": "); + }); + + it("inserts valueBoolean for boolean-constrained type", () => { + const { text } = buildParameterSnippet("active", ["boolean"], " "); + expect(text).toContain('"name": "active"'); + expect(text).toContain('"valueBoolean": '); + }); + + it("inserts complex object for CodeableConcept type", () => { + const { text } = buildParameterSnippet("code", ["CodeableConcept"], " "); + expect(text).toContain('"name": "code"'); + expect(text).toContain('"valueCodeableConcept": {'); + expect(text).toContain("}"); + }); + + it("uses correct indentation", () => { + const { text } = buildParameterSnippet("test", [], " "); + const lines = text.split("\n"); + // Line 0: { + expect(lines[0]).toBe("{"); + // Line 1: inner indent + "name" + expect(lines[1]).toMatch(/^ "name": "test",$/); + // Line 2: inner indent + "valueString" + expect(lines[2]).toMatch(/^ "valueString": ""$/); + // Line 3: outer indent + } + expect(lines[3]).toBe(" }"); + }); + + it("inserts empty name for generic template with cursor in name", () => { + const { text, cursorOffset } = buildParameterSnippet("", [], " "); + expect(text).toContain('"name": ""'); + expect(text).toContain('"valueString": ""'); + // Cursor should be inside the name quotes (first ""), not valueString + const nameQuoteIdx = text.indexOf('"name": ""') + '"name": "'.length; + expect(cursorOffset).toBe(nameQuoteIdx); + }); +}); diff --git a/packages/react-components/src/components/code-editor/fhir-autocomplete.ts b/packages/react-components/src/components/code-editor/fhir-autocomplete.ts new file mode 100644 index 00000000..6b841c43 --- /dev/null +++ b/packages/react-components/src/components/code-editor/fhir-autocomplete.ts @@ -0,0 +1,2347 @@ +import { + type Completion, + type CompletionContext, + type CompletionResult, + type CompletionSource, + completionStatus, + startCompletion, +} from "@codemirror/autocomplete"; +import { jsonLanguage } from "@codemirror/lang-json"; +import { ensureSyntaxTree, syntaxTree } from "@codemirror/language"; +import { + type Extension, + RangeSet, + StateEffect, + StateField, +} from "@codemirror/state"; +import { + Decoration, + EditorView, + GutterMarker, + gutterLineClass, + ViewPlugin, + type ViewUpdate, +} from "@codemirror/view"; +import { + buildJsonDocumentContext, + type DocumentContext, + findRootJsonObject, + type PropertyInfo, + walkJsonProperties, +} from "./json-ast"; + +// ── Types ────────────────────────────────────────────────────────────── + +interface FhirElementType { + code: string; + profile?: string[]; + targetProfile?: string[]; +} + +interface FhirElement { + path: string; + short?: string; + definition?: string; + min?: number; + max?: string; + type?: FhirElementType[]; + binding?: { valueSet: string; strength: string }; + contentReference?: string; + sliceName?: string; + fixedUri?: string; + fixedString?: string; + fixedCode?: string; +} + +interface StructureDefinition { + type: string; + url?: string; + name?: string; + baseDefinition?: string; + context?: { expression: string; type: string }[]; + differential?: { element: FhirElement[] }; +} + +export interface StructureDefinitionSearchParams { + type?: string; + url?: string; + derivation?: string; + "derivation:missing"?: string; + kind?: string; + _count?: string; + _elements?: string; + _ilike?: string; +} + +export type GetStructureDefinitions = ( + params: StructureDefinitionSearchParams, +) => Promise; + +export type ExpandValueSet = ( + url: string, + filter: string, +) => Promise<{ code: string; display?: string; system?: string }[]>; + +// ── Constants ────────────────────────────────────────────────────────── + +const PRIMITIVE_TYPES = new Set([ + "boolean", + "integer", + "string", + "decimal", + "uri", + "url", + "canonical", + "base64Binary", + "instant", + "date", + "dateTime", + "time", + "code", + "oid", + "id", + "markdown", + "unsignedInt", + "positiveInt", + "uuid", + "xhtml", +]); + +function isPrimitiveType(typeCode: string): boolean { + return ( + PRIMITIVE_TYPES.has(typeCode) || + typeCode.startsWith("http://hl7.org/fhirpath/System.") + ); +} + +const FHIR_STRING_TYPES = new Set([ + "string", + "code", + "uri", + "url", + "canonical", + "id", + "markdown", + "oid", + "uuid", + "base64Binary", + "xhtml", + "http://hl7.org/fhirpath/System.String", +]); + +const FHIR_NUMBER_TYPES = new Set([ + "boolean", + "integer", + "decimal", + "positiveInt", + "unsignedInt", + "http://hl7.org/fhirpath/System.Boolean", + "http://hl7.org/fhirpath/System.Integer", + "http://hl7.org/fhirpath/System.Decimal", +]); + +// ── Cache ────────────────────────────────────────────────────────────── + +const sdCache = new Map(); +const pendingRequests = new Map>(); +const listCache = new Map(); +const pendingListRequests = new Map>(); + +const SD_ELEMENTS = "differential,type,name,baseDefinition,url,context"; + +function cacheKey(params: StructureDefinitionSearchParams): string { + return JSON.stringify(params); +} + +async function getCachedSDList( + params: StructureDefinitionSearchParams, + getSDs: GetStructureDefinitions, +): Promise { + const key = cacheKey(params); + if (listCache.has(key)) return listCache.get(key) ?? []; + + let pending = pendingListRequests.get(key); + if (!pending) { + pending = getSDs(params) + .then((list) => { + listCache.set(key, list); + pendingListRequests.delete(key); + for (const sd of list) { + if (sd.differential?.element) { + sdCache.set(sd.type, sd); + } + } + return list; + }) + .catch(() => { + pendingListRequests.delete(key); + listCache.set(key, []); + return []; + }); + pendingListRequests.set(key, pending); + } + return pending; +} + +async function getCachedSD( + type: string, + getSDs: GetStructureDefinitions, +): Promise { + if (sdCache.has(type)) return sdCache.get(type) ?? null; + + const key = `single:${type}`; + let pending = pendingRequests.get(key); + if (!pending) { + const isUrl = type.includes("/"); + const searchByType = (params: StructureDefinitionSearchParams) => + getSDs(params).then((list) => list[0] ?? null); + + pending = ( + isUrl + ? searchByType({ url: type, _elements: SD_ELEMENTS, _count: "1" }) + : searchByType({ + type, + derivation: "specialization", + _elements: SD_ELEMENTS, + _count: "1", + }).then( + (sd) => + sd ?? + searchByType({ + type, + "derivation:missing": "true", + _elements: SD_ELEMENTS, + _count: "1", + }), + ) + ) + .then((sd) => { + sdCache.set(type, sd); + pendingRequests.delete(key); + return sd; + }) + .catch(() => { + pendingRequests.delete(key); + return null; + }); + pendingRequests.set(key, pending); + } + return pending; +} + +// ── Element helpers ──────────────────────────────────────────────────── + +function fieldName(element: FhirElement): string { + const parts = element.path.split("."); + return (parts[parts.length - 1] ?? "").replace("[x]", ""); +} + +function directChildren( + elements: FhirElement[], + parentPath: string, +): FhirElement[] { + const prefix = `${parentPath}.`; + return elements.filter((el) => { + if (!el.path.startsWith(prefix)) return false; + const rest = el.path.slice(prefix.length); + return !rest.includes("."); + }); +} + +function findElement( + elements: FhirElement[], + parentPath: string, + key: string, +): FhirElement | undefined { + const direct = elements.find((el) => { + if (!el.path.startsWith(`${parentPath}.`)) return false; + const name = fieldName(el); + return name === key || name.toLowerCase() === key.toLowerCase(); + }); + if (direct) return direct; + + for (const el of elements) { + if (!el.path.endsWith("[x]")) continue; + if (!el.path.startsWith(`${parentPath}.`)) continue; + const baseName = fieldName(el); + if (!key.toLowerCase().startsWith(baseName.toLowerCase())) continue; + const typeSuffix = key.slice(baseName.length).toLowerCase(); + const matchedType = el.type?.find( + (t) => t.code.toLowerCase() === typeSuffix, + ); + if (matchedType) { + return { ...el, type: [matchedType] }; + } + } + return undefined; +} + +// ── Resolve completions at path ──────────────────────────────────────── + +async function collectAllElements( + type: string, + getSDs: GetStructureDefinitions, +): Promise<{ elements: FhirElement[]; basePath: string } | null> { + const sd = await getCachedSD(type, getSDs); + if (!sd?.differential?.element) return null; + + const elements = [...sd.differential.element]; + + if (sd.baseDefinition) { + const base = await collectAllElements(sd.baseDefinition, getSDs); + if (base) { + for (const baseEl of base.elements) { + const remappedPath = baseEl.path.replace( + new RegExp(`^${base.basePath}`), + sd.type, + ); + if (!elements.some((e) => e.path === remappedPath)) { + elements.push({ ...baseEl, path: remappedPath }); + } + } + } + } + + return { elements, basePath: sd.type }; +} + +async function resolveElements( + path: string[], + resourceType: string, + getSDs: GetStructureDefinitions, +): Promise { + const result = await collectAllElements(resourceType, getSDs); + if (!result) return []; + + let currentPath = resourceType; + let currentElements = result.elements; + + for (const key of path) { + if (key === "resourceType") return []; + + const el = findElement(currentElements, currentPath, key); + if (!el) return []; + + if (el.contentReference) { + currentPath = el.contentReference.replace(/^#/, ""); + continue; + } + + if (!el.type?.[0]) return []; + const typeCode = el.type[0].code; + + if (typeCode === "BackboneElement") { + currentPath = el.path; + continue; + } + + const typeResult = await collectAllElements(typeCode, getSDs); + if (!typeResult) return []; + currentPath = typeResult.basePath; + currentElements = typeResult.elements; + } + + const children = directChildren(currentElements, currentPath); + const expanded: FhirElement[] = []; + + for (const el of children) { + const isChoiceType = el.path.endsWith("[x]"); + if (isChoiceType && el.type && el.type.length > 0) { + for (const t of el.type) { + expanded.push({ + ...el, + path: el.path.replace( + "[x]", + t.code.charAt(0).toUpperCase() + t.code.slice(1), + ), + type: [t], + }); + } + } else { + expanded.push(el); + } + } + + return expanded; +} + +async function findResourceBoundary( + path: string[], + resourceType: string, + getSDs: GetStructureDefinitions, +): Promise { + if (path.length === 0) return null; + + const result = await collectAllElements(resourceType, getSDs); + if (!result) return null; + + let currentPath = resourceType; + let currentElements = result.elements; + + for (let i = 0; i < path.length; i++) { + const key = path[i]!; + if (key === "resourceType") return null; + + const el = findElement(currentElements, currentPath, key); + if (!el) return null; + + if (el.type?.some((t) => t.code === "Resource")) { + return i; + } + + if (el.contentReference) { + currentPath = el.contentReference.replace(/^#/, ""); + continue; + } + if (!el.type?.[0]) return null; + const typeCode = el.type[0].code; + if (typeCode === "BackboneElement") { + currentPath = el.path; + continue; + } + const typeResult = await collectAllElements(typeCode, getSDs); + if (!typeResult) return null; + currentPath = typeResult.basePath; + currentElements = typeResult.elements; + } + return null; +} + +// ── Snippet & Completion Builders ────────────────────────────────────── + +type SnippetKind = + | "array-complex" + | "array-primitive" + | "array-extension" + | "object" + | "string" + | "number" + | "bare"; + +function snippetKind(element: FhirElement): SnippetKind { + const isArray = element.max === "*"; + const typeCode = element.type?.[0]?.code; + if (!typeCode) { + if (element.contentReference) return isArray ? "array-complex" : "object"; + return "bare"; + } + if (typeCode === "Extension" && isArray) return "array-extension"; + if (isArray) + return isPrimitiveType(typeCode) ? "array-primitive" : "array-complex"; + if (FHIR_NUMBER_TYPES.has(typeCode)) return "number"; + if (isPrimitiveType(typeCode)) return "string"; + return "object"; +} + +function buildSnippet( + name: string, + kind: SnippetKind, + indent: string, +): { text: string; cursorOffset: number } { + const inner = indent + " "; + const innerInner = inner + " "; + switch (kind) { + case "array-complex": { + const text = `"${name}": [\n${inner}{\n${innerInner}\n${inner}}\n${indent}]`; + return { + text, + cursorOffset: text.indexOf(innerInner) + innerInner.length, + }; + } + case "array-extension": { + const text = `"${name}": [\n${inner}{\n${innerInner}"url": ""\n${inner}}\n${indent}]`; + return { text, cursorOffset: text.lastIndexOf('""') + 1 }; + } + case "array-primitive": { + const text = `"${name}": [\n${inner}\n${indent}]`; + return { text, cursorOffset: text.indexOf(inner + "\n") + inner.length }; + } + case "object": { + const text = `"${name}": {\n${inner}\n${indent}}`; + return { text, cursorOffset: text.indexOf(inner + "\n") + inner.length }; + } + case "string": { + const text = `"${name}": ""`; + return { text, cursorOffset: text.length - 1 }; + } + case "number": + case "bare": + default: { + const text = `"${name}": `; + return { text, cursorOffset: text.length }; + } + } +} + +function toCompletion(element: FhirElement): Completion { + const name = fieldName(element); + const types = element.type?.map((t) => t.code).join(" | ") ?? ""; + const kind = snippetKind(element); + + const completion: Completion = { + label: name, + type: "property", + detail: types, + boost: element.min && element.min > 0 ? 2 : 0, + apply: (view, _completion, from, to) => { + const doc = view.state.doc.toString(); + let actualFrom = from; + let actualTo = to; + if (actualFrom > 0 && doc[actualFrom - 1] === '"') actualFrom--; + if (actualTo < doc.length && doc[actualTo] === '"') actualTo++; + + const afterName = doc.slice(actualTo).match(/^\s*:/); + if (afterName) { + const insert = `"${name}"`; + view.dispatch({ + changes: { from: actualFrom, to: actualTo, insert }, + selection: { anchor: actualFrom + insert.length }, + }); + return; + } + + const line = view.state.doc.lineAt(actualFrom); + const lineText = line.text; + const indent = lineText.match(/^(\s*)/)?.[1] ?? ""; + + const { text, cursorOffset } = buildSnippet(name, kind, indent); + + view.dispatch({ + changes: { from: actualFrom, to: actualTo, insert: text }, + selection: { anchor: actualFrom + cursorOffset }, + }); + + if ( + kind === "string" || + kind === "number" || + kind === "array-primitive" || + kind === "array-extension" + ) { + setTimeout(() => startCompletion(view), 0); + } + }, + }; + if (element.short) completion.info = element.short; + return completion; +} + +function toParameterPropertyCompletion(element: FhirElement): Completion { + const name = fieldName(element); + if (name !== "parameter" && name !== "part") return toCompletion(element); + + const types = element.type?.map((t) => t.code).join(" | ") ?? ""; + const completion: Completion = { + label: name, + type: "property", + detail: types, + boost: element.min && element.min > 0 ? 2 : 0, + apply: (view, _completion, from, to) => { + const doc = view.state.doc.toString(); + let actualFrom = from; + let actualTo = to; + if (actualFrom > 0 && doc[actualFrom - 1] === '"') actualFrom--; + if (actualTo < doc.length && doc[actualTo] === '"') actualTo++; + + const afterName = doc.slice(actualTo).match(/^\s*:/); + if (afterName) { + const insert = `"${name}"`; + view.dispatch({ + changes: { from: actualFrom, to: actualTo, insert }, + selection: { anchor: actualFrom + insert.length }, + }); + return; + } + + const line = view.state.doc.lineAt(actualFrom); + const indent = line.text.match(/^(\s*)/)?.[1] ?? ""; + const inner = indent + " "; + const innerInner = inner + " "; + const text = `"${name}": [\n${inner}{\n${innerInner}"name": ""\n${inner}}\n${indent}]`; + view.dispatch({ + changes: { from: actualFrom, to: actualTo, insert: text }, + selection: { anchor: actualFrom + text.lastIndexOf('""') + 1 }, + }); + setTimeout(() => startCompletion(view), 0); + }, + }; + if (element.short) completion.info = element.short; + return completion; +} + +function elementsToCompletions( + elements: FhirElement[], + mapFn: (el: FhirElement) => Completion, +): Completion[] { + const completions: Completion[] = []; + for (const el of elements) { + completions.push(mapFn(el)); + const name = fieldName(el); + const firstTypeCode = el.type?.[0]?.code; + if ( + el.type?.length === 1 && + firstTypeCode && + PRIMITIVE_TYPES.has(firstTypeCode) + ) { + const ext: Completion = { + label: `_${name}`, + type: "property", + detail: "Element", + boost: -1, + }; + ext.info = "Primitive element extension"; + completions.push(ext); + } + } + return completions; +} + +// ── Extension helpers ────────────────────────────────────────────────── + +interface ExtensionInfo { + url: string; + name?: string | undefined; + isNested: boolean; + valueTypes: string[]; + slices: { sliceName: string; fixedUri: string; short?: string | undefined }[]; +} + +function analyzeExtensionSD(sd: StructureDefinition): ExtensionInfo | null { + if (!sd.differential?.element) return null; + const elements = sd.differential.element; + + const valueEl = elements.find((e) => e.path === "Extension.value[x]"); + const isNested = valueEl?.max === "0"; + const valueTypes = isNested ? [] : (valueEl?.type?.map((t) => t.code) ?? []); + + const slices: ExtensionInfo["slices"] = []; + for (const el of elements) { + if (el.path === "Extension.extension" && el.sliceName) { + const sliceName = el.sliceName; + const urlEl = elements.find( + (e) => + e.path === "Extension.extension.url" && + e.fixedUri && + elements.indexOf(e) > elements.indexOf(el), + ); + const fixedUri = urlEl?.fixedUri ?? sliceName; + slices.push({ sliceName, fixedUri, short: el.short }); + } + } + + return { + url: sd.url ?? sd.type, + name: sd.name, + isNested, + valueTypes, + slices, + }; +} + +// ── Parameters slice helpers ──────────────────────────────────────────── + +const parametersTypeCache = new Map(); + +async function isParametersType( + resourceType: string, + getSDs: GetStructureDefinitions, +): Promise { + if (resourceType === "Parameters") return true; + if (parametersTypeCache.has(resourceType)) + return parametersTypeCache.get(resourceType)!; + + const sd = await getCachedSD(resourceType, getSDs); + if (!sd?.baseDefinition) { + parametersTypeCache.set(resourceType, false); + return false; + } + const baseType = sd.baseDefinition.split("/").pop()?.split("|")[0] ?? ""; + const result = await isParametersType(baseType, getSDs); + parametersTypeCache.set(resourceType, result); + return result; +} + +interface ParameterSlice { + sliceName: string; + fixedName: string; + min: number; + max: string; + valueTypes: string[]; + short?: string; +} + +async function getParameterSlices( + profileUrls: string[], + getSDs: GetStructureDefinitions, +): Promise { + const slices: ParameterSlice[] = []; + + for (const profileUrl of profileUrls) { + const sd = await getCachedSD(profileUrl, getSDs); + if (!sd?.differential?.element) continue; + + const elements = sd.differential.element; + // Find the parameter path dynamically: "X.parameter" where X is the profile's type + const paramPath = `${sd.type}.parameter`; + + let current: { + sliceName: string; + min: number; + max: string; + fixedName: string | null; + valueTypes: string[]; + short: string | undefined; + } | null = null; + + const flush = () => { + if (current?.fixedName) { + const s: ParameterSlice = { + sliceName: current.sliceName, + fixedName: current.fixedName, + min: current.min, + max: current.max, + valueTypes: current.valueTypes, + }; + if (current.short != null) s.short = current.short; + slices.push(s); + } + }; + + for (const el of elements) { + if (el.path === paramPath && el.sliceName) { + flush(); + current = { + sliceName: el.sliceName, + min: el.min ?? 0, + max: el.max ?? "*", + fixedName: null, + valueTypes: [], + short: el.short, + }; + continue; + } + + if (!current) continue; + + if (el.path === `${paramPath}.name` && el.fixedString) { + current.fixedName = el.fixedString; + } + if (el.path === `${paramPath}.value[x]` && el.type) { + current.valueTypes = el.type.map((t) => t.code); + } + } + + flush(); + } + + return slices; +} + +// ── Fixed value helpers ──────────────────────────────────────────────── + +async function getFixedValues( + effectivePath: string[], + valueKey: string, + resourceType: string, + profileUrls: string[], + getSDs: GetStructureDefinitions, +): Promise { + for (const profileUrl of profileUrls) { + const sd = await getCachedSD(profileUrl, getSDs); + if (!sd?.differential?.element) continue; + + const fhirPath = `${resourceType}.${[...effectivePath, valueKey].join(".")}`; + for (const el of sd.differential.element) { + if (el.path !== fhirPath) continue; + if (el.fixedString != null) return el.fixedString; + if (el.fixedUri != null) return el.fixedUri; + if (el.fixedCode != null) return el.fixedCode; + } + } + return null; +} + +/** @internal — exported for tests only */ +export function buildParameterSnippet( + name: string, + valueTypes: string[], + indent: string, +): { text: string; cursorOffset: number } { + const inner = indent + " "; + + if (valueTypes.length === 1 && FHIR_STRING_TYPES.has(valueTypes[0]!)) { + const tc = valueTypes[0]!; + const vf = `value${tc.charAt(0).toUpperCase()}${tc.slice(1)}`; + const text = `{\n${inner}"name": "${name}",\n${inner}"${vf}": ""\n${indent}}`; + return { text, cursorOffset: text.lastIndexOf('""') + 1 }; + } + if (valueTypes.length === 1 && FHIR_NUMBER_TYPES.has(valueTypes[0]!)) { + const tc = valueTypes[0]!; + const vf = `value${tc.charAt(0).toUpperCase()}${tc.slice(1)}`; + const text = `{\n${inner}"name": "${name}",\n${inner}"${vf}": \n${indent}}`; + return { + text, + cursorOffset: text.indexOf(`"${vf}": \n`) + `"${vf}": `.length, + }; + } + if (valueTypes.length === 1) { + const tc = valueTypes[0]!; + const vf = `value${tc.charAt(0).toUpperCase()}${tc.slice(1)}`; + const innerInner = inner + " "; + const text = `{\n${inner}"name": "${name}",\n${inner}"${vf}": {\n${innerInner}\n${inner}}\n${indent}}`; + return { + text, + cursorOffset: + text.indexOf(innerInner + "\n" + inner + "}") + innerInner.length, + }; + } + // Default to valueString when no value type constraint + const text = `{\n${inner}"name": "${name}",\n${inner}"valueString": ""\n${indent}}`; + if (name === "") { + // Generic template: cursor in name + return { text, cursorOffset: text.indexOf('""') + 1 }; + } + return { text, cursorOffset: text.lastIndexOf('""') + 1 }; +} + +// ── Binding & Reference Resolution ───────────────────────────────────── + +function buildFhirElementPath( + resourceType: string, + path: string[], + valueKey: string, +): string { + return `${resourceType}.${[...path, valueKey].join(".")}`; +} + +async function findProfileBinding( + profileUrls: string[], + resourceType: string, + path: string[], + valueKey: string, + getSDs: GetStructureDefinitions, +): Promise { + if (profileUrls.length === 0) return null; + + for (const profileUrl of profileUrls) { + const sd = await getCachedSD(profileUrl, getSDs); + if (!sd?.differential?.element) continue; + + const directPath = buildFhirElementPath(resourceType, path, valueKey); + for (const el of sd.differential.element) { + if (el.path === directPath && el.binding?.valueSet) { + return el.binding.valueSet; + } + } + + if (valueKey === "code") { + for (let i = path.length; i > 0; i--) { + const parentFhirPath = buildFhirElementPath( + resourceType, + path.slice(0, i - 1), + path[i - 1]!, + ); + for (const el of sd.differential.element) { + if (el.path === parentFhirPath && el.binding?.valueSet) { + return el.binding.valueSet; + } + } + } + } + } + return null; +} + +async function findExtensionBinding( + doc: string, + pos: number, + getSDs: GetStructureDefinitions, +): Promise { + const textBefore = doc.slice(0, pos); + + const urlMatches = [...textBefore.matchAll(/"url"\s*:\s*"([^"]+)"/g)]; + if (urlMatches.length === 0) return null; + + for (let i = urlMatches.length - 1; i >= 0; i--) { + const extUrl = urlMatches[i]![1]!; + if ( + !extUrl.includes("StructureDefinition/") && + !extUrl.includes("Extension") + ) { + const parentUrlMatches = [ + ...textBefore + .slice(0, urlMatches[i]!.index) + .matchAll(/"url"\s*:\s*"([^"]+)"/g), + ]; + for (let j = parentUrlMatches.length - 1; j >= 0; j--) { + const parentUrl = parentUrlMatches[j]![1]!; + if (!parentUrl.includes("/")) continue; + const parentSD = await getCachedSD(parentUrl, getSDs); + if (!parentSD?.differential?.element) continue; + let inSlice = false; + for (const el of parentSD.differential.element) { + if (el.path === "Extension.extension" && el.sliceName) { + const urlEl = parentSD.differential.element.find( + (e) => + e.path === "Extension.extension.url" && + e.fixedUri && + parentSD.differential!.element.indexOf(e) > + parentSD.differential!.element.indexOf(el), + ); + if ((urlEl?.fixedUri ?? el.sliceName) === extUrl) { + inSlice = true; + continue; + } + if (inSlice) break; + } + if ( + inSlice && + el.path === "Extension.extension.value[x]" && + el.binding?.valueSet + ) { + return el.binding.valueSet; + } + } + if (inSlice) break; + } + continue; + } + const sd = await getCachedSD(extUrl, getSDs); + if (!sd?.differential?.element) continue; + for (const el of sd.differential.element) { + if (el.path === "Extension.value[x]" && el.binding?.valueSet) { + return el.binding.valueSet; + } + } + } + return null; +} + +async function findBindingForValue( + path: string[], + valueKey: string, + resourceType: string, + getSDs: GetStructureDefinitions, + profileUrls: string[] = [], + doc?: string, + pos?: number, +): Promise { + if (doc != null && pos != null) { + const inExtension = path.some( + (p) => p === "extension" || p === "modifierExtension", + ); + if (inExtension) { + const extBinding = await findExtensionBinding(doc, pos, getSDs); + if (extBinding) return extBinding; + } + } + + const profileBinding = await findProfileBinding( + profileUrls, + resourceType, + path, + valueKey, + getSDs, + ); + if (profileBinding) return profileBinding; + + const elements = await resolveElements(path, resourceType, getSDs); + for (const el of elements) { + if (fieldName(el) === valueKey && el.binding?.valueSet) { + return el.binding.valueSet; + } + } + + if (valueKey === "code") { + for (let i = path.length; i > 0; i--) { + const parentElements = await resolveElements( + path.slice(0, i - 1), + resourceType, + getSDs, + ); + for (const el of parentElements) { + if (fieldName(el) === path[i - 1] && el.binding?.valueSet) { + return el.binding.valueSet; + } + } + } + } + + return null; +} + +async function findCanonicalTargetType( + path: string[], + arrayKey: string, + resourceType: string, + getSDs: GetStructureDefinitions, +): Promise { + const elements = await resolveElements(path, resourceType, getSDs); + for (const el of elements) { + if (fieldName(el) !== arrayKey) continue; + if (el.max !== "*") continue; + const t = el.type?.[0]; + if (t?.code !== "canonical" || !t.targetProfile?.length) continue; + return t.targetProfile[0]?.split("/").pop() ?? null; + } + return null; +} + +async function resolveReferenceTargets( + path: string[], + resourceType: string, + getSDs: GetStructureDefinitions, +): Promise { + const result = await collectAllElements(resourceType, getSDs); + if (!result) return null; + + let currentPath = resourceType; + let currentElements = result.elements; + + for (let i = 0; i < path.length - 1; i++) { + const key = path[i]!; + if (key === "resourceType") return null; + + const el = findElement(currentElements, currentPath, key); + if (!el) return null; + + if (el.contentReference) { + currentPath = el.contentReference.replace(/^#/, ""); + continue; + } + if (!el.type?.[0]) return null; + const typeCode = el.type[0].code; + if (typeCode === "BackboneElement") { + currentPath = el.path; + continue; + } + const typeResult = await collectAllElements(typeCode, getSDs); + if (!typeResult) return null; + currentPath = typeResult.basePath; + currentElements = typeResult.elements; + } + + const lastKey = path[path.length - 1]; + if (!lastKey) return null; + + const el = findElement(currentElements, currentPath, lastKey); + if (!el?.type) return null; + + const targets: string[] = []; + for (const t of el.type) { + if (t.code === "Reference" && t.targetProfile) { + for (const profile of t.targetProfile) { + const rt = profile.split("/").pop(); + if (rt) targets.push(rt); + } + } + } + return targets.length > 0 ? targets : null; +} + +// ── Unified completion handler ───────────────────────────────────────── + +async function fhirComplete( + ctx: DocumentContext, + getSDs: GetStructureDefinitions, + resourceTypeHint: string | undefined, + expandValueSet: ExpandValueSet | undefined, + completionContext: CompletionContext, +): Promise { + const { pos, doc } = ctx; + + // 1. Resolve context: resourceType, effectivePath, profileUrls + // Search from root (outermost scope) inward to find the root resourceType. + // If not found in doc, fall back to resourceTypeHint (derived from URL). + let resourceType: string | undefined; + let hasExplicitResourceType = false; + let rtScope = ctx.getScope(0); + for (let level = ctx.fullPath.length; level >= 0; level--) { + const s = ctx.getScope(level); + const rt = s.getString("resourceType"); + if (rt) { + resourceType = rt; + hasExplicitResourceType = true; + rtScope = s; + break; + } + } + // If no resourceType found in any scope, use hint. + // If found only in an inner scope (not the root), still prefer hint for + // boundary detection — the inner RT will be picked up via getScope later. + if (!resourceType) { + resourceType = resourceTypeHint; + } else if (resourceTypeHint && ctx.fullPath.length > 0) { + // Check if the found RT is actually from an inner scope, not the root. + // If the root scope has no RT but hint is available, use hint as the + // outer RT so that findResourceBoundary can resolve the path correctly. + const rootRT = ctx.getScope(ctx.fullPath.length).getString("resourceType"); + if (!rootRT) { + resourceType = resourceTypeHint; + hasExplicitResourceType = false; + } + } + + let effectivePath = ctx.fullPath; + let profileUrls: string[] = []; + + // Detect Resource-typed boundary (e.g. contained, Bundle.entry.resource) + if (resourceType && effectivePath.length > 0) { + const boundaryIdx = await findResourceBoundary( + effectivePath, + resourceType, + getSDs, + ); + if (boundaryIdx !== null) { + const innerPath = effectivePath.slice(boundaryIdx + 1); + const innerScope = ctx.getScope(innerPath.length); + const innerRT = innerScope.getString("resourceType"); + effectivePath = innerPath; + if (innerRT) { + resourceType = innerRT; + hasExplicitResourceType = true; + } else { + resourceType = "DomainResource"; + hasExplicitResourceType = false; + } + profileUrls = innerScope.getStringArray("meta", "profile"); + } else { + profileUrls = rtScope.getStringArray("meta", "profile"); + } + } else { + profileUrls = rtScope.getStringArray("meta", "profile"); + } + + // 2. Handle cursor position kinds + const cp = ctx.cursorPosition; + + if (cp.kind === "value") { + return handleValueCompletion( + cp.key, + effectivePath, + resourceType, + hasExplicitResourceType, + profileUrls, + doc, + pos, + getSDs, + expandValueSet, + completionContext, + ctx, + ); + } + + if (cp.kind === "array-item") { + return handleArrayItemCompletion( + cp.parentKey, + effectivePath, + resourceType, + doc, + pos, + getSDs, + completionContext, + profileUrls, + ); + } + + if (cp.kind === "property") { + // Don't offer property completions inside arrays + if (ctx.isInsideArray()) return null; + + return handlePropertyCompletion( + effectivePath, + resourceType, + hasExplicitResourceType, + doc, + pos, + getSDs, + completionContext, + ctx, + ); + } + + return null; +} + +// ── Value completion ─────────────────────────────────────────────────── + +async function handleValueCompletion( + valueKey: string, + effectivePath: string[], + resourceType: string | undefined, + _hasExplicitResourceType: boolean, + profileUrls: string[], + doc: string, + pos: number, + getSDs: GetStructureDefinitions, + expandValueSet: ExpandValueSet | undefined, + completionContext: CompletionContext, + ctx: DocumentContext, +): Promise { + // resourceType value + if (valueKey === "resourceType") { + const sds = await getCachedSDList( + { + derivation: "specialization", + kind: "resource", + _elements: "type", + _count: "500", + }, + getSDs, + ); + const options: Completion[] = sds.map((sd) => ({ + label: sd.type, + type: "type", + apply: (view: EditorView, _c: Completion, from: number, to: number) => { + const d = view.state.doc.toString(); + let actualTo = to; + if (actualTo < d.length && d[actualTo] === '"') actualTo++; + view.dispatch({ + changes: { from, to: actualTo, insert: `${sd.type}"` }, + selection: { anchor: from + sd.type.length + 1 }, + }); + }, + })); + if (options.length === 0) return null; + const word = completionContext.matchBefore(/[\w]*/); + return { from: word?.from ?? pos, options, validFor: /^\w*$/ }; + } + + // Parameters.parameter.name → slice names from profile + if ( + valueKey === "name" && + resourceType && + effectivePath.length > 0 && + effectivePath[effectivePath.length - 1] === "parameter" && + (await isParametersType(resourceType, getSDs)) + ) { + const slices = await getParameterSlices(profileUrls, getSDs); + if (slices.length > 0) { + const options: Completion[] = slices.map((slice) => ({ + label: slice.fixedName, + type: "text", + detail: slice.min > 0 ? "required" : "optional", + boost: slice.min > 0 ? 2 : 0, + ...(slice.short ? { info: slice.short } : {}), + apply: ( + view: EditorView, + _c: Completion, + from: number, + to: number, + ) => { + const d = view.state.doc.toString(); + let actualTo = to; + if (actualTo < d.length && d[actualTo] === '"') actualTo++; + view.dispatch({ + changes: { + from, + to: actualTo, + insert: `${slice.fixedName}"`, + }, + selection: { anchor: from + slice.fixedName.length + 1 }, + }); + // Auto-insert value[x] field based on slice type + const vTypes = slice.valueTypes; + if (vTypes.length === 0) vTypes.push("string"); + if (vTypes.length === 1) { + setTimeout(() => { + const cp = view.state.selection.main.head; + const cd = view.state.doc.toString(); + const after = cd.slice(cp); + if (!/^\s*\n\s*\}/.test(after)) return; + const lineObj = view.state.doc.lineAt(cp); + const ind = lineObj.text.match(/^(\s*)/)?.[1] ?? ""; + const tc = vTypes[0]!; + const vf = `value${tc.charAt(0).toUpperCase()}${tc.slice(1)}`; + let ins: string; + let cOff: number; + if (FHIR_STRING_TYPES.has(tc)) { + ins = `,\n${ind}"${vf}": ""`; + cOff = ins.length - 1; + } else if (FHIR_NUMBER_TYPES.has(tc)) { + ins = `,\n${ind}"${vf}": `; + cOff = ins.length; + } else { + const inner = ind + " "; + ins = `,\n${ind}"${vf}": {\n${inner}\n${ind}}`; + cOff = + ins.indexOf(inner + "\n" + ind + "}") + + inner.length; + } + view.dispatch({ + changes: { from: cp, insert: ins }, + selection: { anchor: cp + cOff }, + }); + setTimeout(() => startCompletion(view), 0); + }, 10); + } + }, + })); + const word = completionContext.matchBefore(/[\w]*/); + return { from: word?.from ?? pos, options, validFor: /^\w*$/ }; + } + } + + // Fixed value from profile (fixedString, fixedUri, fixedCode) + if (resourceType && profileUrls.length > 0) { + const fixedVal = await getFixedValues( + effectivePath, + valueKey, + resourceType, + profileUrls, + getSDs, + ); + if (fixedVal != null) { + const option: Completion = { + label: fixedVal, + type: "text", + boost: 10, + apply: ( + view: EditorView, + _c: Completion, + from: number, + to: number, + ) => { + const d = view.state.doc.toString(); + let actualTo = to; + if (actualTo < d.length && d[actualTo] === '"') actualTo++; + view.dispatch({ + changes: { from, to: actualTo, insert: `${fixedVal}"` }, + selection: { anchor: from + fixedVal.length + 1 }, + }); + }, + }; + const word = completionContext.matchBefore(/[\w.:/-]*/); + return { from: word?.from ?? pos, options: [option], validFor: /^[\w.:/-]*$/ }; + } + } + + // reference value + if (valueKey === "reference" && resourceType) { + const targets = await resolveReferenceTargets( + effectivePath, + resourceType, + getSDs, + ); + if (targets) { + const options: Completion[] = targets.map((rt) => ({ + label: `${rt}/`, + type: "type", + apply: (view: EditorView, _c: Completion, from: number, to: number) => { + view.dispatch({ + changes: { from, to, insert: `${rt}/` }, + selection: { anchor: from + rt.length + 1 }, + }); + }, + })); + const word = completionContext.matchBefore(/[\w/]*/); + return { from: word?.from ?? pos, options, validFor: /^[\w/]*$/ }; + } + } + + // Extension URL value + if (valueKey === "url") { + const lastSeg = ctx.fullPath[ctx.fullPath.length - 1]; + if (lastSeg === "extension" || lastSeg === "modifierExtension") { + return handleExtensionUrlCompletion( + effectivePath, + resourceType, + profileUrls, + doc, + pos, + getSDs, + completionContext, + ctx, + ); + } + } + + // Boolean value + if (resourceType) { + const elements = await resolveElements(effectivePath, resourceType, getSDs); + const el = elements.find((e) => fieldName(e) === valueKey); + if (el?.type?.length === 1 && el.type[0]!.code === "boolean") { + const word = completionContext.matchBefore(/[\w]*/); + const options: Completion[] = ["true", "false"].map((v) => ({ + label: v, + type: "keyword", + apply: ( + view: EditorView, + _c: Completion, + from: number, + to: number, + ) => { + const d = view.state.doc.toString(); + let actualFrom = from; + let actualTo = to; + // Remove surrounding quotes if present + if (actualFrom > 0 && d[actualFrom - 1] === '"') actualFrom--; + if (actualTo < d.length && d[actualTo] === '"') actualTo++; + view.dispatch({ + changes: { from: actualFrom, to: actualTo, insert: v }, + selection: { anchor: actualFrom + v.length }, + }); + }, + })); + return { from: word?.from ?? pos, options, validFor: /^\w*$/ }; + } + } + + // Terminology binding + if ( + valueKey !== "resourceType" && + valueKey !== "reference" && + valueKey !== "url" && + expandValueSet && + resourceType + ) { + const valueSetUrl = await findBindingForValue( + effectivePath, + valueKey, + resourceType, + getSDs, + profileUrls, + doc, + pos, + ); + if (valueSetUrl) { + const quoteWord = completionContext.matchBefore(/"[\w-]*/); + const from = quoteWord?.from ?? pos; + const filter = quoteWord ? quoteWord.text.replace(/^"/, "") : ""; + try { + const codes = await expandValueSet(valueSetUrl, filter); + if (codes.length > 0) { + const options: Completion[] = codes.map((c) => ({ + label: c.code, + ...(c.display ? { info: c.display } : {}), + type: "text", + apply: ( + view: EditorView, + _c: Completion, + applyFrom: number, + applyTo: number, + ) => { + const d = view.state.doc.toString(); + let actualFrom = applyFrom; + let actualTo = applyTo; + if (d[actualFrom] === '"') actualFrom++; + if (actualTo < d.length && d[actualTo] === '"') actualTo++; + view.dispatch({ + changes: { + from: actualFrom, + to: actualTo, + insert: `${c.code}"`, + }, + selection: { anchor: actualFrom + c.code.length + 1 }, + }); + }, + })); + return { from, options, filter: false }; + } + } catch { + // expand failed + } + } + } + + return null; +} + +// ── Extension URL completion ─────────────────────────────────────────── + +async function handleExtensionUrlCompletion( + _effectivePath: string[], + resourceType: string | undefined, + profileUrls: string[], + doc: string, + pos: number, + getSDs: GetStructureDefinitions, + completionContext: CompletionContext, + ctx: DocumentContext, +): Promise { + // Check for nested extension (parent has url) + const urlKeyPos = doc.lastIndexOf('"url"', pos); + let scanEnd = urlKeyPos !== -1 ? urlKeyPos : pos; + for (let i = scanEnd - 1; i >= 0; i--) { + const c = doc[i]; + if (c === "{") { + scanEnd = i; + break; + } + if (c === "}" || c === "]" || c === "[") break; + } + const textBefore = doc.slice(0, scanEnd); + let parentExtUrl: string | null = null; + let depth = 0; + let inStr = false; + let esc = false; + let foundExtArray = false; + for (let i = textBefore.length - 1; i >= 0; i--) { + const ch = textBefore[i]; + if (esc) { + esc = false; + continue; + } + if (ch === "\\") { + esc = true; + continue; + } + if (ch === '"') { + inStr = !inStr; + continue; + } + if (inStr) continue; + if (ch === "}" || ch === "]") { + depth++; + } else if (ch === "[") { + if (depth === 0) { + foundExtArray = true; + continue; + } + depth--; + } else if (ch === "{") { + if (depth === 0 && foundExtArray) { + const objText = textBefore.slice(i); + const urlMatch = objText.match(/^\{[\s\S]*?"url"\s*:\s*"([^"]+)"/); + if (urlMatch?.[1]?.includes("/")) { + parentExtUrl = urlMatch[1]; + } + break; + } + if (depth === 0) break; + depth--; + } + } + + if (parentExtUrl) { + return handleNestedExtensionSlices( + parentExtUrl, + pos, + getSDs, + completionContext, + ); + } + + if (!resourceType) return null; + + // Top-level extension URL completions + const path = ctx.fullPath; + const contextTypes: string[] = [ + resourceType, + "DomainResource", + "Resource", + "Element", + ]; + const extIdx = path.lastIndexOf("extension"); + if (extIdx > 0) { + let currentRT = resourceType; + for (let i = 0; i < extIdx; i++) { + const seg = path[i]; + if (!seg) break; + const elements = await resolveElements([], currentRT, getSDs); + const el = elements.find((e) => fieldName(e) === seg); + if (el?.type?.[0]?.code && !isPrimitiveType(el.type[0].code)) + currentRT = el.type[0].code; + else break; + } + if (currentRT !== resourceType) { + contextTypes.length = 0; + contextTypes.push( + currentRT, + "Element", + `${resourceType}.${path.slice(0, extIdx).join(".")}`, + ); + } + } + + // Profile extensions + const profileExtUrls: string[] = []; + if (contextTypes.includes(resourceType)) { + for (const pUrl of profileUrls) { + const profileSD = await getCachedSD(pUrl, getSDs); + if (!profileSD?.differential?.element) continue; + for (const el of profileSD.differential.element) { + for (const t of el.type ?? []) { + if (t.code === "Extension") { + for (const p of t.profile ?? []) { + const clean = p.includes("|") ? p.slice(0, p.indexOf("|")) : p; + if (!profileExtUrls.includes(clean)) profileExtUrls.push(clean); + } + } + } + } + } + } + + const bareWord = completionContext.matchBefore(/[\w.:/-]*/); + const filter = bareWord?.text ?? ""; + const searchParams: StructureDefinitionSearchParams = { + type: "Extension", + derivation: "constraint", + _elements: "url,context", + _count: "500", + }; + if (filter) searchParams._ilike = filter; + const results = await getCachedSDList(searchParams, getSDs); + + const containerType = contextTypes[0]; + const fhirPath = contextTypes.find((c) => c.includes(".")); + const contextExts = results.filter((sd) => + sd.context?.some( + (c) => c.type === "element" && contextTypes.includes(c.expression), + ), + ); + const seen = new Set(); + const allExts: { url: string; boost: number }[] = []; + if (contextTypes.includes(resourceType)) { + for (const u of profileExtUrls) { + if (!seen.has(u)) { + seen.add(u); + allExts.push({ url: u, boost: 20 }); + } + } + } + for (const sd of contextExts) { + const u = sd.url ?? sd.type; + if (seen.has(u)) continue; + seen.add(u); + const ctxExprs = + sd.context + ?.filter((c) => c.type === "element") + .map((c) => c.expression) ?? []; + let boost = 0; + if (fhirPath && ctxExprs.includes(fhirPath)) boost = 15; + else if (containerType && ctxExprs.includes(containerType)) boost = 10; + else if (ctxExprs.includes(resourceType)) boost = 5; + else if (ctxExprs.some((e) => e === "DomainResource" || e === "Resource")) + boost = 2; + else if (ctxExprs.includes("Element")) boost = 1; + allExts.push({ url: u, boost }); + } + const lf = filter.toLowerCase(); + const filtered = ( + lf ? allExts.filter((e) => e.url.toLowerCase().includes(lf)) : allExts + ).sort((a, b) => b.boost - a.boost); + if (filtered.length > 0) { + const options: Completion[] = filtered.map((ext) => ({ + label: ext.url, + type: "text", + boost: ext.boost, + apply: ( + view: EditorView, + _c: Completion, + applyFrom: number, + applyTo: number, + ) => { + const d = view.state.doc.toString(); + let actualTo = applyTo; + if (actualTo < d.length && d[actualTo] === '"') actualTo++; + view.dispatch({ + changes: { from: applyFrom, to: actualTo, insert: `${ext.url}"` }, + selection: { anchor: applyFrom + ext.url.length + 1 }, + }); + setTimeout(async () => { + const fullSD = await getCachedSD(ext.url, getSDs); + if (!fullSD) return; + const extInfo = analyzeExtensionSD(fullSD); + if (!extInfo) return; + const cursorPos = view.state.selection.main.head; + const curDoc = view.state.doc.toString(); + const after = curDoc.slice(cursorPos); + if (!/^\s*\n\s*\}/.test(after)) return; + const lineObj = view.state.doc.lineAt(cursorPos); + const ind = lineObj.text.match(/^(\s*)/)?.[1] ?? ""; + let ins: string; + let cOff: number; + if (extInfo.isNested) { + const inner = ind + " "; + const innerInner = inner + " "; + ins = `,\n${ind}"extension": [\n${inner}{\n${innerInner}"url": ""\n${inner}}\n${ind}]`; + cOff = ins.lastIndexOf('""') + 1; + } else if (extInfo.valueTypes.length === 1) { + const tc = extInfo.valueTypes[0]!; + const vf = `value${tc.charAt(0).toUpperCase()}${tc.slice(1)}`; + if (FHIR_STRING_TYPES.has(tc) || tc === "code") { + ins = `,\n${ind}"${vf}": ""`; + cOff = ins.length - 1; + } else if (FHIR_NUMBER_TYPES.has(tc)) { + ins = `,\n${ind}"${vf}": `; + cOff = ins.length; + } else { + const inner = ind + " "; + ins = `,\n${ind}"${vf}": {\n${inner}\n${ind}}`; + cOff = ins.indexOf(inner + "\n" + ind) + inner.length; + } + } else { + return; + } + view.dispatch({ + changes: { from: cursorPos, insert: ins }, + selection: { anchor: cursorPos + cOff }, + }); + setTimeout(() => startCompletion(view), 0); + }, 10); + }, + })); + return { from: bareWord?.from ?? pos, options, filter: false }; + } + return null; +} + +async function handleNestedExtensionSlices( + parentExtUrl: string, + pos: number, + getSDs: GetStructureDefinitions, + completionContext: CompletionContext, +): Promise { + const parentSD = await getCachedSD(parentExtUrl, getSDs); + if (!parentSD) return null; + const info = analyzeExtensionSD(parentSD); + if (!info?.slices.length) return null; + + const word = completionContext.matchBefore(/[\w.:/-]*/); + const filter = word?.text.toLowerCase() ?? ""; + const matching = filter + ? info.slices.filter( + (s) => + s.fixedUri.toLowerCase().includes(filter) || + (s.short?.toLowerCase().includes(filter) ?? false), + ) + : info.slices; + + const options: Completion[] = matching.map((slice) => { + const sliceElements = parentSD.differential?.element ?? []; + let sliceValueTypes: string[] = []; + let inSl = false; + for (const el of sliceElements) { + if ( + el.path === "Extension.extension" && + el.sliceName === slice.sliceName + ) { + inSl = true; + continue; + } + if (inSl && el.path === "Extension.extension.value[x]") { + sliceValueTypes = el.type?.map((t) => t.code) ?? []; + break; + } + if (inSl && el.path === "Extension.extension" && el.sliceName) break; + } + return { + label: slice.fixedUri, + ...(slice.short ? { info: slice.short } : {}), + type: "text", + apply: (view: EditorView, _c: Completion, from: number, to: number) => { + const d = view.state.doc.toString(); + let actualTo = to; + if (actualTo < d.length && d[actualTo] === '"') actualTo++; + view.dispatch({ + changes: { from, to: actualTo, insert: `${slice.fixedUri}"` }, + selection: { anchor: from + slice.fixedUri.length + 1 }, + }); + if (sliceValueTypes.length === 1) { + setTimeout(() => { + const cp = view.state.selection.main.head; + const cd = view.state.doc.toString(); + const af = cd.slice(cp); + if (!/^\s*\n\s*\}/.test(af)) return; + const lo = view.state.doc.lineAt(cp); + const ind = lo.text.match(/^(\s*)/)?.[1] ?? ""; + const tc = sliceValueTypes[0]!; + const vf = `value${tc.charAt(0).toUpperCase()}${tc.slice(1)}`; + let ins: string; + let cOff: number; + if (FHIR_STRING_TYPES.has(tc) || tc === "code") { + ins = `,\n${ind}"${vf}": ""`; + cOff = ins.length - 1; + } else if (FHIR_NUMBER_TYPES.has(tc)) { + ins = `,\n${ind}"${vf}": `; + cOff = ins.length; + } else { + const inner = ind + " "; + ins = `,\n${ind}"${vf}": {\n${inner}\n${ind}}`; + cOff = ins.indexOf(inner + "\n" + ind) + inner.length; + } + view.dispatch({ + changes: { from: cp, insert: ins }, + selection: { anchor: cp + cOff }, + }); + setTimeout(() => startCompletion(view), 0); + }, 10); + } + }, + }; + }); + if (options.length > 0) + return { from: word?.from ?? pos, options, filter: false }; + return null; +} + +// ── Array item completion ────────────────────────────────────────────── + +async function handleArrayItemCompletion( + parentKey: string, + effectivePath: string[], + resourceType: string | undefined, + _doc: string, + pos: number, + getSDs: GetStructureDefinitions, + completionContext: CompletionContext, + profileUrls: string[], +): Promise { + if (!resourceType) return null; + + // Parameters.parameter or part → snippet completions from profile slices + if ( + (parentKey === "parameter" || parentKey === "part") && + (await isParametersType(resourceType, getSDs)) + ) { + const slices = + parentKey === "parameter" + ? await getParameterSlices(profileUrls, getSDs) + : []; + + const options: Completion[] = []; + + for (const slice of slices) { + options.push({ + label: slice.fixedName, + type: "text", + detail: slice.min > 0 + ? `${slice.min}..${slice.max}` + : `0..${slice.max}`, + boost: slice.min > 0 ? 2 : 0, + ...(slice.short ? { info: slice.short } : {}), + apply: ( + view: EditorView, + _c: Completion, + from: number, + to: number, + ) => { + const line = view.state.doc.lineAt(from); + const indent = line.text.match(/^(\s*)/)?.[1] ?? ""; + const { text, cursorOffset } = buildParameterSnippet( + slice.fixedName, + slice.valueTypes, + indent, + ); + + view.dispatch({ + changes: { from, to, insert: text }, + selection: { anchor: from + cursorOffset }, + }); + setTimeout(() => startCompletion(view), 0); + }, + }); + } + + // Generic parameter template (always available) + options.push({ + label: "parameter", + type: "text", + boost: -1, + info: "Custom parameter", + apply: ( + view: EditorView, + _c: Completion, + from: number, + to: number, + ) => { + const line = view.state.doc.lineAt(from); + const indent = line.text.match(/^(\s*)/)?.[1] ?? ""; + const { text, cursorOffset } = buildParameterSnippet("", [], indent); + view.dispatch({ + changes: { from, to, insert: text }, + selection: { anchor: from + cursorOffset }, + }); + setTimeout(() => startCompletion(view), 0); + }, + }); + + const word = completionContext.matchBefore(/[\w]*/); + return { from: word?.from ?? pos, options }; + } + + // Get path excluding the array key itself + const parentPath = + effectivePath.length > 0 && + effectivePath[effectivePath.length - 1] === parentKey + ? effectivePath.slice(0, -1) + : effectivePath; + + const targetType = await findCanonicalTargetType( + parentPath, + parentKey, + resourceType, + getSDs, + ); + if (targetType === "StructureDefinition") { + const allSDs = await getCachedSDList( + { + type: `${resourceType},DomainResource,Resource`, + derivation: "constraint", + _elements: "url,name", + _count: "50", + }, + getSDs, + ); + const seen = new Set(); + const uniqueSDs = allSDs.filter((sd) => { + const u = sd.url ?? sd.type; + if (seen.has(u)) return false; + seen.add(u); + return true; + }); + if (uniqueSDs.length > 0) { + const quoteWord = completionContext.matchBefore(/"[^"]*/); + const bareWord = completionContext.matchBefore(/[\w.:/-]*/); + const from = quoteWord?.from ?? bareWord?.from ?? pos; + const filter = quoteWord + ? quoteWord.text.replace(/^"/, "").toLowerCase() + : (bareWord?.text.toLowerCase() ?? ""); + const filtered = filter + ? uniqueSDs.filter( + (sd) => + sd.name?.toLowerCase().includes(filter) || + sd.url?.toLowerCase().includes(filter), + ) + : uniqueSDs; + const options: Completion[] = filtered.map((sd) => { + const url = sd.url ?? sd.type; + return { + label: url, + ...(sd.name ? { info: sd.name } : {}), + type: "text", + apply: ( + view: EditorView, + _c: Completion, + applyFrom: number, + applyTo: number, + ) => { + const d = view.state.doc.toString(); + let actualTo = applyTo; + if (actualTo < d.length && d[actualTo] === '"') actualTo++; + view.dispatch({ + changes: { from: applyFrom, to: actualTo, insert: `"${url}"` }, + selection: { anchor: applyFrom + url.length + 2 }, + }); + }, + }; + }); + if (options.length > 0) { + return { from, options, filter: false }; + } + } + } + if (targetType) return null; + return null; +} + +// ── Property completion ──────────────────────────────────────────────── + +async function handlePropertyCompletion( + effectivePath: string[], + resourceType: string | undefined, + hasExplicitResourceType: boolean, + doc: string, + pos: number, + getSDs: GetStructureDefinitions, + completionContext: CompletionContext, + ctx: DocumentContext, +): Promise { + const line = completionContext.state.doc.lineAt(pos); + const beforeCursor = line.text.slice(0, pos - line.from).trimStart(); + + // Only auto-trigger property completions when user has started typing + if ( + !completionContext.explicit && + /,\s*"?\s*$/.test(beforeCursor) && + !completionContext.matchBefore(/\w+/) + ) + return null; + + const makeJsonRtCompletion = (): Completion => { + const c: Completion = { + label: "resourceType", + type: "property", + detail: "string", + boost: 10, + apply: (view, _completion, from, to) => { + const d = view.state.doc.toString(); + let actualFrom = from; + let actualTo = to; + if (actualFrom > 0 && d[actualFrom - 1] === '"') actualFrom--; + if (actualTo < d.length && d[actualTo] === '"') actualTo++; + const text = '"resourceType": ""'; + view.dispatch({ + changes: { from: actualFrom, to: actualTo, insert: text }, + selection: { anchor: actualFrom + text.length - 1 }, + }); + }, + }; + c.info = "FHIR resource type"; + return c; + }; + + let completions: Completion[]; + if (resourceType) { + const elements = await resolveElements(effectivePath, resourceType, getSDs); + const isParams = await isParametersType(resourceType, getSDs); + const mapFn = isParams + ? (el: FhirElement) => toParameterPropertyCompletion(el) + : toCompletion; + completions = elementsToCompletions(elements, mapFn); + if (!hasExplicitResourceType && effectivePath.length === 0) { + completions = [makeJsonRtCompletion(), ...completions]; + } + } else if (effectivePath.length === 0) { + const domainElements = await resolveElements( + effectivePath, + "DomainResource", + getSDs, + ); + completions = [ + makeJsonRtCompletion(), + ...elementsToCompletions(domainElements, toCompletion), + ]; + } else { + return null; + } + + // Filter out properties already present in current object + const existingKeys = new Set(ctx.getScope(0).getKeys()); + completions = completions.filter((c) => !existingKeys.has(c.label)); + + if (completions.length === 0) return null; + + const word = completionContext.matchBefore(/"?\w*/); + let from = word?.from ?? pos; + if (from < doc.length && doc[from] === '"') from++; + + return { from, options: completions, validFor: /^\w*$/ }; +} + +// ── Thin wrapper ─────────────────────────────────────────────────────── + +/** @internal — exported for tests only */ +export function jsonCompletionSource( + getSDs: GetStructureDefinitions, + resourceTypeHint?: string, + expandValueSet?: ExpandValueSet, +): CompletionSource { + return async (cc: CompletionContext): Promise => { + const ctx = buildJsonDocumentContext(cc.state.doc.toString(), cc.pos); + return fhirComplete(ctx, getSDs, resourceTypeHint, expandValueSet, cc); + }; +} + +// ── Validation ───────────────────────────────────────────────────────── + +type FhirDiagnostic = { + from: number; + to: number; + message: string; +}; + +async function validateFhirProperties( + properties: PropertyInfo[], + getSDs: GetStructureDefinitions, +): Promise { + const groups = new Map< + string, + { resourceType: string; path: string[]; props: PropertyInfo[] } + >(); + for (const prop of properties) { + const key = `${prop.resourceType}|${prop.path.join(".")}`; + let group = groups.get(key); + if (!group) { + group = { + resourceType: prop.resourceType, + path: [...prop.path], + props: [], + }; + groups.set(key, group); + } + group.props.push(prop); + } + + const diagnostics: FhirDiagnostic[] = []; + + for (const { resourceType, path, props } of groups.values()) { + const elements = await resolveElements(path, resourceType, getSDs); + if (elements.length === 0) continue; + + const validNames = new Set(); + for (const el of elements) { + const name = fieldName(el); + validNames.add(name); + const typeCode = el.type?.[0]?.code; + if (el.type?.length === 1 && typeCode && isPrimitiveType(typeCode)) { + validNames.add(`_${name}`); + } + } + if (path.length === 0) { + validNames.add("resourceType"); + } + + for (const prop of props) { + if (!validNames.has(prop.name)) { + diagnostics.push({ + from: prop.from, + to: prop.to, + message: `Unknown property "${prop.name}"`, + }); + } + } + } + + return diagnostics; +} + +function buildFhirValidationPlugin( + getSDs: GetStructureDefinitions, + resourceTypeHint?: string, +): Extension { + return ViewPlugin.define((view) => { + let timeout: ReturnType | null = null; + let destroyed = false; + + function hasActiveDiagnostics() { + try { + return view.state.field(fhirDiagnosticsField).messages.size > 0; + } catch { + return false; + } + } + + function scheduleCheck() { + if (timeout) clearTimeout(timeout); + const delay = hasActiveDiagnostics() ? 0 : 1500; + timeout = setTimeout(() => check(), delay); + } + + async function check() { + if (destroyed) return; + const currentDoc = view.state.doc.toString(); + const tree = + ensureSyntaxTree(view.state, view.state.doc.length, 1000) ?? + syntaxTree(view.state); + + const { properties, emptyStrings } = walkJsonProperties( + currentDoc, + tree, + resourceTypeHint ?? null, + ); + + if (!findRootJsonObject(currentDoc, tree)) { + try { + view.dispatch({ effects: setFhirDiagnosticsEffect.of([]) }); + } catch { + /* view destroyed */ + } + return; + } + + if (properties.length === 0 && emptyStrings.length === 0) { + try { + view.dispatch({ effects: setFhirDiagnosticsEffect.of([]) }); + } catch { + /* view destroyed */ + } + return; + } + + const rawDiags = await validateFhirProperties(properties, getSDs); + if (destroyed) return; + if (view.state.doc.toString() !== currentDoc) return; + + for (const es of emptyStrings) { + rawDiags.push({ + from: es.from, + to: es.to, + message: "Value must not be empty", + }); + } + + const diags: FhirDiagnosticWithLine[] = rawDiags.map((d) => ({ + ...d, + line: view.state.doc.lineAt(d.from).number, + })); + + try { + view.dispatch({ effects: setFhirDiagnosticsEffect.of(diags) }); + } catch { + /* view destroyed */ + } + } + + scheduleCheck(); + + return { + update(update: ViewUpdate) { + if (update.docChanged) { + scheduleCheck(); + } + }, + destroy() { + destroyed = true; + if (timeout) clearTimeout(timeout); + }, + }; + }); +} + +// ── FHIR validation decorations ─────────────────────────────────────── + +type FhirDiagnosticWithLine = FhirDiagnostic & { line: number }; + +const setFhirDiagnosticsEffect = StateEffect.define(); + +const fhirUnderline = Decoration.mark({ class: "cm-fhir-error-underline" }); +const fhirErrorLineDecoration = Decoration.line({ class: "cm-errorLine" }); + +class FhirGutterMarker extends GutterMarker { + elementClass = "cm-errorLineGutter"; +} +const fhirGutterMarker = new FhirGutterMarker(); + +export const fhirDiagnosticsField = StateField.define<{ + marks: RangeSet; + lineDecos: RangeSet; + gutterMarkers: RangeSet; + messages: Map; +}>({ + create() { + return { + marks: Decoration.none, + lineDecos: Decoration.none, + gutterMarkers: RangeSet.empty, + messages: new Map(), + }; + }, + update(value, tr) { + for (const effect of tr.effects) { + if (effect.is(setFhirDiagnosticsEffect)) { + const diags = effect.value; + if (diags.length === 0) { + return { + marks: Decoration.none, + lineDecos: Decoration.none, + gutterMarkers: RangeSet.empty, + messages: new Map(), + }; + } + + const marks: { from: number; to: number; value: Decoration }[] = []; + const lineDecos: { from: number; to: number; value: Decoration }[] = []; + const gutter: { from: number; to: number; value: GutterMarker }[] = []; + const messages = new Map(); + + for (const d of diags) { + marks.push(fhirUnderline.range(d.from, d.to)); + const existing = messages.get(d.line); + if (existing) { + messages.set(d.line, `${existing}\n${d.message}`); + } else { + messages.set(d.line, d.message); + const line = tr.state.doc.line(d.line); + lineDecos.push(fhirErrorLineDecoration.range(line.from)); + gutter.push(fhirGutterMarker.range(line.from)); + } + } + + return { + marks: Decoration.set(marks, true), + lineDecos: Decoration.set(lineDecos, true), + gutterMarkers: RangeSet.of(gutter, true), + messages, + }; + } + } + if (tr.docChanged) { + try { + return { + marks: value.marks.map(tr.changes), + lineDecos: value.lineDecos.map(tr.changes), + gutterMarkers: value.gutterMarkers.map(tr.changes), + messages: value.messages, + }; + } catch { + return { + marks: Decoration.none, + lineDecos: Decoration.none, + gutterMarkers: RangeSet.empty, + messages: new Map(), + }; + } + } + return value; + }, + provide(field) { + return [ + EditorView.decorations.from(field, (v) => v.marks), + EditorView.decorations.from(field, (v) => v.lineDecos), + gutterLineClass.from(field, (v) => v.gutterMarkers), + ]; + }, +}); + +const fhirLinterTheme = EditorView.theme({ + ".cm-fhir-error-underline": { + textDecorationLine: "underline", + textDecorationStyle: "wavy", + textDecorationColor: "var(--color-text-error-primary)", + textUnderlineOffset: "3px", + }, + ".cm-lineNumbers .cm-gutterElement.cm-errorLineGutter": { + color: "var(--color-text-error-primary)", + backgroundColor: + "color-mix(in srgb, var(--color-text-error-primary) 7%, transparent)", + }, +}); + +// ── Public API ───────────────────────────────────────────────────────── + +export function buildFhirCompletionExtension( + getSDs: GetStructureDefinitions, + resourceTypeHint?: string, + expandValueSet?: ExpandValueSet, +): Extension { + const jsonSource = jsonCompletionSource( + getSDs, + resourceTypeHint, + expandValueSet, + ); + + const autoTrigger = EditorView.updateListener.of((update) => { + if (!update.docChanged) return; + if (completionStatus(update.view.state)) return; + const { state } = update.view; + const pos = state.selection.main.head; + const doc = state.doc.toString(); + const line = state.doc.lineAt(pos); + const beforeCursor = line.text.slice(0, pos - line.from).trimStart(); + // Trigger on empty lines (including after snippet insertion), + // after [ (array open), or after " (string value start) + const shouldTrigger = + beforeCursor === "" || + (pos > 0 && doc[pos - 1] === "[") || + (pos > 0 && doc[pos - 1] === '"' && pos > 1 && doc[pos - 2] !== "\\"); + if (!shouldTrigger) return; + // Skip bulk replacements (e.g. tab switch, currentValue update) + // but allow snippets — check only if the ENTIRE doc was replaced + let totalInserted = 0; + update.changes.iterChanges((_fA, _tA, _fB, _tB, ins) => { + totalInserted += ins.length; + }); + if (totalInserted > doc.length * 0.5) return; + setTimeout(() => startCompletion(update.view), 0); + }); + + return [ + jsonLanguage.data.of({ autocomplete: jsonSource }), + autoTrigger, + fhirDiagnosticsField, + fhirLinterTheme, + buildFhirValidationPlugin(getSDs, resourceTypeHint), + ]; +} diff --git a/packages/react-components/src/components/code-editor/fhir-completion.ts b/packages/react-components/src/components/code-editor/fhir-completion.ts deleted file mode 100644 index 5b3242c7..00000000 --- a/packages/react-components/src/components/code-editor/fhir-completion.ts +++ /dev/null @@ -1,2466 +0,0 @@ -import { - type Completion, - type CompletionContext, - type CompletionResult, - type CompletionSource, - completionStatus, - startCompletion, -} from "@codemirror/autocomplete"; -import { jsonLanguage } from "@codemirror/lang-json"; -import { yamlLanguage } from "@codemirror/lang-yaml"; -import { - type Extension, - RangeSet, - StateEffect, - StateField, -} from "@codemirror/state"; -import { - Decoration, - EditorView, - GutterMarker, - gutterLineClass, - ViewPlugin, - type ViewUpdate, -} from "@codemirror/view"; -import { ensureSyntaxTree, syntaxTree } from "@codemirror/language"; -import type { SyntaxNode } from "@lezer/common"; - -// ── Types ────────────────────────────────────────────────────────────── - -interface FhirElementType { - code: string; - profile?: string[]; - targetProfile?: string[]; -} - -interface FhirElement { - path: string; - short?: string; - definition?: string; - min?: number; - max?: string; - type?: FhirElementType[]; - binding?: { valueSet: string; strength: string }; - contentReference?: string; - sliceName?: string; - fixedUri?: string; -} - -interface StructureDefinition { - type: string; - url?: string; - name?: string; - baseDefinition?: string; - context?: { expression: string; type: string }[]; - differential?: { element: FhirElement[] }; -} - -export interface StructureDefinitionSearchParams { - type?: string; - url?: string; - derivation?: string; - "derivation:missing"?: string; - kind?: string; - _count?: string; - _elements?: string; - _ilike?: string; -} - -export type GetStructureDefinitions = ( - params: StructureDefinitionSearchParams, -) => Promise; - -export type ExpandValueSet = ( - url: string, - filter: string, -) => Promise<{ code: string; display?: string; system?: string }[]>; - -// ── Cache ────────────────────────────────────────────────────────────── - -const sdCache = new Map(); -const pendingRequests = new Map>(); -const listCache = new Map(); -const pendingListRequests = new Map>(); - -const SD_ELEMENTS = "differential,type,name,baseDefinition,url,context"; - -function cacheKey(params: StructureDefinitionSearchParams): string { - return JSON.stringify(params); -} - -async function getCachedSDList( - params: StructureDefinitionSearchParams, - getSDs: GetStructureDefinitions, -): Promise { - const key = cacheKey(params); - if (listCache.has(key)) return listCache.get(key) ?? []; - - let pending = pendingListRequests.get(key); - if (!pending) { - pending = getSDs(params) - .then((list) => { - listCache.set(key, list); - pendingListRequests.delete(key); - // Only cache individual SDs that have differential - for (const sd of list) { - if (sd.differential?.element) { - sdCache.set(sd.type, sd); - } - } - return list; - }) - .catch(() => { - pendingListRequests.delete(key); - listCache.set(key, []); - return []; - }); - pendingListRequests.set(key, pending); - } - return pending; -} - -async function getCachedSD( - type: string, - getSDs: GetStructureDefinitions, -): Promise { - if (sdCache.has(type)) return sdCache.get(type) ?? null; - - const key = `single:${type}`; - let pending = pendingRequests.get(key); - if (!pending) { - const isUrl = type.includes("/"); - const searchByType = (params: StructureDefinitionSearchParams) => - getSDs(params).then((list) => list[0] ?? null); - - pending = ( - isUrl - ? searchByType({ url: type, _elements: SD_ELEMENTS, _count: "1" }) - : searchByType({ - type, - derivation: "specialization", - _elements: SD_ELEMENTS, - _count: "1", - }).then( - (sd) => - sd ?? - searchByType({ - type, - "derivation:missing": "true", - _elements: SD_ELEMENTS, - _count: "1", - }), - ) - ) - .then((sd) => { - sdCache.set(type, sd); - pendingRequests.delete(key); - return sd; - }) - .catch(() => { - pendingRequests.delete(key); - return null; - }); - pendingRequests.set(key, pending); - } - return pending; -} - -// ── JSON path at cursor ──────────────────────────────────────────────── - -function getJsonPathAtCursor(doc: string, pos: number): string[] { - const path: string[] = []; - let inString = false; - let isEscaped = false; - let currentKey = ""; - let collectingKey = false; - let lastKey = ""; - - for (let i = 0; i < pos; i++) { - const ch = doc[i]; - - if (isEscaped) { - if (collectingKey) currentKey += ch; - isEscaped = false; - continue; - } - if (ch === "\\") { - isEscaped = true; - if (collectingKey) currentKey += ch; - continue; - } - if (ch === '"') { - if (!inString) { - inString = true; - collectingKey = true; - currentKey = ""; - } else { - inString = false; - if (collectingKey) { - lastKey = currentKey; - collectingKey = false; - } - } - continue; - } - if (inString) { - if (collectingKey) currentKey += ch; - continue; - } - if (ch === "{") { - if (lastKey) path.push(lastKey); - lastKey = ""; - } else if (ch === "}") { - path.pop(); - lastKey = ""; - } else if (ch === ",") { - lastKey = ""; - } - } - return path; -} - -// ── YAML path at cursor ───────────────────────────────────────────────── - -function getYamlPathAtCursor(doc: string, pos: number): string[] { - const lines = doc.slice(0, pos).split("\n"); - const currentLine = lines[lines.length - 1] ?? ""; - let currentIndent = currentLine.search(/\S/); - if (currentIndent === -1) currentIndent = currentLine.length; - - // Walk backwards to build path from indentation - const path: string[] = []; - let targetIndent = currentIndent; - - for (let i = lines.length - 2; i >= 0; i--) { - const line = lines[i] ?? ""; - const trimmed = line.trimStart(); - if (!trimmed || trimmed.startsWith("#")) continue; - - const indent = line.search(/\S/); - const isArrayItem = trimmed.startsWith("- "); - const content = isArrayItem ? trimmed.slice(2) : trimmed; - const colonIdx = content.indexOf(":"); - - if (indent < targetIndent && colonIdx > 0) { - // For array items like " - given:", dash is at indent 2 but - // content starts at indent 4. If cursor is at indent 4, it's a - // sibling of "given" (same array item), not nested under it. - if (isArrayItem && indent + 2 >= targetIndent) { - targetIndent = indent; - continue; - } - const key = content.slice(0, colonIdx).trim(); - path.unshift(key); - targetIndent = indent; - } - } - - return path; -} - -function getYamlResourceType(doc: string): string | null { - const match = doc.match(/^resourceType:\s*(\S+)/m); - return match?.[1] ?? null; -} - -function isYamlPropertyPosition(beforeCursor: string): boolean { - const trimmed = beforeCursor.trimStart(); - // Empty line, or after "- " (dash with space) - if (trimmed === "" || trimmed === "- ") return true; - // Bare "-" without space — not ready for property yet - if (trimmed === "-") return false; - if (trimmed.includes(":")) return false; - // Typing a word without colon = key position (optionally after "- ") - return /^(-\s+)?[\w]*$/.test(trimmed); -} - -function isYamlValuePosition(beforeCursor: string): string | null { - const match = beforeCursor.match(/(\w+):\s*(\S*)$/); - if (match) return match[1] ?? null; - return null; -} - -// ── Element helpers ──────────────────────────────────────────────────── - -function fieldName(element: FhirElement): string { - const parts = element.path.split("."); - return (parts[parts.length - 1] ?? "").replace("[x]", ""); -} - -function directChildren( - elements: FhirElement[], - parentPath: string, -): FhirElement[] { - const prefix = `${parentPath}.`; - return elements.filter((el) => { - if (!el.path.startsWith(prefix)) return false; - const rest = el.path.slice(prefix.length); - return !rest.includes("."); - }); -} - -function findElement( - elements: FhirElement[], - parentPath: string, - key: string, -): FhirElement | undefined { - // Direct match - const direct = elements.find((el) => { - if (!el.path.startsWith(`${parentPath}.`)) return false; - const name = fieldName(el); - return name === key || name.toLowerCase() === key.toLowerCase(); - }); - if (direct) return direct; - - // Choice type match: key "deceasedBoolean" → element "deceased[x]" with type boolean - // Return element with only the matched type so resolveElements picks the right one - for (const el of elements) { - if (!el.path.endsWith("[x]")) continue; - if (!el.path.startsWith(`${parentPath}.`)) continue; - const baseName = fieldName(el); - if (!key.toLowerCase().startsWith(baseName.toLowerCase())) continue; - const typeSuffix = key.slice(baseName.length).toLowerCase(); - const matchedType = el.type?.find((t) => t.code.toLowerCase() === typeSuffix); - if (matchedType) { - return { ...el, type: [matchedType] }; - } - } - return undefined; -} - -// ── Resolve completions at path ──────────────────────────────────────── - -// Collect all elements including inherited from base definitions -async function collectAllElements( - type: string, - getSDs: GetStructureDefinitions, -): Promise<{ elements: FhirElement[]; basePath: string } | null> { - const sd = await getCachedSD(type, getSDs); - if (!sd?.differential?.element) return null; - - const elements = [...sd.differential.element]; - - // Recursively load base definition elements - if (sd.baseDefinition) { - const base = await collectAllElements(sd.baseDefinition, getSDs); - if (base) { - for (const baseEl of base.elements) { - const remappedPath = baseEl.path.replace( - new RegExp(`^${base.basePath}`), - sd.type, - ); - if (!elements.some((e) => e.path === remappedPath)) { - elements.push({ ...baseEl, path: remappedPath }); - } - } - } - } - - return { elements, basePath: sd.type }; -} - -async function resolveElements( - path: string[], - resourceType: string, - getSDs: GetStructureDefinitions, -): Promise { - const result = await collectAllElements(resourceType, getSDs); - if (!result) return []; - - let currentPath = resourceType; - let currentElements = result.elements; - - for (const key of path) { - if (key === "resourceType") return []; - - const el = findElement(currentElements, currentPath, key); - if (!el) return []; - - // contentReference (e.g. "#Questionnaire.item") — resolve to referenced path - if (el.contentReference) { - const refPath = el.contentReference.replace(/^#/, ""); - currentPath = refPath; - continue; - } - - if (!el.type?.[0]) return []; - const typeCode = el.type[0].code; - - if (typeCode === "BackboneElement") { - currentPath = el.path; - continue; - } - - const typeResult = await collectAllElements(typeCode, getSDs); - if (!typeResult) return []; - currentPath = typeResult.basePath; - currentElements = typeResult.elements; - } - - // Expand choice types and collect - const children = directChildren(currentElements, currentPath); - const expanded: FhirElement[] = []; - - for (const el of children) { - const isChoiceType = el.path.endsWith("[x]"); - if (isChoiceType && el.type && el.type.length > 0) { - for (const t of el.type) { - expanded.push({ - ...el, - path: el.path.replace( - "[x]", - t.code.charAt(0).toUpperCase() + t.code.slice(1), - ), - type: [t], - }); - } - } else { - expanded.push(el); - } - } - - return expanded; -} - -function elementsToCompletions( - elements: FhirElement[], - mapFn: (el: FhirElement) => Completion, -): Completion[] { - const completions: Completion[] = []; - for (const el of elements) { - completions.push(mapFn(el)); - // Primitive extensions - const name = fieldName(el); - const firstTypeCode = el.type?.[0]?.code; - if ( - el.type?.length === 1 && - firstTypeCode && - PRIMITIVE_TYPES.has(firstTypeCode) - ) { - const ext: Completion = { - label: `_${name}`, - type: "property", - detail: "Element", - boost: -1, - }; - ext.info = "Primitive element extension"; - completions.push(ext); - } - } - return completions; -} - -const PRIMITIVE_TYPES = new Set([ - "boolean", - "integer", - "string", - "decimal", - "uri", - "url", - "canonical", - "base64Binary", - "instant", - "date", - "dateTime", - "time", - "code", - "oid", - "id", - "markdown", - "unsignedInt", - "positiveInt", - "uuid", - "xhtml", -]); - -function isPrimitiveType(typeCode: string): boolean { - return ( - PRIMITIVE_TYPES.has(typeCode) || - typeCode.startsWith("http://hl7.org/fhirpath/System.") - ); -} - -const FHIR_STRING_TYPES = new Set([ - "string", - "code", - "uri", - "url", - "canonical", - "id", - "markdown", - "oid", - "uuid", - "base64Binary", - "xhtml", - "http://hl7.org/fhirpath/System.String", -]); - -const FHIR_NUMBER_TYPES = new Set([ - "boolean", - "integer", - "decimal", - "positiveInt", - "unsignedInt", - "http://hl7.org/fhirpath/System.Boolean", - "http://hl7.org/fhirpath/System.Integer", - "http://hl7.org/fhirpath/System.Decimal", -]); - -type SnippetKind = - | "array-complex" - | "array-primitive" - | "array-extension" - | "object" - | "string" - | "number" - | "bare"; - -function snippetKind(element: FhirElement): SnippetKind { - const isArray = element.max === "*"; - const typeCode = element.type?.[0]?.code; - if (!typeCode) { - // contentReference elements have no type but are complex objects - if (element.contentReference) return isArray ? "array-complex" : "object"; - return "bare"; - } - // Extension arrays get special snippet with {"url": ""} - if (typeCode === "Extension" && isArray) return "array-extension"; - if (isArray) - return isPrimitiveType(typeCode) ? "array-primitive" : "array-complex"; - if (FHIR_NUMBER_TYPES.has(typeCode)) return "number"; - if (isPrimitiveType(typeCode)) return "string"; - return "object"; -} - -function buildSnippet( - name: string, - kind: SnippetKind, - indent: string, -): { text: string; cursorOffset: number } { - const inner = indent + " "; - const innerInner = inner + " "; - switch (kind) { - case "array-complex": { - const text = `"${name}": [\n${inner}{\n${innerInner}\n${inner}}\n${indent}]`; - return { - text, - cursorOffset: text.indexOf(innerInner) + innerInner.length, - }; - } - case "array-extension": { - const text = `"${name}": [\n${inner}{\n${innerInner}"url": ""\n${inner}}\n${indent}]`; - return { text, cursorOffset: text.lastIndexOf('""') + 1 }; - } - case "array-primitive": { - const text = `"${name}": [\n${inner}\n${indent}]`; - return { text, cursorOffset: text.indexOf(inner + "\n") + inner.length }; - } - case "object": { - const text = `"${name}": {\n${inner}\n${indent}}`; - return { text, cursorOffset: text.indexOf(inner + "\n") + inner.length }; - } - case "string": { - const text = `"${name}": ""`; - return { text, cursorOffset: text.length - 1 }; - } - case "number": - case "bare": - default: { - const text = `"${name}": `; - return { text, cursorOffset: text.length }; - } - } -} - -// ── Extension helpers ────────────────────────────────────────────────── - -interface ExtensionInfo { - url: string; - name?: string | undefined; - isNested: boolean; - valueTypes: string[]; - slices: { sliceName: string; fixedUri: string; short?: string | undefined }[]; -} - -function analyzeExtensionSD(sd: StructureDefinition): ExtensionInfo | null { - if (!sd.differential?.element) return null; - const elements = sd.differential.element; - - const valueEl = elements.find((e) => e.path === "Extension.value[x]"); - const isNested = valueEl?.max === "0"; - const valueTypes = isNested ? [] : (valueEl?.type?.map((t) => t.code) ?? []); - - const slices: ExtensionInfo["slices"] = []; - for (const el of elements) { - if (el.path === "Extension.extension" && el.sliceName) { - const sliceName = el.sliceName; - const urlEl = elements.find( - (e) => - e.path === "Extension.extension.url" && - e.fixedUri && - elements.indexOf(e) > elements.indexOf(el), - ); - const fixedUri = urlEl?.fixedUri ?? sliceName; - slices.push({ sliceName, fixedUri, short: el.short }); - } - } - - return { - url: sd.url ?? sd.type, - name: sd.name, - isNested, - valueTypes, - slices, - }; -} - -function toCompletion(element: FhirElement): Completion { - const name = fieldName(element); - const types = element.type?.map((t) => t.code).join(" | ") ?? ""; - const kind = snippetKind(element); - - const completion: Completion = { - label: name, - type: "property", - detail: types, - boost: element.min && element.min > 0 ? 2 : 0, - apply: (view, _completion, from, to) => { - const doc = view.state.doc.toString(); - let actualFrom = from; - let actualTo = to; - if (actualFrom > 0 && doc[actualFrom - 1] === '"') actualFrom--; - if (actualTo < doc.length && doc[actualTo] === '"') actualTo++; - - // If replacing an existing property name (colon already follows), - // only replace the name, don't insert a snippet with value - const afterName = doc.slice(actualTo).match(/^\s*:/); - if (afterName) { - const insert = `"${name}"`; - view.dispatch({ - changes: { from: actualFrom, to: actualTo, insert }, - selection: { anchor: actualFrom + insert.length }, - }); - return; - } - - // Detect current indentation - const line = view.state.doc.lineAt(actualFrom); - const lineText = line.text; - const indent = lineText.match(/^(\s*)/)?.[1] ?? ""; - - const { text, cursorOffset } = buildSnippet(name, kind, indent); - - view.dispatch({ - changes: { from: actualFrom, to: actualTo, insert: text }, - selection: { anchor: actualFrom + cursorOffset }, - }); - - // Trigger value autocomplete after inserting a snippet with cursor in value position - if (kind === "string" || kind === "array-primitive" || kind === "array-extension") { - setTimeout(() => startCompletion(view), 0); - } - }, - }; - if (element.short) completion.info = element.short; - return completion; -} - -// ── Completion source ────────────────────────────────────────────────── - -// ── Terminology binding resolution ───────────────────────────────────── - -// Build the FHIR element path for a given JSON path + valueKey -// e.g. resourceType="Patient", path=["address"], valueKey="state" → "Patient.address.state" -// For "code" inside Coding: path=["maritalStatus","coding"], valueKey="code" → walk up to find bound parent -function buildFhirElementPath( - resourceType: string, - path: string[], - valueKey: string, -): string { - return `${resourceType}.${[...path, valueKey].join(".")}`; -} - -// Check if a profile's differential overrides the binding for a given element path -async function findProfileBinding( - profileUrls: string[], - resourceType: string, - path: string[], - valueKey: string, - getSDs: GetStructureDefinitions, -): Promise { - if (profileUrls.length === 0) return null; - - for (const profileUrl of profileUrls) { - const sd = await getCachedSD(profileUrl, getSDs); - if (!sd?.differential?.element) continue; - - // Direct match: e.g. Patient.gender - const directPath = buildFhirElementPath(resourceType, path, valueKey); - for (const el of sd.differential.element) { - if (el.path === directPath && el.binding?.valueSet) { - return el.binding.valueSet; - } - } - - // For "code" inside Coding/CodeableConcept — check parent paths - if (valueKey === "code") { - for (let i = path.length; i > 0; i--) { - const parentFhirPath = buildFhirElementPath(resourceType, path.slice(0, i - 1), path[i - 1]!); - for (const el of sd.differential.element) { - if (el.path === parentFhirPath && el.binding?.valueSet) { - return el.binding.valueSet; - } - } - } - } - } - return null; -} - -// Find binding from extension SD differential for a value inside extension object -async function findExtensionBinding( - doc: string, - pos: number, - getSDs: GetStructureDefinitions, -): Promise { - const textBefore = doc.slice(0, pos); - - // Find the nearest extension "url" in current or parent object - // Match "url": "value" scanning backwards through the text - const urlMatches = [...textBefore.matchAll(/"url"\s*:\s*"([^"]+)"/g)]; - if (urlMatches.length === 0) return null; - - // Work backwards from the most recent url match - for (let i = urlMatches.length - 1; i >= 0; i--) { - const extUrl = urlMatches[i]![1]!; - // Skip non-extension URLs (like profile URLs) - if (!extUrl.includes("StructureDefinition/") && !extUrl.includes("Extension")) { - // Could be a slice name like "ombCategory" — find the parent extension URL - const parentUrlMatches = [...textBefore.slice(0, urlMatches[i]!.index).matchAll(/"url"\s*:\s*"([^"]+)"/g)]; - for (let j = parentUrlMatches.length - 1; j >= 0; j--) { - const parentUrl = parentUrlMatches[j]![1]!; - if (!parentUrl.includes("/")) continue; - const parentSD = await getCachedSD(parentUrl, getSDs); - if (!parentSD?.differential?.element) continue; - // Find the slice matching extUrl and check its value[x] binding - let inSlice = false; - for (const el of parentSD.differential.element) { - if (el.path === "Extension.extension" && el.sliceName) { - const urlEl = parentSD.differential.element.find( - (e) => e.path === "Extension.extension.url" && e.fixedUri && - parentSD.differential!.element.indexOf(e) > parentSD.differential!.element.indexOf(el), - ); - if ((urlEl?.fixedUri ?? el.sliceName) === extUrl) { - inSlice = true; - continue; - } - if (inSlice) break; - } - if (inSlice && el.path === "Extension.extension.value[x]" && el.binding?.valueSet) { - return el.binding.valueSet; - } - } - if (inSlice) break; - } - continue; - } - // Direct extension URL — check its value[x] binding - const sd = await getCachedSD(extUrl, getSDs); - if (!sd?.differential?.element) continue; - for (const el of sd.differential.element) { - if (el.path === "Extension.value[x]" && el.binding?.valueSet) { - return el.binding.valueSet; - } - } - } - return null; -} - -async function findBindingForValue( - path: string[], - valueKey: string, - resourceType: string, - getSDs: GetStructureDefinitions, - profileUrls: string[] = [], - doc?: string, - pos?: number, -): Promise { - // Check extension binding first (for values inside extension objects) - if (doc != null && pos != null) { - // Check if we're inside an extension context (path contains "extension") - const inExtension = path.some((p) => p === "extension" || p === "modifierExtension"); - if (inExtension) { - const extBinding = await findExtensionBinding(doc, pos, getSDs); - if (extBinding) return extBinding; - } - } - - // Check profile overrides first - const profileBinding = await findProfileBinding(profileUrls, resourceType, path, valueKey, getSDs); - if (profileBinding) return profileBinding; - - // Case 1: Direct binding on the field (e.g. code type like Patient.gender) - const elements = await resolveElements(path, resourceType, getSDs); - for (const el of elements) { - if (fieldName(el) === valueKey && el.binding?.valueSet) { - return el.binding.valueSet; - } - } - - // Case 2: valueKey is "code" — walk up to find Coding/CodeableConcept with binding - // Handles: Coding.code, CodeableConcept.coding[].code - if (valueKey === "code") { - for (let i = path.length; i > 0; i--) { - const parentElements = await resolveElements( - path.slice(0, i - 1), - resourceType, - getSDs, - ); - for (const el of parentElements) { - if (fieldName(el) === path[i - 1] && el.binding?.valueSet) { - return el.binding.valueSet; - } - } - } - } - - return null; -} - -// Find canonical targetProfile for an array element (e.g. Meta.profile → StructureDefinition) -// Returns the FHIR resource type name from the targetProfile URL, or null -async function findCanonicalTargetType( - path: string[], - arrayKey: string, - resourceType: string, - getSDs: GetStructureDefinitions, -): Promise { - const elements = await resolveElements(path, resourceType, getSDs); - for (const el of elements) { - if (fieldName(el) !== arrayKey) continue; - if (el.max !== "*") continue; - const t = el.type?.[0]; - if (t?.code !== "canonical" || !t.targetProfile?.length) continue; - // Extract resource type from targetProfile URL - return t.targetProfile[0]?.split("/").pop() ?? null; - } - return null; -} - -// ── Reference target resolution ──────────────────────────────────────── - -async function resolveReferenceTargets( - path: string[], - resourceType: string, - getSDs: GetStructureDefinitions, -): Promise { - // path is the path TO the Reference element (e.g. ["managingOrganization"]) - // We need to find the element at this path and check if its type is Reference - const result = await collectAllElements(resourceType, getSDs); - if (!result) return null; - - let currentPath = resourceType; - let currentElements = result.elements; - - // Walk all segments except the last to resolve the parent context - for (let i = 0; i < path.length - 1; i++) { - const key = path[i]!; - if (key === "resourceType") return null; - - const el = findElement(currentElements, currentPath, key); - if (!el) return null; - - if (el.contentReference) { - currentPath = el.contentReference.replace(/^#/, ""); - continue; - } - if (!el.type?.[0]) return null; - const typeCode = el.type[0].code; - if (typeCode === "BackboneElement") { - currentPath = el.path; - continue; - } - const typeResult = await collectAllElements(typeCode, getSDs); - if (!typeResult) return null; - currentPath = typeResult.basePath; - currentElements = typeResult.elements; - } - - // Now find the last segment — this should be a Reference element - const lastKey = path[path.length - 1]; - if (!lastKey) return null; - - const el = findElement(currentElements, currentPath, lastKey); - if (!el?.type) return null; - - // Collect targetProfile from all Reference types - const targets: string[] = []; - for (const t of el.type) { - if (t.code === "Reference" && t.targetProfile) { - for (const profile of t.targetProfile) { - // Extract resource type from profile URL: "http://hl7.org/fhir/StructureDefinition/Organization" → "Organization" - const rt = profile.split("/").pop(); - if (rt) targets.push(rt); - } - } - } - return targets.length > 0 ? targets : null; -} - -// Check if cursor is in a value position (after "key": or key: ) -function isValuePosition(beforeCursor: string): string | null { - const match = beforeCursor.match(/"?(\w+)"?\s*:\s*"?([^"]*)?$/); - if (match) return match[1] ?? null; - return null; -} - -// Extract meta.profile URLs from JSON document -function getJsonProfileUrls(doc: string): string[] { - const match = doc.match(/"profile"\s*:\s*\[([\s\S]*?)\]/); - if (!match?.[1]) return []; - const urls: string[] = []; - const re = /"([^"]+)"/g; - let m: RegExpExecArray | null; - while ((m = re.exec(match[1])) !== null) { - if (m[1]) urls.push(m[1]); - } - return urls; -} - -// Extract meta.profile URLs from YAML document -function getYamlProfileUrls(doc: string): string[] { - const profileSection = doc.match(/profile:\s*\n((?:\s+-\s+.+\n?)*)/); - if (!profileSection?.[1]) return []; - const urls: string[] = []; - const re = /-\s+['"]?([^'"\n]+)['"]?/g; - let m: RegExpExecArray | null; - while ((m = re.exec(profileSection[1])) !== null) { - if (m[1]) urls.push(m[1].trim()); - } - return urls; -} - -export function fhirCompletionSource( - getSDs: GetStructureDefinitions, - resourceTypeHint?: string, - expandValueSet?: ExpandValueSet, -): CompletionSource { - return async ( - context: CompletionContext, - ): Promise => { - const { state, pos } = context; - const doc = state.doc.toString(); - - const line = state.doc.lineAt(pos); - const beforeCursor = line.text.slice(0, pos - line.from).trimStart(); - - // Check if we're in a value position for resourceType - const valueKey = isValuePosition(beforeCursor); - if (valueKey === "resourceType") { - const sds = await getCachedSDList( - { - derivation: "specialization", - kind: "resource", - _elements: "type", - _count: "500", - }, - getSDs, - ); - const options: Completion[] = sds.map((sd) => ({ - label: sd.type, - type: "type", - apply: (view: EditorView, _c: Completion, from: number, to: number) => { - const d = view.state.doc.toString(); - let actualTo = to; - if (actualTo < d.length && d[actualTo] === '"') actualTo++; - view.dispatch({ - changes: { from, to: actualTo, insert: `${sd.type}"` }, - selection: { anchor: from + sd.type.length + 1 }, - }); - }, - })); - if (options.length === 0) return null; - const word = context.matchBefore(/[\w]*/); - return { from: word?.from ?? pos, options, validFor: /^\w*$/ }; - } - - // Check if we're in a value position for "reference" inside a Reference type - if (valueKey === "reference") { - const path = getJsonPathAtCursor(doc, pos); - const rtMatch = doc.match(/"resourceType"\s*:\s*"([^"]+)"/); - const resourceType = rtMatch?.[1] ?? resourceTypeHint; - if (resourceType && path.length > 0) { - const targets = await resolveReferenceTargets(path, resourceType, getSDs); - if (targets) { - const options: Completion[] = targets.map((rt) => ({ - label: `${rt}/`, - type: "type", - apply: (view: EditorView, _c: Completion, from: number, to: number) => { - view.dispatch({ - changes: { from, to, insert: `${rt}/` }, - selection: { anchor: from + rt.length + 1 }, - }); - }, - })); - const word = context.matchBefore(/[\w/]*/); - return { from: word?.from ?? pos, options, validFor: /^[\w/]*$/ }; - } - } - } - - // Extension URL value completion — "url": "|" inside extension object - if (valueKey === "url") { - const bodyStart = doc.indexOf("\n\n"); - const jsonStart = bodyStart !== -1 ? bodyStart + 2 : 0; - const jsonBody = doc.slice(jsonStart); - const posInBody = pos - jsonStart; - const path = getJsonPathAtCursor(jsonBody, posInBody); - const lastSeg = path[path.length - 1]; - if (lastSeg === "extension" || lastSeg === "modifierExtension") { - const rtMatch = doc.match(/"resourceType"\s*:\s*"([^"]+)"/); - const resourceType = rtMatch?.[1] ?? resourceTypeHint; - // Determine if nested by scanning backwards for parent extension URL - // Find the "extension": [ that contains our cursor, then check if - // the object containing that array has a "url" field - // Find the { opening current extension object by finding "url" key position - // then scanning backwards from there (avoids string-tracking issues) - const urlKeyPos = doc.lastIndexOf('"url"', pos); - let scanEnd = urlKeyPos !== -1 ? urlKeyPos : pos; - // From "url" position, find the opening { - for (let i = scanEnd - 1; i >= 0; i--) { - const c = doc[i]; - if (c === "{") { scanEnd = i; break; } - if (c === "}" || c === "]" || c === "[") break; - } - const textBefore = doc.slice(0, scanEnd); - let parentExtUrl: string | null = null; - let depth = 0; - let inStr = false; - let esc = false; - let foundExtArray = false; - for (let i = textBefore.length - 1; i >= 0; i--) { - const ch = textBefore[i]; - if (esc) { esc = false; continue; } - if (ch === "\\") { esc = true; continue; } - if (ch === '"') { inStr = !inStr; continue; } - if (inStr) continue; - if (ch === "}" || ch === "]") { depth++; } - else if (ch === "[") { - if (depth === 0) { foundExtArray = true; continue; } - depth--; - } else if (ch === "{") { - if (depth === 0 && foundExtArray) { - // Found the parent object of the extension array - // Check if it has "url": "..." inside - const objText = textBefore.slice(i); - const urlMatch = objText.match(/^\{[\s\S]*?"url"\s*:\s*"([^"]+)"/); - if (urlMatch?.[1]?.includes("/")) { - parentExtUrl = urlMatch[1]; - } - break; - } - if (depth === 0) break; - depth--; - } - } - if (parentExtUrl) { - const parentSD = await getCachedSD(parentExtUrl, getSDs); - if (parentSD) { - const info = analyzeExtensionSD(parentSD); - if (info?.slices.length) { - const word = context.matchBefore(/[\w.:/-]*/); - const filter = word?.text.toLowerCase() ?? ""; - const matching = filter - ? info.slices.filter((s) => s.fixedUri.toLowerCase().includes(filter) || (s.short?.toLowerCase().includes(filter) ?? false)) - : info.slices; - const options: Completion[] = matching.map((slice) => { - // Find value type for this slice - const sliceElements = parentSD.differential?.element ?? []; - let sliceValueTypes: string[] = []; - let inSl = false; - for (const el of sliceElements) { - if (el.path === "Extension.extension" && el.sliceName === slice.sliceName) { inSl = true; continue; } - if (inSl && el.path === "Extension.extension.value[x]") { sliceValueTypes = el.type?.map((t) => t.code) ?? []; break; } - if (inSl && el.path === "Extension.extension" && el.sliceName) break; - } - return { - label: slice.fixedUri, - ...(slice.short ? { info: slice.short } : {}), - type: "text", - apply: (view: EditorView, _c: Completion, from: number, to: number) => { - const d = view.state.doc.toString(); - let actualTo = to; - if (actualTo < d.length && d[actualTo] === '"') actualTo++; - view.dispatch({ - changes: { from, to: actualTo, insert: `${slice.fixedUri}"` }, - selection: { anchor: from + slice.fixedUri.length + 1 }, - }); - if (sliceValueTypes.length === 1) { - setTimeout(() => { - const cp = view.state.selection.main.head; - const cd = view.state.doc.toString(); - const af = cd.slice(cp); - if (!/^\s*\n\s*\}/.test(af)) return; - const lo = view.state.doc.lineAt(cp); - const ind = lo.text.match(/^(\s*)/)?.[1] ?? ""; - const tc = sliceValueTypes[0]!; - const vf = `value${tc.charAt(0).toUpperCase()}${tc.slice(1)}`; - let ins: string; - let cOff: number; - if (FHIR_STRING_TYPES.has(tc) || tc === "code") { - ins = `,\n${ind}"${vf}": ""`; - cOff = ins.length - 1; - } else if (FHIR_NUMBER_TYPES.has(tc)) { - ins = `,\n${ind}"${vf}": `; - cOff = ins.length; - } else { - const inner = ind + " "; - ins = `,\n${ind}"${vf}": {\n${inner}\n${ind}}`; - cOff = ins.indexOf(inner + "\n" + ind) + inner.length; - } - view.dispatch({ - changes: { from: cp, insert: ins }, - selection: { anchor: cp + cOff }, - }); - setTimeout(() => startCompletion(view), 0); - }, 10); - } - }, - }; - }); - if (options.length > 0) return { from: word?.from ?? pos, options, filter: false }; - } - } - } else if (resourceType) { - // Top-level or nested-in-type: resolve context - const contextTypes: string[] = [resourceType, "DomainResource", "Resource", "Element"]; - const extIdx = path.lastIndexOf("extension"); - if (extIdx > 0) { - let currentRT = resourceType; - for (let i = 0; i < extIdx; i++) { - const seg = path[i]; - if (!seg) break; - const elements = await resolveElements([], currentRT, getSDs); - const el = elements.find((e) => fieldName(e) === seg); - if (el?.type?.[0]?.code && !isPrimitiveType(el.type[0].code)) currentRT = el.type[0].code; - else break; - } - if (currentRT !== resourceType) { - contextTypes.length = 0; - contextTypes.push(currentRT, "Element", `${resourceType}.${path.slice(0, extIdx).join(".")}`); - } - } - // Profile extensions - const profileExtUrls: string[] = []; - if (contextTypes.includes(resourceType)) { - for (const pUrl of getJsonProfileUrls(doc)) { - const profileSD = await getCachedSD(pUrl, getSDs); - if (!profileSD?.differential?.element) continue; - for (const el of profileSD.differential.element) { - for (const t of el.type ?? []) { - if (t.code === "Extension") { - for (const p of t.profile ?? []) { - const clean = p.includes("|") ? p.slice(0, p.indexOf("|")) : p; - if (!profileExtUrls.includes(clean)) profileExtUrls.push(clean); - } - } - } - } - } - } - const bareWord = context.matchBefore(/[\w.:/-]*/); - const filter = bareWord?.text ?? ""; - const searchParams: StructureDefinitionSearchParams = { type: "Extension", derivation: "constraint", _elements: "url,context", _count: "500" }; - if (filter) searchParams._ilike = filter; - const results = await getCachedSDList(searchParams, getSDs); - // Build context hierarchy for boost scoring - const containerType = contextTypes[0]; // e.g. "Address" or "Patient" - const fhirPath = contextTypes.find((c) => c.includes(".")); // e.g. "Patient.address" - const contextExts = results.filter((sd) => sd.context?.some((c) => c.type === "element" && contextTypes.includes(c.expression))); - const seen = new Set(); - const allExts: { url: string; boost: number }[] = []; - if (contextTypes.includes(resourceType)) { - for (const u of profileExtUrls) { if (!seen.has(u)) { seen.add(u); allExts.push({ url: u, boost: 20 }); } } - } - for (const sd of contextExts) { - const u = sd.url ?? sd.type; - if (seen.has(u)) continue; - seen.add(u); - const ctxExprs = sd.context?.filter((c) => c.type === "element").map((c) => c.expression) ?? []; - let boost = 0; - // Exact FHIR path match (e.g. "Patient.address") — highest - if (fhirPath && ctxExprs.includes(fhirPath)) boost = 15; - // Exact container type match (e.g. "Address") - else if (containerType && ctxExprs.includes(containerType)) boost = 10; - // Resource type match (e.g. "Patient") - else if (ctxExprs.includes(resourceType)) boost = 5; - // DomainResource/Resource - else if (ctxExprs.some((e) => e === "DomainResource" || e === "Resource")) boost = 2; - // Element (generic) - else if (ctxExprs.includes("Element")) boost = 1; - allExts.push({ url: u, boost }); - } - const lf = filter.toLowerCase(); - const filtered = (lf ? allExts.filter((e) => e.url.toLowerCase().includes(lf)) : allExts) - .sort((a, b) => b.boost - a.boost); - if (filtered.length > 0) { - const options: Completion[] = filtered.map((ext) => ({ - label: ext.url, type: "text", boost: ext.boost, - apply: (view: EditorView, _c: Completion, applyFrom: number, applyTo: number) => { - const d = view.state.doc.toString(); - let actualTo = applyTo; - if (actualTo < d.length && d[actualTo] === '"') actualTo++; - view.dispatch({ - changes: { from: applyFrom, to: actualTo, insert: `${ext.url}"` }, - selection: { anchor: applyFrom + ext.url.length + 1 }, - }); - // After inserting URL, fetch SD and append value/extension field - setTimeout(async () => { - const fullSD = await getCachedSD(ext.url, getSDs); - if (!fullSD) return; - const extInfo = analyzeExtensionSD(fullSD); - if (!extInfo) return; - const cursorPos = view.state.selection.main.head; - const curDoc = view.state.doc.toString(); - const after = curDoc.slice(cursorPos); - if (!/^\s*\n\s*\}/.test(after)) return; - const lineObj = view.state.doc.lineAt(cursorPos); - const ind = lineObj.text.match(/^(\s*)/)?.[1] ?? ""; - let ins: string; - let cOff: number; - if (extInfo.isNested) { - const inner = ind + " "; - const innerInner = inner + " "; - ins = `,\n${ind}"extension": [\n${inner}{\n${innerInner}"url": ""\n${inner}}\n${ind}]`; - cOff = ins.lastIndexOf('""') + 1; - } else if (extInfo.valueTypes.length === 1) { - const tc = extInfo.valueTypes[0]!; - const vf = `value${tc.charAt(0).toUpperCase()}${tc.slice(1)}`; - if (FHIR_STRING_TYPES.has(tc) || tc === "code") { - ins = `,\n${ind}"${vf}": ""`; - cOff = ins.length - 1; - } else if (FHIR_NUMBER_TYPES.has(tc)) { - ins = `,\n${ind}"${vf}": `; - cOff = ins.length; - } else { - const inner = ind + " "; - ins = `,\n${ind}"${vf}": {\n${inner}\n${ind}}`; - cOff = ins.indexOf(inner + "\n" + ind) + inner.length; - } - } else { - return; - } - view.dispatch({ - changes: { from: cursorPos, insert: ins }, - selection: { anchor: cursorPos + cOff }, - }); - setTimeout(() => startCompletion(view), 0); - }, 10); - }, - })); - return { from: bareWord?.from ?? pos, options, filter: false }; - } - } - return null; - } - } - - // Terminology binding value completion - if (valueKey && valueKey !== "resourceType" && valueKey !== "reference" && valueKey !== "url" && expandValueSet) { - const path = getJsonPathAtCursor(doc, pos); - const rtMatch = doc.match(/"resourceType"\s*:\s*"([^"]+)"/); - const resourceType = rtMatch?.[1] ?? resourceTypeHint; - if (resourceType) { - const profileUrls = getJsonProfileUrls(doc); - const valueSetUrl = await findBindingForValue(path, valueKey, resourceType, getSDs, profileUrls, doc, pos); - if (valueSetUrl) { - // Include opening quote in match so from < pos (triggers auto-show) - const quoteWord = context.matchBefore(/"[\w-]*/); - const from = quoteWord?.from ?? pos; - const filter = quoteWord ? quoteWord.text.replace(/^"/, "") : ""; - try { - const codes = await expandValueSet(valueSetUrl, filter); - if (codes.length > 0) { - const options: Completion[] = codes.map((c) => ({ - label: c.code, - ...(c.display ? { info: c.display } : {}), - type: "text", - apply: (view: EditorView, _c: Completion, applyFrom: number, applyTo: number) => { - const d = view.state.doc.toString(); - let actualFrom = applyFrom; - let actualTo = applyTo; - if (d[actualFrom] === '"') actualFrom++; - if (actualTo < d.length && d[actualTo] === '"') actualTo++; - view.dispatch({ - changes: { from: actualFrom, to: actualTo, insert: `${c.code}"` }, - selection: { anchor: actualFrom + c.code.length + 1 }, - }); - }, - })); - return { from, options, filter: false }; - } - } catch { - // expand failed — fall through - } - } - } - } - - // Canonical array completion (e.g. meta.profile → StructureDefinition profiles) - // Use regex to detect array context — isInsideJsonArray fails inside "" - { - const textBefore = doc.slice(0, pos); - // Match "key": [ ... with cursor inside the array (possibly inside "") - const arrayMatch = textBefore.match(/"(\w+)"\s*:\s*\[\s*(?:"[^"]*"\s*,\s*)*"?[^"]*$/s); - if (arrayMatch) { - const arrayKey = arrayMatch[1]!; - // Find JSON body start for correct path resolution - const bodyStart = doc.indexOf("\n\n"); - const jsonStart = bodyStart !== -1 ? bodyStart + 2 : 0; - const jsonBody = doc.slice(jsonStart); - const posInBody = pos - jsonStart; - // Get path excluding the array key itself (parent path) - const fullPath = getJsonPathAtCursor(jsonBody, posInBody); - // The array key is the last segment pushed by { before [ - // Remove it to get parentPath - const parentPath = fullPath.length > 0 && fullPath[fullPath.length - 1] === arrayKey - ? fullPath.slice(0, -1) - : fullPath; - const rtMatch = doc.match(/"resourceType"\s*:\s*"([^"]+)"/); - const resourceType = rtMatch?.[1] ?? resourceTypeHint; - if (resourceType) { - const targetType = await findCanonicalTargetType(parentPath, arrayKey, resourceType, getSDs); - if (targetType === "StructureDefinition") { - const allSDs = await getCachedSDList( - { type: `${resourceType},DomainResource,Resource`, derivation: "constraint", _elements: "url,name", _count: "50" }, - getSDs, - ); - const seen = new Set(); - const uniqueSDs = allSDs.filter((sd) => { - const u = sd.url ?? sd.type; - if (seen.has(u)) return false; - seen.add(u); - return true; - }); - if (uniqueSDs.length > 0) { - const quoteWord = context.matchBefore(/"[^"]*/); - const bareWord = context.matchBefore(/[\w.:/-]*/); - const from = quoteWord?.from ?? bareWord?.from ?? pos; - const filter = quoteWord - ? quoteWord.text.replace(/^"/, "").toLowerCase() - : (bareWord?.text.toLowerCase() ?? ""); - const filtered = filter - ? uniqueSDs.filter((sd) => sd.name?.toLowerCase().includes(filter) || sd.url?.toLowerCase().includes(filter)) - : uniqueSDs; - const options: Completion[] = filtered.map((sd) => { - const url = sd.url ?? sd.type; - return { - label: url, - ...(sd.name ? { info: sd.name } : {}), - type: "text", - apply: (view: EditorView, _c: Completion, applyFrom: number, applyTo: number) => { - const d = view.state.doc.toString(); - let actualTo = applyTo; - if (actualTo < d.length && d[actualTo] === '"') actualTo++; - view.dispatch({ - changes: { from: applyFrom, to: actualTo, insert: `"${url}"` }, - selection: { anchor: applyFrom + url.length + 2 }, - }); - }, - }; - }); - if (options.length > 0) { - return { from, options, filter: false }; - } - } - } - if (targetType) return null; - } - } - } - - // Don't offer property completions inside arrays - // Scan backwards to find nearest unmatched [ or { - { - const bodyStart = doc.indexOf("\n\n"); - const jsonStart = bodyStart !== -1 ? bodyStart + 2 : 0; - const jsonBody = doc.slice(jsonStart); - const posInBody = pos - jsonStart; - let depth = 0; - let inStr = false; - let escaped = false; - let insideArray = false; - for (let i = posInBody - 1; i >= 0; i--) { - const ch = jsonBody[i]; - if (escaped) { escaped = false; continue; } - if (ch === "\\") { escaped = true; continue; } - if (ch === '"') { inStr = !inStr; continue; } - if (inStr) continue; - if (ch === "}" || ch === "]") { depth++; } - else if (ch === "{") { if (depth === 0) { insideArray = false; break; } depth--; } - else if (ch === "[") { if (depth === 0) { insideArray = true; break; } depth--; } - } - if (insideArray) return null; - } - - // Property name position — with or without quotes - const isPropertyPosition = - beforeCursor === "" || - beforeCursor === '"' || - /^"?[\w]*$/.test(beforeCursor) || - /[{,]\s*"?[\w]*$/.test(beforeCursor); - - if (!isPropertyPosition) return null; - - // Only auto-trigger property completions when the user has started typing a name - // Don't auto-trigger after a comma without typing (e.g. "Patient", |) - if (!context.explicit && /,\s*"?\s*$/.test(beforeCursor) && !context.matchBefore(/\w+/)) return null; - - const path = getJsonPathAtCursor(doc, pos); - const rtMatch = doc.match(/"resourceType"\s*:\s*"([^"]+)"/) ?? - doc.match(/resourceType\s*:\s*"([^"]+)"/); - const resourceType = rtMatch?.[1] ?? resourceTypeHint; - - const hasExplicitResourceType = !!rtMatch?.[1]; - - let completions: Completion[]; - if (resourceType) { - const elements = await resolveElements(path, resourceType, getSDs); - completions = elementsToCompletions(elements, toCompletion); - if (!hasExplicitResourceType && path.length === 0) { - const rtCompletion: Completion = { - label: "resourceType", - type: "property", - detail: "string", - boost: 10, - apply: (view, _completion, from, to) => { - const d = view.state.doc.toString(); - let actualFrom = from; - let actualTo = to; - if (actualFrom > 0 && d[actualFrom - 1] === '"') actualFrom--; - if (actualTo < d.length && d[actualTo] === '"') actualTo++; - const text = '"resourceType": ""'; - view.dispatch({ - changes: { from: actualFrom, to: actualTo, insert: text }, - selection: { anchor: actualFrom + text.length - 1 }, - }); - }, - }; - rtCompletion.info = "FHIR resource type"; - completions = [rtCompletion, ...completions]; - } - } else if (path.length === 0) { - const rtCompletion: Completion = { - label: "resourceType", - type: "property", - detail: "string", - boost: 10, - apply: (view, _completion, from, to) => { - const d = view.state.doc.toString(); - let actualFrom = from; - let actualTo = to; - if (actualFrom > 0 && d[actualFrom - 1] === '"') actualFrom--; - if (actualTo < d.length && d[actualTo] === '"') actualTo++; - const text = '"resourceType": ""'; - view.dispatch({ - changes: { from: actualFrom, to: actualTo, insert: text }, - selection: { anchor: actualFrom + text.length - 1 }, - }); - }, - }; - rtCompletion.info = "FHIR resource type"; - const domainElements = await resolveElements( - path, - "DomainResource", - getSDs, - ); - completions = [ - rtCompletion, - ...elementsToCompletions(domainElements, toCompletion), - ]; - } else { - return null; - } - - if (completions.length === 0) return null; - - const word = context.matchBefore(/"?\w*/); - let from = word?.from ?? pos; - if (from < doc.length && doc[from] === '"') from++; - - return { from, options: completions, validFor: /^\w*$/ }; - }; -} - -// ── YAML completion helpers ───────────────────────────────────────────── - -function toYamlFieldCompletion(element: FhirElement): Completion { - const name = fieldName(element); - const types = element.type?.map((t) => t.code).join(" | ") ?? ""; - const isArray = element.max === "*"; - const typeCode = element.type?.[0]?.code; - const isPrimitive = typeCode ? isPrimitiveType(typeCode) : false; - - const completion: Completion = { - label: name, - type: "property", - detail: types, - boost: element.min && element.min > 0 ? 2 : 0, - apply: (view, _completion, from, to) => { - const doc = view.state.doc.toString(); - const afterTo = doc.slice(to); - - // If a colon already follows, just replace the property name - if (/^\s*:/.test(afterTo)) { - view.dispatch({ - changes: { from, to, insert: name }, - selection: { anchor: from + name.length }, - }); - return; - } - - const line = view.state.doc.lineAt(from); - const charsBeforeFrom = from - line.from; - const indent = " ".repeat(charsBeforeFrom); - const inner = `${indent} `; - - let text: string; - let cursorOffset: number; - const isString = typeCode ? FHIR_STRING_TYPES.has(typeCode) : false; - if (isArray) { - text = `${name}:\n${inner}- `; - cursorOffset = text.length; - } else if (isString) { - text = `${name}: ''`; - cursorOffset = text.length - 1; - } else if (isPrimitive) { - text = `${name}: `; - cursorOffset = text.length; - } else { - text = `${name}:\n${inner}`; - cursorOffset = text.length; - } - - view.dispatch({ - changes: { from, to, insert: text }, - selection: { anchor: from + cursorOffset }, - }); - - if (isArray || isString) { - setTimeout(() => startCompletion(view), 0); - } - }, - }; - if (element.short) completion.info = element.short; - return completion; -} - -// ── YAML completion source ────────────────────────────────────────────── - -export function yamlFhirCompletionSource( - getSDs: GetStructureDefinitions, - resourceTypeHint?: string, - expandValueSet?: ExpandValueSet, -): CompletionSource { - return async ( - context: CompletionContext, - ): Promise => { - const { state, pos } = context; - const doc = state.doc.toString(); - - const line = state.doc.lineAt(pos); - const beforeCursor = line.text.slice(0, pos - line.from); - - // Value position for resourceType - const valueKey = isYamlValuePosition(beforeCursor); - if (valueKey === "resourceType") { - const sds = await getCachedSDList( - { - derivation: "specialization", - kind: "resource", - _elements: "type", - _count: "500", - }, - getSDs, - ); - const options: Completion[] = sds.map((sd) => ({ - label: sd.type, - type: "type", - })); - if (options.length === 0) return null; - const word = context.matchBefore(/[\w]*/); - return { from: word?.from ?? pos, options, validFor: /^\w*$/ }; - } - - // Value position for "reference" inside a Reference type - if (valueKey === "reference") { - const path = getYamlPathAtCursor(doc, pos); - // path includes keys up to cursor; "reference" is the current key, - // so the parent Reference element is at path (without "reference" in path since - // getYamlPathAtCursor gives parents). We need path + ["reference"] context. - // Actually path gives ancestor keys. The element containing "reference" is at path. - const resourceType = getYamlResourceType(doc) ?? resourceTypeHint; - if (resourceType && path.length > 0) { - // path = ["managingOrganization"] when cursor is on reference value - // We need to find the element at path[-1] from the grandparent - const targets = await resolveReferenceTargets(path, resourceType, getSDs); - if (targets) { - const options: Completion[] = targets.map((rt) => ({ - label: `${rt}/`, - type: "type", - })); - const word = context.matchBefore(/[\w/]*/); - return { from: word?.from ?? pos, options, validFor: /^[\w/]*$/ }; - } - } - } - - // Extension URL value completion in YAML — url: "|" inside extension - if (valueKey === "url") { - const path = getYamlPathAtCursor(doc, pos); - const lastSeg = path[path.length - 1]; - if (lastSeg === "extension" || lastSeg === "modifierExtension") { - const resourceType = getYamlResourceType(doc) ?? resourceTypeHint; - // Check for nested extension — find parent url in YAML - let parentExtUrl: string | null = null; - const urlMatches = [...doc.slice(0, pos).matchAll(/url:\s*['"]?([^\s'"]+)['"]?/g)]; - // If path has multiple extension segments, find parent - const extCount = path.filter((p) => p === "extension" || p === "modifierExtension").length; - if (extCount >= 2) { - for (let i = urlMatches.length - 1; i >= 0; i--) { - const u = urlMatches[i]![1]!; - if (u.includes("/")) { parentExtUrl = u; break; } - } - } - if (parentExtUrl) { - const parentSD = await getCachedSD(parentExtUrl, getSDs); - if (parentSD) { - const info = analyzeExtensionSD(parentSD); - if (info?.slices.length) { - const word = context.matchBefore(/[\w.:/-]*/); - const filter = word?.text.toLowerCase() ?? ""; - const matching = filter - ? info.slices.filter((s) => s.fixedUri.toLowerCase().includes(filter) || (s.short?.toLowerCase().includes(filter) ?? false)) - : info.slices; - const options: Completion[] = matching.map((slice) => ({ - label: slice.fixedUri, - ...(slice.short ? { info: slice.short } : {}), - type: "text", - })); - if (options.length > 0) return { from: word?.from ?? pos, options, filter: false }; - } - } - } else if (resourceType) { - const contextTypes: string[] = [resourceType, "DomainResource", "Resource", "Element"]; - // Resolve container type from path - const extIdx = path.lastIndexOf("extension"); - if (extIdx > 0) { - let currentRT = resourceType; - for (let i = 0; i < extIdx; i++) { - const seg = path[i]; - if (!seg) break; - const elements = await resolveElements([], currentRT, getSDs); - const el = elements.find((e) => fieldName(e) === seg); - if (el?.type?.[0]?.code && !isPrimitiveType(el.type[0].code)) currentRT = el.type[0].code; - else break; - } - if (currentRT !== resourceType) { - contextTypes.length = 0; - contextTypes.push(currentRT, "Element", `${resourceType}.${path.slice(0, extIdx).join(".")}`); - } - } - // Profile extensions - const profileExtUrls: string[] = []; - if (contextTypes.includes(resourceType)) { - for (const pUrl of getYamlProfileUrls(doc)) { - const profileSD = await getCachedSD(pUrl, getSDs); - if (!profileSD?.differential?.element) continue; - for (const el of profileSD.differential.element) { - for (const t of el.type ?? []) { - if (t.code === "Extension") { - for (const p of t.profile ?? []) { - const clean = p.includes("|") ? p.slice(0, p.indexOf("|")) : p; - if (!profileExtUrls.includes(clean)) profileExtUrls.push(clean); - } - } - } - } - } - } - const bareWord = context.matchBefore(/[\w.:/-]*/); - const filter = bareWord?.text ?? ""; - const searchParams: StructureDefinitionSearchParams = { type: "Extension", derivation: "constraint", _elements: "url,context", _count: "500" }; - if (filter) searchParams._ilike = filter; - const results = await getCachedSDList(searchParams, getSDs); - const containerType = contextTypes[0]; - const fhirPath = contextTypes.find((c) => c.includes(".")); - const contextExts = results.filter((sd) => sd.context?.some((c) => c.type === "element" && contextTypes.includes(c.expression))); - const seen = new Set(); - const allExts: { url: string; boost: number }[] = []; - if (contextTypes.includes(resourceType)) { - for (const u of profileExtUrls) { if (!seen.has(u)) { seen.add(u); allExts.push({ url: u, boost: 20 }); } } - } - for (const sd of contextExts) { - const u = sd.url ?? sd.type; - if (seen.has(u)) continue; - seen.add(u); - const ctxExprs = sd.context?.filter((c) => c.type === "element").map((c) => c.expression) ?? []; - let boost = 0; - if (fhirPath && ctxExprs.includes(fhirPath)) boost = 15; - else if (containerType && ctxExprs.includes(containerType)) boost = 10; - else if (ctxExprs.includes(resourceType)) boost = 5; - else if (ctxExprs.some((e) => e === "DomainResource" || e === "Resource")) boost = 2; - else if (ctxExprs.includes("Element")) boost = 1; - allExts.push({ url: u, boost }); - } - const lf = filter.toLowerCase(); - const sorted = (lf ? allExts.filter((e) => e.url.toLowerCase().includes(lf)) : allExts).sort((a, b) => b.boost - a.boost); - if (sorted.length > 0) { - const options: Completion[] = sorted.map((ext) => ({ label: ext.url, type: "text", boost: ext.boost })); - return { from: bareWord?.from ?? pos, options, filter: false }; - } - } - return null; - } - } - - // Terminology binding value completion - if (valueKey && valueKey !== "resourceType" && valueKey !== "reference" && valueKey !== "url" && expandValueSet) { - const path = getYamlPathAtCursor(doc, pos); - const resourceType = getYamlResourceType(doc) ?? resourceTypeHint; - if (resourceType) { - const profileUrls = getYamlProfileUrls(doc); - const valueSetUrl = await findBindingForValue(path, valueKey, resourceType, getSDs, profileUrls, doc, pos); - if (valueSetUrl) { - const word = context.matchBefore(/[\w-]*/); - const filter = word ? doc.slice(word.from, pos) : ""; - try { - const codes = await expandValueSet(valueSetUrl, filter); - if (codes.length > 0) { - const options: Completion[] = codes.map((c) => ({ - label: c.code, - ...(c.display ? { info: c.display } : {}), - type: "text", - })); - return { from: word?.from ?? pos, options, validFor: /^[\w-]*$/ }; - } - } catch { - // expand failed — fall through - } - } - } - } - - // Canonical array completion in YAML (e.g. meta.profile → StructureDefinition profiles) - { - const trimmed = beforeCursor.trimStart(); - const isArrayItem = trimmed === "-" || trimmed === "- " || trimmed.startsWith("- "); - if (isArrayItem) { - const path = getYamlPathAtCursor(doc, pos); - const arrayKey = path[path.length - 1]; - if (path.length > 0 && arrayKey) { - const parentPath = path.slice(0, -1); - const resourceType = getYamlResourceType(doc) ?? resourceTypeHint; - if (resourceType) { - const targetType = await findCanonicalTargetType(parentPath, arrayKey, resourceType, getSDs); - if (targetType === "StructureDefinition") { - const allSDs = await getCachedSDList( - { type: `${resourceType},DomainResource,Resource`, derivation: "constraint", _elements: "url,name", _count: "50" }, - getSDs, - ); - const seen = new Set(); - const uniqueSDs = allSDs.filter((sd) => { - const u = sd.url ?? sd.type; - if (seen.has(u)) return false; - seen.add(u); - return true; - }); - if (uniqueSDs.length > 0) { - const word = context.matchBefore(/[\w.:/-]*/); - const from = word?.from ?? pos; - const filter = word?.text.toLowerCase() ?? ""; - const filtered = filter - ? uniqueSDs.filter((sd) => sd.name?.toLowerCase().includes(filter) || sd.url?.toLowerCase().includes(filter)) - : uniqueSDs; - const options: Completion[] = filtered.map((sd) => ({ - label: sd.url ?? sd.type, - ...(sd.name ? { info: sd.name } : {}), - type: "text", - })); - if (options.length > 0) { - return { from, options, filter: false }; - } - } - } - if (targetType) return null; - } - } - } - } - - if (!isYamlPropertyPosition(beforeCursor)) return null; - - // Only auto-trigger property completions when the user has started typing a name - // Don't auto-trigger after a comma without typing (e.g. "Patient", |) - if (!context.explicit && /,\s*"?\s*$/.test(beforeCursor) && !context.matchBefore(/\w+/)) return null; - - const path = getYamlPathAtCursor(doc, pos); - const hasExplicitResourceType = !!getYamlResourceType(doc); - const resourceType = getYamlResourceType(doc) ?? resourceTypeHint; - - let completions: Completion[]; - if (resourceType) { - const elements = await resolveElements(path, resourceType, getSDs); - completions = elementsToCompletions(elements, toYamlFieldCompletion); - if (!hasExplicitResourceType && path.length === 0) { - const rtCompletion: Completion = { - label: "resourceType", - type: "property", - detail: "string", - boost: 10, - apply: "resourceType: ", - }; - rtCompletion.info = "FHIR resource type"; - completions = [rtCompletion, ...completions]; - } - } else if (path.length === 0) { - const rtCompletion: Completion = { - label: "resourceType", - type: "property", - detail: "string", - boost: 10, - apply: "resourceType: ", - }; - rtCompletion.info = "FHIR resource type"; - const domainElements = await resolveElements( - path, - "DomainResource", - getSDs, - ); - completions = [ - rtCompletion, - ...elementsToCompletions(domainElements, toYamlFieldCompletion), - ]; - } else { - return null; - } - - if (completions.length === 0) return null; - - // Strip "- " prefix for matching - const word = context.matchBefore(/[\w]*/); - - return { from: word?.from ?? pos, options: completions, validFor: /^\w*$/ }; - }; -} - -// ── YAML FHIR linter ────────────────────────────────────────────────── - -const HTTP_METHOD_RE = /^(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s/; - -function findRootYamlDocument(doc: string): { start: number } | null { - const firstLine = doc.slice(0, doc.indexOf("\n") >>> 0).trimStart(); - const isHttpMode = HTTP_METHOD_RE.test(firstLine); - - if (isHttpMode) { - // HTTP mode: body starts after blank line - const bodyStart = doc.indexOf("\n\n"); - if (bodyStart === -1) return null; - const start = bodyStart + 2; - if (start >= doc.length) return null; - const bodyContent = doc.slice(start).trimStart(); - if (!bodyContent) return null; - // Check that what follows isn't JSON - if (bodyContent.startsWith("{") || bodyContent.startsWith("[")) return null; - return { start }; - } - - // Pure YAML: check first non-whitespace isn't JSON - const firstNonWs = doc.trimStart(); - if (firstNonWs.startsWith("{") || firstNonWs.startsWith("[")) return null; - return { start: 0 }; -} - -function walkYamlObject( - text: string, - startOffset: number, - parentPath: string[], - parentResourceType: string | null, - result: PropertyInfo[], - emptyStrings?: EmptyStringInfo[], -): void { - const yamlText = text.slice(startOffset); - const yamlLines = yamlText.split("\n"); - - // Detect resourceType - let ownResourceType: string | null = null; - for (const line of yamlLines) { - const trimmed = line.trimStart(); - if (!trimmed || trimmed.startsWith("#")) continue; - const m = trimmed.match(/^resourceType:\s*(\S+)/); - if (m) { - ownResourceType = m[1] ?? null; - break; - } - // Only check top-level lines (indent 0) - if (line.search(/\S/) === 0 && !m) continue; - if (line.search(/\S/) > 0) continue; - } - - const resourceType = ownResourceType ?? parentResourceType; - const basePath = ownResourceType ? [] : parentPath; - if (!resourceType) return; - - const stack: { indent: number; path: string[]; arrayChildIndent: number | null }[] = [ - { indent: -1, path: basePath, arrayChildIndent: null }, - ]; - - for (let i = 0; i < yamlLines.length; i++) { - const line = yamlLines[i]!; - const trimmed = line.trimStart(); - if (!trimmed || trimmed.startsWith("#")) continue; - - const indent = line.length - trimmed.length; - const isArrayItem = trimmed.startsWith("- "); - const content = isArrayItem ? trimmed.slice(2) : trimmed; - // Skip array scalar values (quoted strings, URLs, or bare values) - if (isArrayItem && (content.startsWith("'") || content.startsWith('"') || !content.includes(": "))) continue; - const colonIdx = content.indexOf(": "); - if (colonIdx <= 0) continue; - - const key = content.slice(0, colonIdx).trim(); - const valueAfterColon = content.slice(colonIdx + 2).trim(); - - // Pop stack to find parent - while (stack.length > 1 && stack[stack.length - 1]!.indent >= indent) { - stack.pop(); - } - let parentEntry = stack[stack.length - 1]!; - - // Track where array items appear under this parent - if (isArrayItem && parentEntry.arrayChildIndent === null) { - parentEntry.arrayChildIndent = indent; - } - - // If parent has array children and this non-array line is at the array item - // level (not deeper inside an item), it's invalid YAML — treat as grandparent's child. - if ( - !isArrayItem && - parentEntry.arrayChildIndent !== null && - indent <= parentEntry.arrayChildIndent && - stack.length > 1 - ) { - stack.pop(); - parentEntry = stack[stack.length - 1]!; - } - - // Calculate character offset for this key - const keyIndent = isArrayItem ? indent + 2 : indent; - let charOffset = startOffset; - for (let j = 0; j < i; j++) { - charOffset += yamlLines[j]!.length + 1; - } - const keyFrom = charOffset + keyIndent; - const keyTo = keyFrom + key.length; - - result.push({ - name: key, - path: [...parentEntry.path], - resourceType, - from: keyFrom, - to: keyTo, - }); - - // Check for empty strings - if (emptyStrings && (valueAfterColon === "''" || valueAfterColon === '""')) { - const afterColonStr = content.slice(colonIdx + 2); - const wsLen = afterColonStr.length - afterColonStr.trimStart().length; - const emptyFrom = charOffset + keyIndent + colonIdx + 2 + wsLen; - const emptyTo = emptyFrom + 2; - emptyStrings.push({ from: emptyFrom, to: emptyTo }); - } - - // Push to stack if this key has nested content (no inline value, or value is empty) - // For array items (- key:), use effective indent (indent + 2) so that - // sibling properties at the same level correctly pop this entry. - if (!valueAfterColon || valueAfterColon === "" || valueAfterColon.startsWith("#")) { - const effectiveIndent = isArrayItem ? indent + 2 : indent; - stack.push({ indent: effectiveIndent, path: [...parentEntry.path, key], arrayChildIndent: null }); - } - } -} - -// ── JSON FHIR linter ────────────────────────────────────────────────── - -type PropertyInfo = { - name: string; - path: string[]; - resourceType: string; - from: number; - to: number; -}; - -type EmptyStringInfo = { - from: number; - to: number; -}; - -function walkJsonObject( - node: SyntaxNode, - parentPath: string[], - parentResourceType: string | null, - doc: string, - result: PropertyInfo[], - emptyStrings?: EmptyStringInfo[], -): void { - // Detect if this object declares its own resourceType - let ownResourceType: string | null = null; - for (let child = node.firstChild; child; child = child.nextSibling) { - if (child.name !== "Property") continue; - const nameNode = child.getChild("PropertyName"); - if (!nameNode) continue; - const keyName = doc - .slice(nameNode.from, nameNode.to) - .replace(/^"|"$/g, ""); - if (keyName === "resourceType") { - for (let v = child.firstChild; v; v = v.nextSibling) { - if (v.name === "String") { - ownResourceType = doc - .slice(v.from, v.to) - .replace(/^"|"$/g, ""); - break; - } - } - break; - } - } - - const resourceType = ownResourceType ?? parentResourceType; - const path = ownResourceType ? [] : parentPath; - - if (!resourceType) return; - - for (let child = node.firstChild; child; child = child.nextSibling) { - if (child.name !== "Property") continue; - const nameNode = child.getChild("PropertyName"); - if (!nameNode) continue; - const name = doc - .slice(nameNode.from, nameNode.to) - .replace(/^"|"$/g, ""); - - result.push({ - name, - path: [...path], - resourceType, - from: nameNode.from, - to: nameNode.to, - }); - - for (let v = child.firstChild; v; v = v.nextSibling) { - if (v.name === "Object") { - walkJsonObject( - v, - [...path, name], - resourceType, - doc, - result, - emptyStrings, - ); - } else if (v.name === "Array") { - for ( - let item = v.firstChild; - item; - item = item.nextSibling - ) { - if (item.name === "Object") { - walkJsonObject( - item, - [...path, name], - resourceType, - doc, - result, - emptyStrings, - ); - } - } - } else if (v.name === "String" && emptyStrings) { - const raw = doc.slice(v.from, v.to); - if (raw === '""') { - emptyStrings.push({ from: v.from, to: v.to }); - } - } - } - } -} - -type FhirDiagnostic = { - from: number; - to: number; - message: string; -}; - -async function validateFhirProperties( - properties: PropertyInfo[], - getSDs: GetStructureDefinitions, -): Promise { - const groups = new Map< - string, - { resourceType: string; path: string[]; props: PropertyInfo[] } - >(); - for (const prop of properties) { - const key = `${prop.resourceType}|${prop.path.join(".")}`; - let group = groups.get(key); - if (!group) { - group = { - resourceType: prop.resourceType, - path: [...prop.path], - props: [], - }; - groups.set(key, group); - } - group.props.push(prop); - } - - const diagnostics: FhirDiagnostic[] = []; - - for (const { resourceType, path, props } of groups.values()) { - const elements = await resolveElements(path, resourceType, getSDs); - if (elements.length === 0) continue; - - const validNames = new Set(); - for (const el of elements) { - const name = fieldName(el); - validNames.add(name); - const typeCode = el.type?.[0]?.code; - if ( - el.type?.length === 1 && - typeCode && - isPrimitiveType(typeCode) - ) { - validNames.add(`_${name}`); - } - } - if (path.length === 0) { - validNames.add("resourceType"); - } - - for (const prop of props) { - if (!validNames.has(prop.name)) { - diagnostics.push({ - from: prop.from, - to: prop.to, - message: `Unknown property "${prop.name}"`, - }); - } - } - } - - return diagnostics; -} - -function findRootJsonObject( - doc: string, - tree: ReturnType, -): SyntaxNode | null { - // Pure JSON mode: top node is JsonText with Object child - const direct = tree.topNode.getChild("Object"); - if (direct) return direct; - - // HTTP mode (mixed parsing): find body after blank line, - // then resolve into the mounted JSON subtree - const bodyStart = doc.indexOf("\n\n"); - if (bodyStart === -1) return null; - - const jsonStart = bodyStart + 2; - if (jsonStart >= doc.length) return null; - - // resolveInner enters mounted (mixed-parsed) subtrees - const innerNode = tree.resolveInner(jsonStart, 1); - if (!innerNode) return null; - - // Walk up to find the Object node - let node: SyntaxNode | null = innerNode; - while (node) { - if (node.name === "Object") return node; - if (node.name === "JsonText") { - return node.getChild("Object"); - } - node = node.parent; - } - - return null; -} - -// ── FHIR validation decorations ─────────────────────────────────────── - -type FhirDiagnosticWithLine = FhirDiagnostic & { line: number }; - -const setFhirDiagnosticsEffect = StateEffect.define(); - -const fhirUnderline = Decoration.mark({ class: "cm-fhir-error-underline" }); -const fhirErrorLineDecoration = Decoration.line({ class: "cm-errorLine" }); - -class FhirGutterMarker extends GutterMarker { - elementClass = "cm-errorLineGutter"; -} -const fhirGutterMarker = new FhirGutterMarker(); - -export const fhirDiagnosticsField = StateField.define<{ - marks: RangeSet; - lineDecos: RangeSet; - gutterMarkers: RangeSet; - messages: Map; -}>({ - create() { - return { - marks: Decoration.none, - lineDecos: Decoration.none, - gutterMarkers: RangeSet.empty, - messages: new Map(), - }; - }, - update(value, tr) { - for (const effect of tr.effects) { - if (effect.is(setFhirDiagnosticsEffect)) { - const diags = effect.value; - if (diags.length === 0) { - return { - marks: Decoration.none, - lineDecos: Decoration.none, - gutterMarkers: RangeSet.empty, - messages: new Map(), - }; - } - - const marks: { from: number; to: number; value: Decoration }[] = - []; - const lineDecos: { - from: number; - to: number; - value: Decoration; - }[] = []; - const gutter: { - from: number; - to: number; - value: GutterMarker; - }[] = []; - const messages = new Map(); - - for (const d of diags) { - marks.push(fhirUnderline.range(d.from, d.to)); - const existing = messages.get(d.line); - if (existing) { - messages.set(d.line, `${existing}\n${d.message}`); - } else { - messages.set(d.line, d.message); - const line = tr.state.doc.line(d.line); - lineDecos.push( - fhirErrorLineDecoration.range(line.from), - ); - gutter.push(fhirGutterMarker.range(line.from)); - } - } - - return { - marks: Decoration.set(marks, true), - lineDecos: Decoration.set(lineDecos, true), - gutterMarkers: RangeSet.of(gutter, true), - messages, - }; - } - } - if (tr.docChanged) { - try { - return { - marks: value.marks.map(tr.changes), - lineDecos: value.lineDecos.map(tr.changes), - gutterMarkers: value.gutterMarkers.map(tr.changes), - messages: value.messages, - }; - } catch { - return { - marks: Decoration.none, - lineDecos: Decoration.none, - gutterMarkers: RangeSet.empty, - messages: new Map(), - }; - } - } - return value; - }, - provide(field) { - return [ - EditorView.decorations.from(field, (v) => v.marks), - EditorView.decorations.from(field, (v) => v.lineDecos), - gutterLineClass.from(field, (v) => v.gutterMarkers), - ]; - }, -}); - -const fhirLinterTheme = EditorView.theme({ - ".cm-fhir-error-underline": { - textDecorationLine: "underline", - textDecorationStyle: "wavy", - textDecorationColor: "var(--color-text-error-primary)", - textUnderlineOffset: "3px", - }, - ".cm-lineNumbers .cm-gutterElement.cm-errorLineGutter": { - color: "var(--color-text-error-primary)", - backgroundColor: - "color-mix(in srgb, var(--color-text-error-primary) 7%, transparent)", - }, -}); - -function buildFhirValidationPlugin( - getSDs: GetStructureDefinitions, - resourceTypeHint?: string, -): Extension { - return ViewPlugin.define((view) => { - let timeout: ReturnType | null = null; - let destroyed = false; - - function hasActiveDiagnostics() { - try { - return view.state.field(fhirDiagnosticsField).messages.size > 0; - } catch { - return false; - } - } - - function scheduleCheck() { - if (timeout) clearTimeout(timeout); - const delay = hasActiveDiagnostics() ? 0 : 1500; - timeout = setTimeout(() => check(), delay); - } - - async function check() { - if (destroyed) return; - const currentDoc = view.state.doc.toString(); - // Ensure syntax tree is fully parsed before checking - const tree = - ensureSyntaxTree(view.state, view.state.doc.length, 1000) ?? - syntaxTree(view.state); - - const properties: PropertyInfo[] = []; - const emptyStrings: EmptyStringInfo[] = []; - - // Try JSON first - const rootObj = findRootJsonObject(currentDoc, tree); - if (rootObj) { - walkJsonObject(rootObj, [], resourceTypeHint ?? null, currentDoc, properties, emptyStrings); - } else { - // Try YAML - const yamlDoc = findRootYamlDocument(currentDoc); - if (yamlDoc) { - walkYamlObject(currentDoc, yamlDoc.start, [], resourceTypeHint ?? null, properties, emptyStrings); - } - } - - if (!rootObj && !findRootYamlDocument(currentDoc)) { - try { - view.dispatch({ - effects: setFhirDiagnosticsEffect.of([]), - }); - } catch { - /* view destroyed */ - } - return; - } - - if (properties.length === 0 && emptyStrings.length === 0) { - try { - view.dispatch({ - effects: setFhirDiagnosticsEffect.of([]), - }); - } catch { - /* view destroyed */ - } - return; - } - - const rawDiags = await validateFhirProperties( - properties, - getSDs, - ); - if (destroyed) return; - if (view.state.doc.toString() !== currentDoc) return; - - for (const es of emptyStrings) { - rawDiags.push({ - from: es.from, - to: es.to, - message: "Value must not be empty", - }); - } - - const diags: FhirDiagnosticWithLine[] = rawDiags.map((d) => ({ - ...d, - line: view.state.doc.lineAt(d.from).number, - })); - - try { - view.dispatch({ - effects: setFhirDiagnosticsEffect.of(diags), - }); - } catch { - /* view destroyed */ - } - } - - scheduleCheck(); - - return { - update(update: ViewUpdate) { - if (update.docChanged) { - scheduleCheck(); - } - }, - destroy() { - destroyed = true; - if (timeout) clearTimeout(timeout); - }, - }; - }); -} - -// ── Public API ───────────────────────────────────────────────────────── - -export function buildFhirCompletionExtension( - getSDs: GetStructureDefinitions, - resourceTypeHint?: string, - expandValueSet?: ExpandValueSet, -): Extension { - const jsonSource = fhirCompletionSource(getSDs, resourceTypeHint, expandValueSet); - const yamlSource = yamlFhirCompletionSource(getSDs, resourceTypeHint, expandValueSet); - // Trigger completion on empty lines inside objects (where from === pos - // would cause CodeMirror to suppress auto-triggered results) - const autoTrigger = EditorView.updateListener.of((update) => { - if (!update.docChanged) return; - if (completionStatus(update.view.state)) return; - // Ignore bulk replacements (e.g. tab switch, currentValue update) - let changeSize = 0; - update.changes.iterChanges((_fA, _tA, _fB, _tB, ins) => { changeSize += ins.length; }); - if (changeSize > 50) return; - const { state } = update.view; - const pos = state.selection.main.head; - const doc = state.doc.toString(); - const line = state.doc.lineAt(pos); - const beforeCursor = line.text.slice(0, pos - line.from).trimStart(); - // Empty line, inside [] or inside "" - const shouldTrigger = - beforeCursor === "" || - (pos > 0 && doc[pos - 1] === "[") || - (pos > 0 && doc[pos - 1] === '"' && pos > 1 && doc[pos - 2] !== "\\"); - if (!shouldTrigger) return; - setTimeout(() => startCompletion(update.view), 0); - }); - - return [ - jsonLanguage.data.of({ autocomplete: jsonSource }), - yamlLanguage.data.of({ autocomplete: yamlSource }), - autoTrigger, - fhirDiagnosticsField, - fhirLinterTheme, - buildFhirValidationPlugin(getSDs, resourceTypeHint), - ]; -} diff --git a/packages/react-components/src/components/code-editor/index.tsx b/packages/react-components/src/components/code-editor/index.tsx index 914ff697..cac56143 100644 --- a/packages/react-components/src/components/code-editor/index.tsx +++ b/packages/react-components/src/components/code-editor/index.tsx @@ -82,7 +82,7 @@ import { type ExpandValueSet, fhirDiagnosticsField, type GetStructureDefinitions, -} from "./fhir-completion"; +} from "./fhir-autocomplete"; import { type GetUrlSuggestions, http } from "./http"; import { buildSqlCompletionExtensions, @@ -1057,7 +1057,7 @@ type CodeEditorProps = { export type CodeEditorView = EditorView; -export type { ExpandValueSet, GetStructureDefinitions } from "./fhir-completion"; +export type { ExpandValueSet, GetStructureDefinitions } from "./fhir-autocomplete"; export type { GetUrlSuggestions } from "./http"; export type { SqlConfig, diff --git a/packages/react-components/src/components/code-editor/json-ast.test.ts b/packages/react-components/src/components/code-editor/json-ast.test.ts new file mode 100644 index 00000000..85869658 --- /dev/null +++ b/packages/react-components/src/components/code-editor/json-ast.test.ts @@ -0,0 +1,222 @@ +import { describe, expect, it } from "vitest"; +import { buildJsonDocumentContext } from "./json-ast"; + +// Helpers +const at = (doc: string, marker = "|") => { + const pos = doc.indexOf(marker); + return { doc: doc.slice(0, pos) + doc.slice(pos + 1), pos }; +}; + +describe("buildJsonDocumentContext", () => { + describe("fullPath", () => { + it("empty object root", () => { + const { doc, pos } = at("{\n |\n}"); + const ctx = buildJsonDocumentContext(doc, pos); + expect(ctx.fullPath).toEqual([]); + }); + + it("one level deep", () => { + const { doc, pos } = at('{\n "name": [\n {\n |\n }\n ]\n}'); + const ctx = buildJsonDocumentContext(doc, pos); + expect(ctx.fullPath).toEqual(["name"]); + }); + + it("two levels deep", () => { + const { doc, pos } = at( + '{\n "address": [\n {\n "period": {\n |\n }\n }\n ]\n}', + ); + const ctx = buildJsonDocumentContext(doc, pos); + expect(ctx.fullPath).toEqual(["address", "period"]); + }); + + it("second object in array has correct path", () => { + const { doc, pos } = at( + '{\n "parameter": [\n {"name": "a"},\n {\n |\n }\n ]\n}', + ); + const ctx = buildJsonDocumentContext(doc, pos); + expect(ctx.fullPath).toEqual(["parameter"]); + }); + + it("third object in array has correct path", () => { + const { doc, pos } = at( + '{\n "parameter": [\n {"name": "a"},\n {"name": "b"},\n {\n |\n }\n ]\n}', + ); + const ctx = buildJsonDocumentContext(doc, pos); + expect(ctx.fullPath).toEqual(["parameter"]); + }); + + it("nested array second item", () => { + const { doc, pos } = at( + '{\n "entry": [\n {"resource": {}},\n {\n "resource": {\n |\n }\n }\n ]\n}', + ); + const ctx = buildJsonDocumentContext(doc, pos); + expect(ctx.fullPath).toEqual(["entry", "resource"]); + }); + + it("inside contained resource", () => { + const { doc, pos } = at( + '{\n "resourceType": "Bundle",\n "entry": [\n {\n "resource": {\n "resourceType": "Patient",\n |\n }\n }\n ]\n}', + ); + const ctx = buildJsonDocumentContext(doc, pos); + expect(ctx.fullPath).toEqual(["entry", "resource"]); + }); + }); + + describe("fullPath in HTTP mode", () => { + it("skips HTTP header", () => { + const { doc, pos } = at( + "POST /fhir/Patient\nContent-Type: application/json\n\n{\n |\n}", + ); + const ctx = buildJsonDocumentContext(doc, pos); + expect(ctx.fullPath).toEqual([]); + }); + + it("nested path in HTTP mode", () => { + const { doc, pos } = at( + 'PUT /fhir/Patient/1\n\n{\n "name": [\n {\n |\n }\n ]\n}', + ); + const ctx = buildJsonDocumentContext(doc, pos); + expect(ctx.fullPath).toEqual(["name"]); + }); + }); + + describe("cursorPosition", () => { + it("property on empty line", () => { + const { doc, pos } = at("{\n |\n}"); + const ctx = buildJsonDocumentContext(doc, pos); + expect(ctx.cursorPosition.kind).toBe("property"); + }); + + it("property with partial word", () => { + const { doc, pos } = at('{\n "nam|\n}'); + const ctx = buildJsonDocumentContext(doc, pos); + expect(ctx.cursorPosition.kind).toBe("property"); + }); + + it("value after colon with string", () => { + const { doc, pos } = at('{\n "gender": "|\n}'); + const ctx = buildJsonDocumentContext(doc, pos); + expect(ctx.cursorPosition).toEqual({ + kind: "value", + key: "gender", + prefix: "", + }); + }); + + it("value with partial text", () => { + const { doc, pos } = at('{\n "gender": "mal|\n}'); + const ctx = buildJsonDocumentContext(doc, pos); + expect(ctx.cursorPosition).toEqual({ + kind: "value", + key: "gender", + prefix: "mal", + }); + }); + + it("value for resourceType", () => { + const { doc, pos } = at('{\n "resourceType": "|\n}'); + const ctx = buildJsonDocumentContext(doc, pos); + expect(ctx.cursorPosition).toEqual({ + kind: "value", + key: "resourceType", + prefix: "", + }); + }); + + it("array-item inside canonical array", () => { + const { doc, pos } = at('{\n "profile": [\n "|\n ]\n}'); + const ctx = buildJsonDocumentContext(doc, pos); + expect(ctx.cursorPosition.kind).toBe("array-item"); + if (ctx.cursorPosition.kind === "array-item") { + expect(ctx.cursorPosition.parentKey).toBe("profile"); + } + }); + + it("property after comma in object", () => { + const { doc, pos } = at('{\n "a": 1,\n |\n}'); + const ctx = buildJsonDocumentContext(doc, pos); + expect(ctx.cursorPosition.kind).toBe("property"); + }); + }); + + describe("isInsideArray", () => { + it("false at object root", () => { + const { doc, pos } = at("{\n |\n}"); + const ctx = buildJsonDocumentContext(doc, pos); + expect(ctx.isInsideArray()).toBe(false); + }); + + it("true inside array", () => { + const { doc, pos } = at('{\n "name": [\n |\n ]\n}'); + const ctx = buildJsonDocumentContext(doc, pos); + expect(ctx.isInsideArray()).toBe(true); + }); + + it("false inside object inside array", () => { + const { doc, pos } = at('{\n "name": [\n {\n |\n }\n ]\n}'); + const ctx = buildJsonDocumentContext(doc, pos); + expect(ctx.isInsideArray()).toBe(false); + }); + }); + + describe("getScope", () => { + it("getString finds resourceType at level 0", () => { + const { doc, pos } = at('{\n "resourceType": "Patient",\n |\n}'); + const ctx = buildJsonDocumentContext(doc, pos); + expect(ctx.getScope(0).getString("resourceType")).toBe("Patient"); + }); + + it("getString finds resourceType in parent object (levelsUp=1)", () => { + const { doc, pos } = at( + '{\n "resourceType": "Patient",\n "name": [\n {\n |\n }\n ]\n}', + ); + const ctx = buildJsonDocumentContext(doc, pos); + // level 0 is the inner {} (no resourceType) + expect(ctx.getScope(0).getString("resourceType")).toBe(null); + // level 1 is the outer {} with "Patient" + expect(ctx.getScope(1).getString("resourceType")).toBe("Patient"); + }); + + it("getString finds inner resourceType for contained", () => { + const doc = [ + "{", + ' "resourceType": "Bundle",', + ' "entry": [', + " {", + ' "resource": {', + ' "resourceType": "Patient",', + ' "name": [', + " {", + " |", + " }", + " ]", + " }", + " }", + " ]", + "}", + ].join("\n"); + const { doc: d, pos } = at(doc); + const ctx = buildJsonDocumentContext(d, pos); + // level 0 = inner { } (name array item) + expect(ctx.getScope(0).getString("resourceType")).toBe(null); + // level 1 = resource { "resourceType": "Patient" } + expect(ctx.getScope(1).getString("resourceType")).toBe("Patient"); + }); + + it("getStringArray finds meta.profile", () => { + const { doc, pos } = at( + '{\n "resourceType": "Patient",\n "meta": {\n "profile": ["http://example.com/Patient"]\n },\n |\n}', + ); + const ctx = buildJsonDocumentContext(doc, pos); + expect(ctx.getScope(0).getStringArray("meta", "profile")).toEqual([ + "http://example.com/Patient", + ]); + }); + + it("getStringArray returns empty for missing profile", () => { + const { doc, pos } = at('{\n "resourceType": "Patient",\n |\n}'); + const ctx = buildJsonDocumentContext(doc, pos); + expect(ctx.getScope(0).getStringArray("meta", "profile")).toEqual([]); + }); + }); +}); diff --git a/packages/react-components/src/components/code-editor/json-ast.ts b/packages/react-components/src/components/code-editor/json-ast.ts new file mode 100644 index 00000000..e5fb5249 --- /dev/null +++ b/packages/react-components/src/components/code-editor/json-ast.ts @@ -0,0 +1,587 @@ +import type { syntaxTree } from "@codemirror/language"; +import type { SyntaxNode } from "@lezer/common"; + +// ── Types ────────────────────────────────────────────────────────────── + +export interface ScopeView { + getString(key: string): string | null; + getStringArray(parentKey: string, arrayKey: string): string[]; + getKeys(): string[]; +} + +export interface DocumentContext { + fullPath: string[]; + pos: number; + doc: string; + cursorPosition: + | { kind: "property"; prefix: string } + | { kind: "value"; key: string; prefix: string } + | { kind: "array-item"; parentKey: string; prefix: string } + | { kind: "none" }; + getScope(levelsUp: number): ScopeView; + isInsideArray(): boolean; +} + +export interface PropertyInfo { + name: string; + path: string[]; + resourceType: string; + from: number; + to: number; +} + +export interface EmptyStringInfo { + from: number; + to: number; +} + +// ── HTTP mode helper ─────────────────────────────────────────────────── + +const HTTP_METHOD_RE = /^(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s/; + +function detectJsonStart(doc: string): number { + const firstLine = doc.slice(0, doc.indexOf("\n") >>> 0).trimStart(); + if (HTTP_METHOD_RE.test(firstLine)) { + const bodyStart = doc.indexOf("\n\n"); + if (bodyStart === -1) return 0; + return bodyStart + 2; + } + return 0; +} + +// ── JSON path at cursor ──────────────────────────────────────────────── + +function getJsonPathAtCursor(doc: string, pos: number): string[] { + const path: string[] = []; + const arrayKeyStack: string[] = []; + let inString = false; + let isEscaped = false; + let currentKey = ""; + let collectingKey = false; + let lastKey = ""; + + for (let i = 0; i < pos; i++) { + const ch = doc[i]; + + if (isEscaped) { + if (collectingKey) currentKey += ch; + isEscaped = false; + continue; + } + if (ch === "\\") { + isEscaped = true; + if (collectingKey) currentKey += ch; + continue; + } + if (ch === '"') { + if (!inString) { + inString = true; + collectingKey = true; + currentKey = ""; + } else { + inString = false; + if (collectingKey) { + lastKey = currentKey; + collectingKey = false; + } + } + continue; + } + if (inString) { + if (collectingKey) currentKey += ch; + continue; + } + if (ch === "[") { + arrayKeyStack.push(lastKey); + lastKey = ""; + } else if (ch === "]") { + arrayKeyStack.pop(); + lastKey = ""; + } else if (ch === "{") { + const key = + lastKey || + (arrayKeyStack.length > 0 + ? (arrayKeyStack[arrayKeyStack.length - 1] ?? "") + : ""); + if (key) path.push(key); + lastKey = ""; + } else if (ch === "}") { + path.pop(); + lastKey = ""; + } else if (ch === ",") { + lastKey = ""; + } + } + return path; +} + +// ── Cursor position detection ────────────────────────────────────────── + +function isJsonValuePosition(beforeCursor: string): string | null { + const match = beforeCursor.match(/"?(\w+)"?\s*:\s*"?([^"]*)?$/); + if (match) return match[1] ?? null; + return null; +} + +function isJsonPropertyPosition(beforeCursor: string): boolean { + if (beforeCursor === "" || beforeCursor === '"') return true; + if (/^"?[\w]*$/.test(beforeCursor)) return true; + if (/[{,]\s*"?[\w]*$/.test(beforeCursor)) return true; + return false; +} + +function isInsideJsonArray(doc: string, pos: number): boolean { + let depth = 0; + let inStr = false; + let escaped = false; + for (let i = pos - 1; i >= 0; i--) { + const ch = doc[i]; + if (escaped) { + escaped = false; + continue; + } + if (ch === "\\") { + escaped = true; + continue; + } + if (ch === '"') { + inStr = !inStr; + continue; + } + if (inStr) continue; + if (ch === "}" || ch === "]") { + depth++; + } else if (ch === "{") { + if (depth === 0) return false; + depth--; + } else if (ch === "[") { + if (depth === 0) return true; + depth--; + } + } + return false; +} + +// ── Array-item detection ─────────────────────────────────────────────── + +function detectArrayItemContext( + doc: string, + pos: number, +): { parentKey: string; prefix: string } | null { + const textBefore = doc.slice(0, pos); + const arrayMatch = textBefore.match( + /"(\w+)"\s*:\s*\[\s*(?:"[^"]*"\s*,\s*)*"?([^"]*)$/s, + ); + if (!arrayMatch) return null; + // If there are unmatched { after [, cursor is inside a nested object, not directly in array + const afterBracket = arrayMatch[2] ?? ""; + let braceDepth = 0; + for (const ch of afterBracket) { + if (ch === "{") braceDepth++; + else if (ch === "}") braceDepth--; + } + if (braceDepth > 0) return null; + return { parentKey: arrayMatch[1]!, prefix: afterBracket }; +} + +// ── Scope view (find values in ancestor objects) ─────────────────────── + +function findStringValueInObject( + doc: string, + objStart: number, + limit: number, + targetKey: string, +): string | null { + let fd = 0; + let fs = false; + let fe = false; + let lastKey = ""; + let collecting = false; + let current = ""; + let afterColon = false; + + for (let i = objStart + 1; i < limit; i++) { + const ch = doc[i]; + if (fe) { + if (collecting) current += ch; + fe = false; + continue; + } + if (ch === "\\") { + fe = true; + if (collecting) current += ch; + continue; + } + if (ch === '"') { + if (!fs) { + fs = true; + if (fd === 0) { + collecting = true; + current = ""; + } + } else { + fs = false; + if (collecting) { + if (afterColon) { + if (lastKey === targetKey) return current; + afterColon = false; + } else { + lastKey = current; + } + collecting = false; + } + } + continue; + } + if (fs) { + if (collecting) current += ch; + continue; + } + if (ch === "{" || ch === "[") fd++; + else if (ch === "}" || ch === "]") fd--; + else if (ch === ":" && fd === 0) afterColon = true; + else if (ch === "," && fd === 0) { + afterColon = false; + lastKey = ""; + } + } + return null; +} + +function findStringArrayInObject( + doc: string, + objStart: number, + limit: number, + parentKey: string, + arrayKey: string, +): string[] { + // Find "parentKey": { ... "arrayKey": ["v1", "v2"] ... } + // or if parentKey is empty, find "arrayKey": [...] at top level + const searchDoc = doc.slice(objStart, limit); + let pattern: RegExp; + if (parentKey) { + pattern = new RegExp( + `"${parentKey}"\\s*:\\s*\\{[\\s\\S]*?"${arrayKey}"\\s*:\\s*\\[([\\s\\S]*?)\\]`, + ); + } else { + pattern = new RegExp(`"${arrayKey}"\\s*:\\s*\\[([\\s\\S]*?)\\]`); + } + const match = searchDoc.match(pattern); + if (!match?.[1]) return []; + const urls: string[] = []; + const re = /"([^"]+)"/g; + let m: RegExpExecArray | null; + while ((m = re.exec(match[1])) !== null) { + if (m[1]) urls.push(m[1]); + } + return urls; +} + +function findKeysInObject( + doc: string, + objStart: number, + limit: number, +): string[] { + const keys: string[] = []; + let fd = 0; + let fs = false; + let fe = false; + let collecting = false; + let current = ""; + let afterColon = false; + + for (let i = objStart + 1; i < limit; i++) { + const ch = doc[i]; + if (fe) { + if (collecting) current += ch; + fe = false; + continue; + } + if (ch === "\\") { + fe = true; + if (collecting) current += ch; + continue; + } + if (ch === '"') { + if (!fs) { + fs = true; + if (fd === 0) { + collecting = true; + current = ""; + } + } else { + fs = false; + if (collecting) { + if (!afterColon) { + keys.push(current); + } + collecting = false; + } + } + continue; + } + if (fs) { + if (collecting) current += ch; + continue; + } + if (ch === "{" || ch === "[") fd++; + else if (ch === "}" || ch === "]") fd--; + else if (ch === ":" && fd === 0) afterColon = true; + else if (ch === "," && fd === 0) { + afterColon = false; + } + } + return keys; +} + +function buildScopeView(doc: string, pos: number, levelsUp: number): ScopeView { + // Forward scan to find enclosing objects — avoids string-tracking bugs + // from backward scanning when cursor is inside an unclosed string. + const objectStack: number[] = []; + let inString = false; + let isEscaped = false; + + for (let i = 0; i < pos; i++) { + const ch = doc[i]; + if (isEscaped) { + isEscaped = false; + continue; + } + if (ch === "\\") { + isEscaped = true; + continue; + } + if (ch === '"') { + inString = !inString; + continue; + } + if (inString) continue; + if (ch === "{") { + objectStack.push(i); + } else if (ch === "}") { + objectStack.pop(); + } + } + + // objectStack[last] is innermost, objectStack[last - levelsUp] is target + const targetIdx = objectStack.length - 1 - levelsUp; + if (targetIdx < 0) { + return { + getString() { + return null; + }, + getStringArray() { + return []; + }, + getKeys() { + return []; + }, + }; + } + + const objStart = objectStack[targetIdx]!; + const scopeEnd = doc.length; + + return { + getString(key: string): string | null { + return findStringValueInObject(doc, objStart, scopeEnd, key); + }, + getStringArray(parentKey: string, arrayKey: string): string[] { + return findStringArrayInObject( + doc, + objStart, + scopeEnd, + parentKey, + arrayKey, + ); + }, + getKeys(): string[] { + return findKeysInObject(doc, objStart, scopeEnd); + }, + }; +} + +// ── buildJsonDocumentContext ──────────────────────────────────────────── + +export function buildJsonDocumentContext( + doc: string, + pos: number, +): DocumentContext { + const jsonStart = detectJsonStart(doc); + const jsonBody = doc.slice(jsonStart); + const posInBody = pos - jsonStart; + + const fullPath = getJsonPathAtCursor(jsonBody, posInBody); + + // Determine cursor position kind + const lineStart = doc.lastIndexOf("\n", pos - 1) + 1; + const beforeCursor = doc.slice(lineStart, pos).trimStart(); + + let cursorPosition: DocumentContext["cursorPosition"]; + + const valueKey = isJsonValuePosition(beforeCursor); + if (valueKey) { + cursorPosition = { kind: "value", key: valueKey, prefix: "" }; + const wordMatch = beforeCursor.match(/"?(\w+)"?\s*:\s*"?([^"]*)?$/); + if (wordMatch?.[2] != null) { + cursorPosition.prefix = wordMatch[2]; + } + } else { + const arrayItem = detectArrayItemContext(doc.slice(jsonStart), posInBody); + if (arrayItem) { + cursorPosition = { + kind: "array-item", + parentKey: arrayItem.parentKey, + prefix: arrayItem.prefix, + }; + } else if (isJsonPropertyPosition(beforeCursor)) { + const wordMatch = beforeCursor.match(/"?(\w*)$/); + cursorPosition = { kind: "property", prefix: wordMatch?.[1] ?? "" }; + } else { + cursorPosition = { kind: "none" }; + } + } + + return { + fullPath, + pos, + doc, + cursorPosition, + getScope(levelsUp: number): ScopeView { + return buildScopeView(jsonBody, posInBody, levelsUp); + }, + isInsideArray(): boolean { + return isInsideJsonArray(jsonBody, posInBody); + }, + }; +} + +// ── Validation helpers ───────────────────────────────────────────────── + +export function walkJsonProperties( + doc: string, + tree: ReturnType, + resourceTypeHint: string | null, +): { properties: PropertyInfo[]; emptyStrings: EmptyStringInfo[] } { + const properties: PropertyInfo[] = []; + const emptyStrings: EmptyStringInfo[] = []; + + const rootObj = findRootJsonObject(doc, tree); + if (rootObj) { + walkJsonObject( + rootObj, + [], + resourceTypeHint, + doc, + properties, + emptyStrings, + ); + } + + return { properties, emptyStrings }; +} + +export function findRootJsonObject( + doc: string, + tree: ReturnType, +): SyntaxNode | null { + const direct = tree.topNode.getChild("Object"); + if (direct) return direct; + + const bodyStart = doc.indexOf("\n\n"); + if (bodyStart === -1) return null; + + const jsonStart = bodyStart + 2; + if (jsonStart >= doc.length) return null; + + const innerNode = tree.resolveInner(jsonStart, 1); + if (!innerNode) return null; + + let node: SyntaxNode | null = innerNode; + while (node) { + if (node.name === "Object") return node; + if (node.name === "JsonText") { + return node.getChild("Object"); + } + node = node.parent; + } + + return null; +} + +function walkJsonObject( + node: SyntaxNode, + parentPath: string[], + parentResourceType: string | null, + doc: string, + result: PropertyInfo[], + emptyStrings?: EmptyStringInfo[], +): void { + let ownResourceType: string | null = null; + for (let child = node.firstChild; child; child = child.nextSibling) { + if (child.name !== "Property") continue; + const nameNode = child.getChild("PropertyName"); + if (!nameNode) continue; + const keyName = doc.slice(nameNode.from, nameNode.to).replace(/^"|"$/g, ""); + if (keyName === "resourceType") { + for (let v = child.firstChild; v; v = v.nextSibling) { + if (v.name === "String") { + ownResourceType = doc.slice(v.from, v.to).replace(/^"|"$/g, ""); + break; + } + } + break; + } + } + + const resourceType = ownResourceType ?? parentResourceType; + const path = ownResourceType ? [] : parentPath; + + if (!resourceType) return; + + for (let child = node.firstChild; child; child = child.nextSibling) { + if (child.name !== "Property") continue; + const nameNode = child.getChild("PropertyName"); + if (!nameNode) continue; + const name = doc.slice(nameNode.from, nameNode.to).replace(/^"|"$/g, ""); + + result.push({ + name, + path: [...path], + resourceType, + from: nameNode.from, + to: nameNode.to, + }); + + for (let v = child.firstChild; v; v = v.nextSibling) { + if (v.name === "Object") { + walkJsonObject( + v, + [...path, name], + resourceType, + doc, + result, + emptyStrings, + ); + } else if (v.name === "Array") { + for (let item = v.firstChild; item; item = item.nextSibling) { + if (item.name === "Object") { + walkJsonObject( + item, + [...path, name], + resourceType, + doc, + result, + emptyStrings, + ); + } + } + } else if (v.name === "String" && emptyStrings) { + const raw = doc.slice(v.from, v.to); + if (raw === '""') { + emptyStrings.push({ from: v.from, to: v.to }); + } + } + } + } +} diff --git a/packages/react-components/tsconfig.app.json b/packages/react-components/tsconfig.app.json index f7a81415..e4926616 100644 --- a/packages/react-components/tsconfig.app.json +++ b/packages/react-components/tsconfig.app.json @@ -35,5 +35,5 @@ } }, "include": ["src"], - "exclude": ["src/**/*.stories.tsx", "src/**/*.stories.ts"] + "exclude": ["src/**/*.stories.tsx", "src/**/*.stories.ts", "src/**/*.test.ts", "src/**/*.test.tsx"] } From 6135e8c547a347340e132d335082ca63f2129e57 Mon Sep 17 00:00:00 2001 From: Panthevm Date: Mon, 23 Mar 2026 13:06:49 +0300 Subject: [PATCH 13/55] Fix boolean completions shown after completed value with comma MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit isJsonValuePosition incorrectly matched "active": true, | as a value position because [^"]* captured "true, ". Added early return when beforeCursor ends with comma — the value is complete, not in progress. --- .../components/code-editor/fhir-autocomplete.test.ts | 12 ++++++++++++ .../src/components/code-editor/json-ast.test.ts | 8 ++++++++ .../src/components/code-editor/json-ast.ts | 2 ++ 3 files changed, 22 insertions(+) diff --git a/packages/react-components/src/components/code-editor/fhir-autocomplete.test.ts b/packages/react-components/src/components/code-editor/fhir-autocomplete.test.ts index 703a228a..b7541e6a 100644 --- a/packages/react-components/src/components/code-editor/fhir-autocomplete.test.ts +++ b/packages/react-components/src/components/code-editor/fhir-autocomplete.test.ts @@ -611,6 +611,18 @@ describe("fhir-autocomplete: jsonCompletionSource", () => { expect(l).toContain("false"); expect(l).toHaveLength(2); }); + + it("offers property completions (not booleans) on new line after boolean value", async () => { + const { cc } = completionAt( + '{\n "resourceType": "Patient",\n "active": true,\n |\n}', + ); + const result = await source(cc); + const l = labels(result); + expect(l).not.toContain("true"); + expect(l).not.toContain("false"); + expect(l).toContain("name"); + expect(l).toContain("gender"); + }); }); describe("reference target completions", () => { diff --git a/packages/react-components/src/components/code-editor/json-ast.test.ts b/packages/react-components/src/components/code-editor/json-ast.test.ts index 85869658..6b00094a 100644 --- a/packages/react-components/src/components/code-editor/json-ast.test.ts +++ b/packages/react-components/src/components/code-editor/json-ast.test.ts @@ -137,6 +137,14 @@ describe("buildJsonDocumentContext", () => { const ctx = buildJsonDocumentContext(doc, pos); expect(ctx.cursorPosition.kind).toBe("property"); }); + + it("property position after boolean value with comma on same line", () => { + const { doc, pos } = at( + '{\n "resourceType": "Patient",\n "active": true, |\n}', + ); + const ctx = buildJsonDocumentContext(doc, pos); + expect(ctx.cursorPosition.kind).toBe("property"); + }); }); describe("isInsideArray", () => { diff --git a/packages/react-components/src/components/code-editor/json-ast.ts b/packages/react-components/src/components/code-editor/json-ast.ts index e5fb5249..ccd3ed23 100644 --- a/packages/react-components/src/components/code-editor/json-ast.ts +++ b/packages/react-components/src/components/code-editor/json-ast.ts @@ -118,6 +118,8 @@ function getJsonPathAtCursor(doc: string, pos: number): string[] { // ── Cursor position detection ────────────────────────────────────────── function isJsonValuePosition(beforeCursor: string): string | null { + // Don't match if a comma follows the value (value is complete) + if (/,\s*$/.test(beforeCursor)) return null; const match = beforeCursor.match(/"?(\w+)"?\s*:\s*"?([^"]*)?$/); if (match) return match[1] ?? null; return null; From 4d342d742800f75a799b433d737c0c9681af5c2c Mon Sep 17 00:00:00 2001 From: Panthevm Date: Mon, 23 Mar 2026 14:48:00 +0300 Subject: [PATCH 14/55] Add drag & drop reorder support to TabsBrowserList --- .../src/shadcn/components/ui/tabs.tsx | 151 +++++++++++++++++- 1 file changed, 149 insertions(+), 2 deletions(-) diff --git a/packages/react-components/src/shadcn/components/ui/tabs.tsx b/packages/react-components/src/shadcn/components/ui/tabs.tsx index 43818509..6b1583fc 100644 --- a/packages/react-components/src/shadcn/components/ui/tabs.tsx +++ b/packages/react-components/src/shadcn/components/ui/tabs.tsx @@ -443,17 +443,164 @@ function TabScrollRightButton({ ); } +type DragState = { + index: number; + startX: number; + offsetX: number; + currentIndex: number; + widths: number[]; + lefts: number[]; +}; + +const DRAG_THRESHOLD = 5; + +function useTabReorder( + onReorder: ((fromIndex: number, toIndex: number) => void) | undefined, +) { + const [drag, setDrag] = React.useState(null); + const dragRef = React.useRef(null); + const pendingRef = React.useRef(false); + const itemsRef = React.useRef<(HTMLDivElement | null)[]>([]); + + const handlePointerDown = React.useCallback( + (e: React.PointerEvent, index: number) => { + if (!onReorder || e.button !== 0) return; + const items = itemsRef.current; + const widths = items.map((el) => el?.offsetWidth ?? 0); + const lefts: number[] = []; + let acc = 0; + for (const w of widths) { + lefts.push(acc); + acc += w; + } + dragRef.current = { + index, + startX: e.clientX, + offsetX: 0, + currentIndex: index, + widths, + lefts, + }; + pendingRef.current = true; + (e.target as HTMLElement).setPointerCapture(e.pointerId); + }, + [onReorder], + ); + + const handlePointerMove = React.useCallback( + (e: React.PointerEvent) => { + const d = dragRef.current; + if (!d) return; + const dx = e.clientX - d.startX; + if (pendingRef.current) { + if (Math.abs(dx) < DRAG_THRESHOLD) return; + pendingRef.current = false; + } + const draggedLeft = d.lefts[d.index] ?? 0; + const draggedWidth = d.widths[d.index] ?? 0; + const draggedRightEdge = draggedLeft + draggedWidth + dx; + const draggedLeftEdge = draggedLeft + dx; + let newIndex = d.index; + const TRIGGER_RATIO = 0.3; + for (let i = 0; i < d.lefts.length; i++) { + if (i === d.index) continue; + const left = d.lefts[i] ?? 0; + const width = d.widths[i] ?? 0; + if (i > d.index) { + // Dragging right: trigger when right edge enters 30% of target + if (draggedRightEdge > left + width * TRIGGER_RATIO) newIndex = i; + } else { + // Dragging left: trigger when left edge enters 30% from right + if (draggedLeftEdge < left + width * (1 - TRIGGER_RATIO)) + newIndex = Math.min(newIndex, i); + } + } + const next: DragState = { ...d, offsetX: dx, currentIndex: newIndex }; + dragRef.current = next; + setDrag(next); + }, + [], + ); + + const handlePointerUp = React.useCallback(() => { + const d = dragRef.current; + if (d && !pendingRef.current && d.index !== d.currentIndex) { + onReorder?.(d.index, d.currentIndex); + } + dragRef.current = null; + pendingRef.current = false; + setDrag(null); + }, [onReorder]); + + const getTransform = React.useCallback( + (index: number): React.CSSProperties => { + if (!drag || pendingRef.current) return {}; + if (index === drag.index) { + return { + transform: `translateX(${drag.offsetX}px)`, + zIndex: 10, + position: "relative", + background: "var(--color-bg-primary)", + borderLeft: "1px solid var(--color-border-default)", + }; + } + const from = drag.index; + const to = drag.currentIndex; + const draggedWidth = drag.widths[from] ?? 0; + if (from < to && index > from && index <= to) { + return { + transform: `translateX(${-draggedWidth}px)`, + transition: "transform 200ms ease", + }; + } + if (from > to && index >= to && index < from) { + return { + transform: `translateX(${draggedWidth}px)`, + transition: "transform 200ms ease", + }; + } + return { transition: "transform 200ms ease" }; + }, + [drag], + ); + + return { drag, itemsRef, handlePointerDown, handlePointerMove, handlePointerUp, getTransform }; +} + function TabsBrowserList({ className, children, + onReorder, ...props -}: React.ComponentProps) { +}: React.ComponentProps & { + onReorder?: (fromIndex: number, toIndex: number) => void; +}) { const tabsListRef = React.useRef(null); const [showScrollButtons, setShowScrollButtons] = React.useState(false); const [canScrollLeft, setCanScrollLeft] = React.useState(false); const [canScrollRight, setCanScrollRight] = React.useState(false); + const { drag, itemsRef, handlePointerDown, handlePointerMove, handlePointerUp, getTransform } = + useTabReorder(onReorder); + + const wrappedChildren = onReorder + ? React.Children.map(children, (child, index) => ( +
{ itemsRef.current[index] = el; }} + style={getTransform(index)} + onPointerDown={(e) => handlePointerDown(e, index)} + onPointerMove={handlePointerMove} + onPointerUp={handlePointerUp} + className={cn( + drag?.index === index && "cursor-grabbing", + )} + > + {child} +
+ )) + : children; + return ( {showScrollButtons && ( @@ -506,7 +653,7 @@ function TabsBrowserList({ {...props} ref={tabsListRef} > - {children} + {wrappedChildren} {showScrollButtons && ( From 9662b412db34b1bf8023aa1f2329704ff4d46e5c Mon Sep 17 00:00:00 2001 From: Panthevm Date: Mon, 23 Mar 2026 14:57:01 +0300 Subject: [PATCH 15/55] Fix HTTP header completion triggering on empty input --- .../src/components/code-editor/http/index.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/react-components/src/components/code-editor/http/index.ts b/packages/react-components/src/components/code-editor/http/index.ts index a18d087a..21f24ab2 100644 --- a/packages/react-components/src/components/code-editor/http/index.ts +++ b/packages/react-components/src/components/code-editor/http/index.ts @@ -308,11 +308,12 @@ function httpCompletionSource( if (!inHeaderName && !inHeaders && !parentIsHeaders) return null; - const word = context.matchBefore(/[\w-]*/); + const word = context.matchBefore(/[\w-]+/); + if (!word) return null; return { - from: word?.from ?? pos, + from: word.from, options: COMMON_HEADERS, - validFor: /^[\w-]*$/, + validFor: /^[\w-]+$/, }; } From 87b5180e6d9cc341f32b748b17181b61c429adae Mon Sep 17 00:00:00 2001 From: Panthevm Date: Mon, 23 Mar 2026 15:00:06 +0300 Subject: [PATCH 16/55] Skip JSON lint errors on empty document --- .../react-components/src/components/code-editor/index.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/react-components/src/components/code-editor/index.tsx b/packages/react-components/src/components/code-editor/index.tsx index cac56143..a0e556f5 100644 --- a/packages/react-components/src/components/code-editor/index.tsx +++ b/packages/react-components/src/components/code-editor/index.tsx @@ -976,7 +976,10 @@ function languageExtensions( } else { return [ json(), - linter(jsonParseLinter(), { delay: 300 }), + linter((view) => { + if (!view.state.doc.toString().trim()) return []; + return jsonParseLinter()(view); + }, { delay: 300 }), syntaxHighlighting(customHighlightStyle), jsonAutoExpandBraces(), ]; From 0c92818da8ecd8368d42a34594cb46f260fab10b Mon Sep 17 00:00:00 2001 From: Andrey Listopadov Date: Mon, 23 Mar 2026 17:12:29 +0300 Subject: [PATCH 17/55] new date picker --- .../components/date-picker-input.stories.tsx | 79 +++++++++++++ .../src/components/date-picker-input.tsx | 110 ++++++++++++++++++ packages/react-components/src/index.tsx | 1 + 3 files changed, 190 insertions(+) create mode 100644 packages/react-components/src/components/date-picker-input.stories.tsx create mode 100644 packages/react-components/src/components/date-picker-input.tsx diff --git a/packages/react-components/src/components/date-picker-input.stories.tsx b/packages/react-components/src/components/date-picker-input.stories.tsx new file mode 100644 index 00000000..fbc76466 --- /dev/null +++ b/packages/react-components/src/components/date-picker-input.stories.tsx @@ -0,0 +1,79 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { useState } from "react"; +import { DatePickerInput } from "./date-picker-input"; + +function DatePickerInputWrapper(props: { + placeholder?: string; + disabled?: boolean; +}) { + const [value, setValue] = useState(""); + return ( +
+ +
+ ); +} + +function DatePickerInputPrefilledWrapper() { + const [value, setValue] = useState("15.06.2025"); + return ( +
+ +
+ ); +} + +function DatePickerRangeWrapper() { + const [from, setFrom] = useState(""); + const [to, setTo] = useState(""); + return ( +
+ + + +
+ ); +} + +const meta = { + title: "Component/DatePickerInput", + component: DatePickerInputWrapper, + parameters: { + layout: "centered", + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const WithPlaceholder: Story = { + args: { + placeholder: "Select a date...", + }, +}; + +export const Disabled: Story = { + args: { + disabled: true, + }, +}; + +export const Prefilled: StoryObj = { + render: () => , +}; + +export const DateRange: StoryObj = { + render: () => , +}; diff --git a/packages/react-components/src/components/date-picker-input.tsx b/packages/react-components/src/components/date-picker-input.tsx new file mode 100644 index 00000000..1ced0340 --- /dev/null +++ b/packages/react-components/src/components/date-picker-input.tsx @@ -0,0 +1,110 @@ +import { CalendarIcon } from "lucide-react"; +import * as React from "react"; +import { Button } from "#shadcn/components/ui/button"; +import { Calendar } from "#shadcn/components/ui/calendar"; +import { Input } from "#shadcn/components/ui/input"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "#shadcn/components/ui/popover"; +import { cn } from "#shadcn/lib/utils"; + +function formatDate(date: Date | undefined): string { + if (!date) return ""; + const d = date.getDate().toString().padStart(2, "0"); + const m = (date.getMonth() + 1).toString().padStart(2, "0"); + const y = date.getFullYear(); + return `${d}.${m}.${y}`; +} + +function parseDate(value: string): Date | undefined { + if (!value) return undefined; + const parts = value.split("."); + if (parts.length === 3) { + const [d, m, y] = parts; + const date = new Date(`${y}-${m}-${d}`); + if (!Number.isNaN(date.getTime())) return date; + } + const date = new Date(value); + if (!Number.isNaN(date.getTime())) return date; + return undefined; +} + +interface DatePickerInputProps { + value: string; + onChange: (value: string) => void; + placeholder?: string; + className?: string; + disabled?: boolean; +} + +function DatePickerInput({ + value, + onChange, + placeholder = "dd.mm.yyyy", + className, + disabled, +}: DatePickerInputProps) { + const [open, setOpen] = React.useState(false); + const selectedDate = parseDate(value); + const [month, setMonth] = React.useState( + selectedDate ?? new Date(), + ); + + return ( +
+ onChange(e.target.value)} + onKeyDown={(e) => { + if (e.key === "ArrowDown") { + e.preventDefault(); + setOpen(true); + } + }} + rightSlot={ + + + + + + setMonth(m)} + onSelect={(date) => { + onChange(formatDate(date)); + if (date) setMonth(date); + setOpen(false); + }} + /> + + + } + /> +
+ ); +} + +export { DatePickerInput, type DatePickerInputProps }; diff --git a/packages/react-components/src/index.tsx b/packages/react-components/src/index.tsx index b0a0cf2a..f682afb5 100644 --- a/packages/react-components/src/index.tsx +++ b/packages/react-components/src/index.tsx @@ -4,6 +4,7 @@ export * from "./components/button-dropdown"; export * from "./components/code-editor"; export * from "./components/copy-icon"; export * from "./components/data-table"; +export * from "./components/date-picker-input"; export * from "./components/fhir-structure-view"; export * from "./components/icon-button"; export * from "./components/operation-outcome-view"; From c386cb25442dbd86a66e8e1d51507708fb0d53e3 Mon Sep 17 00:00:00 2001 From: Panthevm Date: Mon, 23 Mar 2026 16:44:25 +0300 Subject: [PATCH 18/55] Disable tab reorder in TabsBrowserList when only one tab --- packages/react-components/src/shadcn/components/ui/tabs.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-components/src/shadcn/components/ui/tabs.tsx b/packages/react-components/src/shadcn/components/ui/tabs.tsx index 6b1583fc..a6d016b2 100644 --- a/packages/react-components/src/shadcn/components/ui/tabs.tsx +++ b/packages/react-components/src/shadcn/components/ui/tabs.tsx @@ -584,7 +584,7 @@ function TabsBrowserList({ const { drag, itemsRef, handlePointerDown, handlePointerMove, handlePointerUp, getTransform } = useTabReorder(onReorder); - const wrappedChildren = onReorder + const wrappedChildren = onReorder && React.Children.count(children) > 1 ? React.Children.map(children, (child, index) => (
{ itemsRef.current[index] = el; }} From d47ec72198673d302edf405ccadfa7c60d3a02da Mon Sep 17 00:00:00 2001 From: Panthevm Date: Mon, 23 Mar 2026 19:38:30 +0300 Subject: [PATCH 19/55] Bump fhirpath LSP deps: fhirpath 0.1.4, fhirpath-lsp 0.0.7 Fixes primitive type completions (use FHIR names) and filters out profiles from resource type completions in ofType()/is/as context. --- packages/aidbox-fhirpath-lsp/package.json | 4 ++-- pnpm-lock.yaml | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/aidbox-fhirpath-lsp/package.json b/packages/aidbox-fhirpath-lsp/package.json index 04c51890..384e8d68 100644 --- a/packages/aidbox-fhirpath-lsp/package.json +++ b/packages/aidbox-fhirpath-lsp/package.json @@ -24,8 +24,8 @@ "tsc:check": "tsc -b --noEmit" }, "dependencies": { - "@atomic-ehr/fhirpath": "0.1.3", - "@atomic-ehr/fhirpath-lsp": "0.0.6", + "@atomic-ehr/fhirpath": "0.1.4", + "@atomic-ehr/fhirpath-lsp": "0.0.7", "@health-samurai/aidbox-client": "workspace:^" }, "peerDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9bcefee2..8f64e636 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -52,11 +52,11 @@ importers: packages/aidbox-fhirpath-lsp: dependencies: '@atomic-ehr/fhirpath': - specifier: 0.1.3 - version: 0.1.3(typescript@5.9.3) + specifier: 0.1.4 + version: 0.1.4(typescript@5.9.3) '@atomic-ehr/fhirpath-lsp': - specifier: 0.0.6 - version: 0.0.6(typescript@5.9.3) + specifier: 0.0.7 + version: 0.0.7(typescript@5.9.3) '@health-samurai/aidbox-client': specifier: workspace:^ version: link:../aidbox-client @@ -405,11 +405,11 @@ packages: peerDependencies: typescript: ^5 - '@atomic-ehr/fhirpath-lsp@0.0.6': - resolution: {integrity: sha512-G7uvLBESblf4SRTqeC8iCLPPZj+s6tB8eTFUGyT4OPIAbjWqeDI3D16iL6HXiubL+ypC6uIWJw7JfcnAXbbdaw==} + '@atomic-ehr/fhirpath-lsp@0.0.7': + resolution: {integrity: sha512-rhnf/VrTKSa69cOzlFc9Z3bow1YlvJ2/5AZ4uKX9C769muZZKTbu8atN+NOmCOgSD8m+4q8e6xfRvbRSj7lZxg==} - '@atomic-ehr/fhirpath@0.1.3': - resolution: {integrity: sha512-VoeVRMbHk6i41wErdki82Mp2LLhxsDbE1deJiQx3v+HUWydHyyjg7ZHbuc9d+9Ecu13ZWEmkvcdumH+7bJmsew==} + '@atomic-ehr/fhirpath@0.1.4': + resolution: {integrity: sha512-A26knB2XGfCiu6vXAfYyusxToxHwdIddRLrkpjrYlI/qZssjspHULQLzANvLVlsQW5uPpOF+bloXIv0r3qOpSQ==} peerDependencies: typescript: ^5 @@ -4263,9 +4263,9 @@ snapshots: dependencies: typescript: 5.9.3 - '@atomic-ehr/fhirpath-lsp@0.0.6(typescript@5.9.3)': + '@atomic-ehr/fhirpath-lsp@0.0.7(typescript@5.9.3)': dependencies: - '@atomic-ehr/fhirpath': 0.1.3(typescript@5.9.3) + '@atomic-ehr/fhirpath': 0.1.4(typescript@5.9.3) '@codemirror/autocomplete': 6.20.1 '@codemirror/commands': 6.10.3 '@codemirror/language': 6.12.2 @@ -4280,7 +4280,7 @@ snapshots: transitivePeerDependencies: - typescript - '@atomic-ehr/fhirpath@0.1.3(typescript@5.9.3)': + '@atomic-ehr/fhirpath@0.1.4(typescript@5.9.3)': dependencies: '@atomic-ehr/fhir-canonical-manager': 0.0.11(typescript@5.9.3) '@atomic-ehr/fhirschema': 0.0.2(typescript@5.9.3) From d1761f58e106b25f1c4d0cbe84c38dbd984318ba Mon Sep 17 00:00:00 2001 From: Panthevm Date: Mon, 23 Mar 2026 19:46:54 +0300 Subject: [PATCH 20/55] Bump react-components to alpha.20, aidbox-fhirpath-lsp to alpha.7 --- packages/aidbox-fhirpath-lsp/package.json | 2 +- packages/react-components/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/aidbox-fhirpath-lsp/package.json b/packages/aidbox-fhirpath-lsp/package.json index 384e8d68..7f837baf 100644 --- a/packages/aidbox-fhirpath-lsp/package.json +++ b/packages/aidbox-fhirpath-lsp/package.json @@ -1,6 +1,6 @@ { "name": "@health-samurai/aidbox-fhirpath-lsp", - "version": "0.0.0-alpha.6", + "version": "0.0.0-alpha.7", "type": "module", "files": [ "dist", diff --git a/packages/react-components/package.json b/packages/react-components/package.json index 2a38c6fb..135f937b 100644 --- a/packages/react-components/package.json +++ b/packages/react-components/package.json @@ -1,6 +1,6 @@ { "name": "@health-samurai/react-components", - "version": "0.0.0-alpha.19", + "version": "0.0.0-alpha.20", "type": "module", "files": [ "dist", From b96764b65624efc272dc9c3ee9d47721d4bacded Mon Sep 17 00:00:00 2001 From: Panthevm Date: Mon, 23 Mar 2026 19:53:40 +0300 Subject: [PATCH 21/55] Fix lint errors and override fast-xml-parser to resolve CVE-2026-26278 --- package.json | 5 + .../code-editor/fhir-autocomplete.test.ts | 48 ++++- .../code-editor/fhir-autocomplete.ts | 88 +++----- .../src/components/code-editor/http/index.ts | 51 +++-- .../src/components/code-editor/index.tsx | 198 ++++++++++-------- .../src/components/code-editor/json-ast.ts | 1 + .../src/components/date-picker-input.tsx | 10 +- .../src/shadcn/components/ui/tabs.tsx | 117 ++++++----- packages/react-components/tsconfig.app.json | 7 +- pnpm-lock.yaml | 26 ++- 10 files changed, 305 insertions(+), 246 deletions(-) diff --git a/package.json b/package.json index 3521708f..d00e3f83 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,11 @@ "packages/*" ], "packageManager": "pnpm@10.21.0", + "pnpm": { + "overrides": { + "fast-xml-parser": ">=5.5.6" + } + }, "devDependencies": { "@biomejs/biome": "2.4.6", "@swc/cli": "^0.8.0", diff --git a/packages/react-components/src/components/code-editor/fhir-autocomplete.test.ts b/packages/react-components/src/components/code-editor/fhir-autocomplete.test.ts index b7541e6a..ac1f51a6 100644 --- a/packages/react-components/src/components/code-editor/fhir-autocomplete.test.ts +++ b/packages/react-components/src/components/code-editor/fhir-autocomplete.test.ts @@ -1,7 +1,7 @@ import { CompletionContext } from "@codemirror/autocomplete"; import { json } from "@codemirror/lang-json"; import { EditorState } from "@codemirror/state"; -import { beforeEach, describe, expect, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { buildParameterSnippet, type ExpandValueSet, @@ -214,8 +214,18 @@ const BUNDLE_SD = { element: [ { path: "Bundle", min: 0, max: "*" }, { path: "Bundle.type", min: 1, max: "1", type: [{ code: "code" }] }, - { path: "Bundle.entry", min: 0, max: "*", type: [{ code: "BackboneElement" }] }, - { path: "Bundle.entry.resource", min: 0, max: "1", type: [{ code: "Resource" }] }, + { + path: "Bundle.entry", + min: 0, + max: "*", + type: [{ code: "BackboneElement" }], + }, + { + path: "Bundle.entry.resource", + min: 0, + max: "1", + type: [{ code: "Resource" }], + }, ], }, }; @@ -697,7 +707,11 @@ describe("fhir-autocomplete: jsonCompletionSource", () => { }); it("offers Observation fields when Bundle resourceType comes from hint (URL)", async () => { - const hintSource = jsonCompletionSource(mockGetSDs, "Bundle", mockExpandValueSet); + const hintSource = jsonCompletionSource( + mockGetSDs, + "Bundle", + mockExpandValueSet, + ); const { cc } = completionAt( '{\n "entry": [\n {\n "resource": {\n "resourceType": "Observation",\n |\n }\n }\n ]\n}', ); @@ -810,7 +824,11 @@ describe("fhir-autocomplete: jsonCompletionSource", () => { }); it("offers typed snippet for profile with value[x] constraint", async () => { - const typedSource = jsonCompletionSource(mockGetSDs, undefined, mockExpandValueSet); + const typedSource = jsonCompletionSource( + mockGetSDs, + undefined, + mockExpandValueSet, + ); const { cc } = completionAt( '{\n "resourceType": "Parameters",\n "meta": {\n "profile": ["http://example.com/StructureDefinition/typed-params"]\n },\n "parameter": [\n |\n ]\n}', ); @@ -858,8 +876,8 @@ describe("fhir-autocomplete: jsonCompletionSource", () => { const result = await source(cc); const option = result?.options.find((o) => o.label === "parameter"); expect(option).toBeDefined(); - expect(option!.boost).toBe(-1); - expect(option!.info).toBe("Custom parameter"); + expect(option?.boost).toBe(-1); + expect(option?.info).toBe("Custom parameter"); }); }); @@ -885,7 +903,11 @@ describe("fhir-autocomplete: jsonCompletionSource", () => { }); it("offers Observation fields inside Bundle entry.resource via hint", async () => { - const hintSource = jsonCompletionSource(mockGetSDs, "Bundle", mockExpandValueSet); + const hintSource = jsonCompletionSource( + mockGetSDs, + "Bundle", + mockExpandValueSet, + ); const doc = 'POST /fhir/Bundle\nContent-Type: application/json\n\n{\n "entry": [\n {\n "resource": {\n "resourceType": "Observation",\n |\n }\n }\n ]\n}'; const { cc } = completionAt(doc); @@ -922,7 +944,11 @@ describe("buildParameterSnippet", () => { }); it("inserts valueInteger for integer-constrained type", () => { - const { text, cursorOffset } = buildParameterSnippet("count", ["integer"], " "); + const { text, cursorOffset } = buildParameterSnippet( + "count", + ["integer"], + " ", + ); expect(text).toContain('"name": "count"'); expect(text).toContain('"valueInteger": '); expect(text).not.toContain('"valueString"'); @@ -949,9 +975,9 @@ describe("buildParameterSnippet", () => { // Line 0: { expect(lines[0]).toBe("{"); // Line 1: inner indent + "name" - expect(lines[1]).toMatch(/^ "name": "test",$/); + expect(lines[1]).toMatch(/^ {4}"name": "test",$/); // Line 2: inner indent + "valueString" - expect(lines[2]).toMatch(/^ "valueString": ""$/); + expect(lines[2]).toMatch(/^ {4}"valueString": ""$/); // Line 3: outer indent + } expect(lines[3]).toBe(" }"); }); diff --git a/packages/react-components/src/components/code-editor/fhir-autocomplete.ts b/packages/react-components/src/components/code-editor/fhir-autocomplete.ts index 6b841c43..9a3b1c28 100644 --- a/packages/react-components/src/components/code-editor/fhir-autocomplete.ts +++ b/packages/react-components/src/components/code-editor/fhir-autocomplete.ts @@ -438,8 +438,8 @@ function buildSnippet( kind: SnippetKind, indent: string, ): { text: string; cursorOffset: number } { - const inner = indent + " "; - const innerInner = inner + " "; + const inner = `${indent} `; + const innerInner = `${inner} `; switch (kind) { case "array-complex": { const text = `"${name}": [\n${inner}{\n${innerInner}\n${inner}}\n${indent}]`; @@ -454,18 +454,16 @@ function buildSnippet( } case "array-primitive": { const text = `"${name}": [\n${inner}\n${indent}]`; - return { text, cursorOffset: text.indexOf(inner + "\n") + inner.length }; + return { text, cursorOffset: text.indexOf(`${inner}\n`) + inner.length }; } case "object": { const text = `"${name}": {\n${inner}\n${indent}}`; - return { text, cursorOffset: text.indexOf(inner + "\n") + inner.length }; + return { text, cursorOffset: text.indexOf(`${inner}\n`) + inner.length }; } case "string": { const text = `"${name}": ""`; return { text, cursorOffset: text.length - 1 }; } - case "number": - case "bare": default: { const text = `"${name}": `; return { text, cursorOffset: text.length }; @@ -554,8 +552,8 @@ function toParameterPropertyCompletion(element: FhirElement): Completion { const line = view.state.doc.lineAt(actualFrom); const indent = line.text.match(/^(\s*)/)?.[1] ?? ""; - const inner = indent + " "; - const innerInner = inner + " "; + const inner = `${indent} `; + const innerInner = `${inner} `; const text = `"${name}": [\n${inner}{\n${innerInner}"name": ""\n${inner}}\n${indent}]`; view.dispatch({ changes: { from: actualFrom, to: actualTo, insert: text }, @@ -766,7 +764,7 @@ export function buildParameterSnippet( valueTypes: string[], indent: string, ): { text: string; cursorOffset: number } { - const inner = indent + " "; + const inner = `${indent} `; if (valueTypes.length === 1 && FHIR_STRING_TYPES.has(valueTypes[0]!)) { const tc = valueTypes[0]!; @@ -786,12 +784,12 @@ export function buildParameterSnippet( if (valueTypes.length === 1) { const tc = valueTypes[0]!; const vf = `value${tc.charAt(0).toUpperCase()}${tc.slice(1)}`; - const innerInner = inner + " "; + const innerInner = `${inner} `; const text = `{\n${inner}"name": "${name}",\n${inner}"${vf}": {\n${innerInner}\n${inner}}\n${indent}}`; return { text, cursorOffset: - text.indexOf(innerInner + "\n" + inner + "}") + innerInner.length, + text.indexOf(`${innerInner}\n${inner}}`) + innerInner.length, }; } // Default to valueString when no value type constraint @@ -869,7 +867,7 @@ async function findExtensionBinding( ) { const parentUrlMatches = [ ...textBefore - .slice(0, urlMatches[i]!.index) + .slice(0, urlMatches[i]?.index) .matchAll(/"url"\s*:\s*"([^"]+)"/g), ]; for (let j = parentUrlMatches.length - 1; j >= 0; j--) { @@ -1222,12 +1220,7 @@ async function handleValueCompletion( detail: slice.min > 0 ? "required" : "optional", boost: slice.min > 0 ? 2 : 0, ...(slice.short ? { info: slice.short } : {}), - apply: ( - view: EditorView, - _c: Completion, - from: number, - to: number, - ) => { + apply: (view: EditorView, _c: Completion, from: number, to: number) => { const d = view.state.doc.toString(); let actualTo = to; if (actualTo < d.length && d[actualTo] === '"') actualTo++; @@ -1261,11 +1254,9 @@ async function handleValueCompletion( ins = `,\n${ind}"${vf}": `; cOff = ins.length; } else { - const inner = ind + " "; + const inner = `${ind} `; ins = `,\n${ind}"${vf}": {\n${inner}\n${ind}}`; - cOff = - ins.indexOf(inner + "\n" + ind + "}") + - inner.length; + cOff = ins.indexOf(`${inner}\n${ind}}`) + inner.length; } view.dispatch({ changes: { from: cp, insert: ins }, @@ -1295,12 +1286,7 @@ async function handleValueCompletion( label: fixedVal, type: "text", boost: 10, - apply: ( - view: EditorView, - _c: Completion, - from: number, - to: number, - ) => { + apply: (view: EditorView, _c: Completion, from: number, to: number) => { const d = view.state.doc.toString(); let actualTo = to; if (actualTo < d.length && d[actualTo] === '"') actualTo++; @@ -1311,7 +1297,11 @@ async function handleValueCompletion( }, }; const word = completionContext.matchBefore(/[\w.:/-]*/); - return { from: word?.from ?? pos, options: [option], validFor: /^[\w.:/-]*$/ }; + return { + from: word?.from ?? pos, + options: [option], + validFor: /^[\w.:/-]*$/, + }; } } @@ -1359,17 +1349,12 @@ async function handleValueCompletion( if (resourceType) { const elements = await resolveElements(effectivePath, resourceType, getSDs); const el = elements.find((e) => fieldName(e) === valueKey); - if (el?.type?.length === 1 && el.type[0]!.code === "boolean") { + if (el?.type?.length === 1 && el.type[0]?.code === "boolean") { const word = completionContext.matchBefore(/[\w]*/); const options: Completion[] = ["true", "false"].map((v) => ({ label: v, type: "keyword", - apply: ( - view: EditorView, - _c: Completion, - from: number, - to: number, - ) => { + apply: (view: EditorView, _c: Completion, from: number, to: number) => { const d = view.state.doc.toString(); let actualFrom = from; let actualTo = to; @@ -1653,8 +1638,8 @@ async function handleExtensionUrlCompletion( let ins: string; let cOff: number; if (extInfo.isNested) { - const inner = ind + " "; - const innerInner = inner + " "; + const inner = `${ind} `; + const innerInner = `${inner} `; ins = `,\n${ind}"extension": [\n${inner}{\n${innerInner}"url": ""\n${inner}}\n${ind}]`; cOff = ins.lastIndexOf('""') + 1; } else if (extInfo.valueTypes.length === 1) { @@ -1667,9 +1652,9 @@ async function handleExtensionUrlCompletion( ins = `,\n${ind}"${vf}": `; cOff = ins.length; } else { - const inner = ind + " "; + const inner = `${ind} `; ins = `,\n${ind}"${vf}": {\n${inner}\n${ind}}`; - cOff = ins.indexOf(inner + "\n" + ind) + inner.length; + cOff = ins.indexOf(`${inner}\n${ind}`) + inner.length; } } else { return; @@ -1757,9 +1742,9 @@ async function handleNestedExtensionSlices( ins = `,\n${ind}"${vf}": `; cOff = ins.length; } else { - const inner = ind + " "; + const inner = `${ind} `; ins = `,\n${ind}"${vf}": {\n${inner}\n${ind}}`; - cOff = ins.indexOf(inner + "\n" + ind) + inner.length; + cOff = ins.indexOf(`${inner}\n${ind}`) + inner.length; } view.dispatch({ changes: { from: cp, insert: ins }, @@ -1806,17 +1791,11 @@ async function handleArrayItemCompletion( options.push({ label: slice.fixedName, type: "text", - detail: slice.min > 0 - ? `${slice.min}..${slice.max}` - : `0..${slice.max}`, + detail: + slice.min > 0 ? `${slice.min}..${slice.max}` : `0..${slice.max}`, boost: slice.min > 0 ? 2 : 0, ...(slice.short ? { info: slice.short } : {}), - apply: ( - view: EditorView, - _c: Completion, - from: number, - to: number, - ) => { + apply: (view: EditorView, _c: Completion, from: number, to: number) => { const line = view.state.doc.lineAt(from); const indent = line.text.match(/^(\s*)/)?.[1] ?? ""; const { text, cursorOffset } = buildParameterSnippet( @@ -1840,12 +1819,7 @@ async function handleArrayItemCompletion( type: "text", boost: -1, info: "Custom parameter", - apply: ( - view: EditorView, - _c: Completion, - from: number, - to: number, - ) => { + apply: (view: EditorView, _c: Completion, from: number, to: number) => { const line = view.state.doc.lineAt(from); const indent = line.text.match(/^(\s*)/)?.[1] ?? ""; const { text, cursorOffset } = buildParameterSnippet("", [], indent); diff --git a/packages/react-components/src/components/code-editor/http/index.ts b/packages/react-components/src/components/code-editor/http/index.ts index 21f24ab2..88b36568 100644 --- a/packages/react-components/src/components/code-editor/http/index.ts +++ b/packages/react-components/src/components/code-editor/http/index.ts @@ -234,25 +234,29 @@ const HEADER_VALUES: Record = { ], }; -const HTTP_METHODS: Completion[] = ["GET", "POST", "PUT", "PATCH", "DELETE"].map( - (method) => ({ - label: method, - type: "keyword" as const, - apply: (view: EditorView, _c: Completion, from: number, to: number) => { - const line = view.state.doc.lineAt(from); - const afterTo = line.text.slice(to - line.from); - // Skip whitespace after the method word to avoid double spaces - const wsMatch = afterTo.match(/^(\s*)/); - const actualTo = to + (wsMatch?.[1]?.length ?? 0); - const rest = line.text.slice(actualTo - line.from); - const insert = rest.startsWith("/") ? `${method} ` : `${method} /`; - view.dispatch({ - changes: { from, to: actualTo, insert }, - selection: { anchor: from + insert.length }, - }); - }, - }), -); +const HTTP_METHODS: Completion[] = [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE", +].map((method) => ({ + label: method, + type: "keyword" as const, + apply: (view: EditorView, _c: Completion, from: number, to: number) => { + const line = view.state.doc.lineAt(from); + const afterTo = line.text.slice(to - line.from); + // Skip whitespace after the method word to avoid double spaces + const wsMatch = afterTo.match(/^(\s*)/); + const actualTo = to + (wsMatch?.[1]?.length ?? 0); + const rest = line.text.slice(actualTo - line.from); + const insert = rest.startsWith("/") ? `${method} ` : `${method} /`; + view.dispatch({ + changes: { from, to: actualTo, insert }, + selection: { anchor: from + insert.length }, + }); + }, +})); function httpCompletionSource( context: CompletionContext, @@ -333,7 +337,9 @@ export type GetUrlSuggestions = ( function httpUrlCompletionSource( getUrlSuggestions: GetUrlSuggestions, ): (context: CompletionContext) => Promise { - return async (context: CompletionContext): Promise => { + return async ( + context: CompletionContext, + ): Promise => { const { state, pos } = context; const tree = syntaxTree(state); const node = tree.resolveInner(pos, -1); @@ -376,7 +382,10 @@ function httpUrlCompletionSource( const hasQuery = currentPath.includes("?"); let from: number; if (hasQuery) { - const lastSep = Math.max(currentPath.lastIndexOf("?"), currentPath.lastIndexOf("&")); + const lastSep = Math.max( + currentPath.lastIndexOf("?"), + currentPath.lastIndexOf("&"), + ); from = pathStart + lastSep + 1; } else { const lastSlash = currentPath.lastIndexOf("/"); diff --git a/packages/react-components/src/components/code-editor/index.tsx b/packages/react-components/src/components/code-editor/index.tsx index a0e556f5..61688c68 100644 --- a/packages/react-components/src/components/code-editor/index.tsx +++ b/packages/react-components/src/components/code-editor/index.tsx @@ -57,6 +57,7 @@ import { type ViewUpdate, } from "@codemirror/view"; import { tags } from "@lezer/highlight"; +import { vim } from "@replit/codemirror-vim"; import { ChevronDown, ChevronsRight, @@ -69,8 +70,6 @@ import { import * as React from "react"; import { flushSync } from "react-dom"; import { createRoot } from "react-dom/client"; - -import { vim } from "@replit/codemirror-vim"; import { ComplexTypeIcon, ResourceIcon, @@ -287,11 +286,7 @@ function handleErrorTooltipMove(event: Event, view: EditorView) { if (!Number.isNaN(lineNo)) { const message = getErrorMessageForLine(view, lineNo); if (message) { - showErrorTooltip( - message, - mouseEvent.clientX, - mouseEvent.clientY, - ); + showErrorTooltip(message, mouseEvent.clientX, mouseEvent.clientY); return false; } } @@ -306,11 +301,7 @@ function handleErrorTooltipMove(event: Event, view: EditorView) { const lineNo = view.state.doc.lineAt(pos).number; const message = getErrorMessageForLine(view, lineNo); if (message) { - showErrorTooltip( - message, - mouseEvent.clientX, - mouseEvent.clientY, - ); + showErrorTooltip(message, mouseEvent.clientX, mouseEvent.clientY); return false; } } @@ -871,7 +862,9 @@ function computeYamlNewlineIndent(lineText: string): string { // After "key:" with no value — increase indent // For " - key:", base indent is at the dash content level const dashMatch = trimmed.match(/^(\s*-\s+)/); - const baseIndent = dashMatch?.[1] ? " ".repeat(dashMatch[1].length) : indent; + const baseIndent = dashMatch?.[1] + ? " ".repeat(dashMatch[1].length) + : indent; return `${baseIndent} `; } if (/^\s*-\s*$/.test(trimmed)) { @@ -885,51 +878,56 @@ function computeYamlNewlineIndent(lineText: string): string { } function yamlEnterKeymap(): Extension { - return keymap.of([{ - key: "Enter", - run: (view) => { - const { state } = view; - const pos = state.selection.main.head; - const line = state.doc.lineAt(pos); - const newIndent = computeYamlNewlineIndent(line.text); - - view.dispatch({ - changes: { from: pos, insert: `\n${newIndent}` }, - selection: { anchor: pos + 1 + newIndent.length }, - }); - return true; + return keymap.of([ + { + key: "Enter", + run: (view) => { + const { state } = view; + const pos = state.selection.main.head; + const line = state.doc.lineAt(pos); + const newIndent = computeYamlNewlineIndent(line.text); + + view.dispatch({ + changes: { from: pos, insert: `\n${newIndent}` }, + selection: { anchor: pos + 1 + newIndent.length }, + }); + return true; + }, }, - }]); + ]); } function httpYamlEnterKeymap(): Extension { - return keymap.of([{ - key: "Enter", - run: (view) => { - const { state } = view; - const pos = state.selection.main.head; - const doc = state.doc.toString(); - - // Only handle if cursor is in YAML body (after blank line separator) - const textBeforeCursor = doc.slice(0, pos); - const blankLineIdx = textBeforeCursor.indexOf("\n\n"); - if (blankLineIdx === -1 || pos <= blankLineIdx + 1) return false; - - // Check if the body looks like YAML (not JSON) - const bodyStart = blankLineIdx + 2; - const bodyPrefix = doc.slice(bodyStart, bodyStart + 20).trimStart(); - if (bodyPrefix.startsWith("{") || bodyPrefix.startsWith("[")) return false; - - const line = state.doc.lineAt(pos); - const newIndent = computeYamlNewlineIndent(line.text); - - view.dispatch({ - changes: { from: pos, insert: `\n${newIndent}` }, - selection: { anchor: pos + 1 + newIndent.length }, - }); - return true; + return keymap.of([ + { + key: "Enter", + run: (view) => { + const { state } = view; + const pos = state.selection.main.head; + const doc = state.doc.toString(); + + // Only handle if cursor is in YAML body (after blank line separator) + const textBeforeCursor = doc.slice(0, pos); + const blankLineIdx = textBeforeCursor.indexOf("\n\n"); + if (blankLineIdx === -1 || pos <= blankLineIdx + 1) return false; + + // Check if the body looks like YAML (not JSON) + const bodyStart = blankLineIdx + 2; + const bodyPrefix = doc.slice(bodyStart, bodyStart + 20).trimStart(); + if (bodyPrefix.startsWith("{") || bodyPrefix.startsWith("[")) + return false; + + const line = state.doc.lineAt(pos); + const newIndent = computeYamlNewlineIndent(line.text); + + view.dispatch({ + changes: { from: pos, insert: `\n${newIndent}` }, + selection: { anchor: pos + 1 + newIndent.length }, + }); + return true; + }, }, - }]); + ]); } type LanguageMode = "json" | "http" | "sql" | "yaml"; @@ -976,10 +974,13 @@ function languageExtensions( } else { return [ json(), - linter((view) => { - if (!view.state.doc.toString().trim()) return []; - return jsonParseLinter()(view); - }, { delay: 300 }), + linter( + (view) => { + if (!view.state.doc.toString().trim()) return []; + return jsonParseLinter()(view); + }, + { delay: 300 }, + ), syntaxHighlighting(customHighlightStyle), jsonAutoExpandBraces(), ]; @@ -1006,10 +1007,7 @@ function jsonAutoExpandBraces(): Extension { const tree = syntaxTree(tr.startState); const nodeBefore = tree.resolveInner(braceFrom, -1); - if ( - nodeBefore.name === "String" || - nodeBefore.parent?.name === "String" - ) { + if (nodeBefore.name === "String" || nodeBefore.parent?.name === "String") { return tr; } @@ -1020,7 +1018,10 @@ function jsonAutoExpandBraces(): Extension { // Check if { is inside an extension array — insert {"url": ""} snippet const docText = tr.startState.doc.toString(); const textBefore = docText.slice(0, braceFrom); - const isInExtArray = /"(?:extension|modifierExtension)"\s*:\s*\[\s*(?:\{[\s\S]*?\}\s*,?\s*)*$/s.test(textBefore); + const isInExtArray = + /"(?:extension|modifierExtension)"\s*:\s*\[\s*(?:\{[\s\S]*?\}\s*,?\s*)*$/s.test( + textBefore, + ); if (isInExtArray) { const insert = `{\n${inner}"url": ""\n${indent}}`; return { @@ -1030,7 +1031,11 @@ function jsonAutoExpandBraces(): Extension { } return { - changes: { from: braceFrom, to: braceTo, insert: `{\n${inner}\n${indent}}` }, + changes: { + from: braceFrom, + to: braceTo, + insert: `{\n${inner}\n${indent}}`, + }, selection: { anchor: braceFrom + 2 + inner.length }, }; }); @@ -1060,7 +1065,10 @@ type CodeEditorProps = { export type CodeEditorView = EditorView; -export type { ExpandValueSet, GetStructureDefinitions } from "./fhir-autocomplete"; +export type { + ExpandValueSet, + GetStructureDefinitions, +} from "./fhir-autocomplete"; export type { GetUrlSuggestions } from "./http"; export type { SqlConfig, @@ -1134,23 +1142,23 @@ export function CodeEditor({ readOnlyCompartment.current.of(EditorState.readOnly.of(false)), ...(enableLineNumbers ? [lineNumbers()] : []), ...(enableFoldGutter - ? [ - foldGutter({ - markerDOM: (open) => { - const el = document.createElement("span"); - el.style.display = "flex"; - el.style.alignItems = "center"; - el.style.justifyContent = "center"; - el.style.width = "100%"; - el.style.height = "100%"; - el.innerHTML = open - ? '' - : ''; - return el; - }, - }), - ] - : []), + ? [ + foldGutter({ + markerDOM: (open) => { + const el = document.createElement("span"); + el.style.display = "flex"; + el.style.alignItems = "center"; + el.style.justifyContent = "center"; + el.style.width = "100%"; + el.style.height = "100%"; + el.innerHTML = open + ? '' + : ''; + return el; + }, + }), + ] + : []), highlightSpecialChars(), history(), drawSelection(), @@ -1239,7 +1247,7 @@ export function CodeEditor({ view.destroy(); setView(() => null); }; - }, [enableFoldGutter, enableLineNumbers]); + }, [enableFoldGutter, enableLineNumbers, vimMode]); React.useEffect(() => { executeSqlRef.current = sql?.executeSql; @@ -1276,14 +1284,18 @@ export function CodeEditor({ return () => { cancelled = true; }; - }, [view, sql]); + }, [view, sql, safeDispatch]); React.useEffect(() => { if (!view) return; if (getStructureDefinitions) { safeDispatch({ effects: fhirCompletionCompartment.current.reconfigure( - buildFhirCompletionExtension(getStructureDefinitions, resourceTypeHint, expandValueSet), + buildFhirCompletionExtension( + getStructureDefinitions, + resourceTypeHint, + expandValueSet, + ), ), }); } else { @@ -1291,7 +1303,13 @@ export function CodeEditor({ effects: fhirCompletionCompartment.current.reconfigure([]), }); } - }, [view, getStructureDefinitions, resourceTypeHint, expandValueSet, safeDispatch]); + }, [ + view, + getStructureDefinitions, + resourceTypeHint, + expandValueSet, + safeDispatch, + ]); React.useEffect(() => { if (viewCallback && view) { @@ -1309,7 +1327,7 @@ export function CodeEditor({ }), ]), }); - }, [view, onChange, safeDispatch]); + }, [onChange, safeDispatch]); React.useEffect(() => { safeDispatch({ @@ -1321,7 +1339,7 @@ export function CodeEditor({ }), ]), }); - }, [view, onUpdate, safeDispatch]); + }, [onUpdate, safeDispatch]); // FIXME: it is probably better to have CM manage its state. React.useEffect(() => { @@ -1348,7 +1366,7 @@ export function CodeEditor({ if (!getUrlSuggestions) return undefined; return ((path: string, method: string) => getUrlSuggestionsRef.current?.(path, method) ?? []) as GetUrlSuggestions; - }, [!!getUrlSuggestions]); + }, [getUrlSuggestions]); React.useEffect(() => { if (view === null) { @@ -1392,9 +1410,7 @@ export function CodeEditor({ return; } safeDispatch({ - effects: [ - vimCompartment.current.reconfigure(vimMode ? vim() : []), - ], + effects: [vimCompartment.current.reconfigure(vimMode ? vim() : [])], }); }, [vimMode, view, safeDispatch]); diff --git a/packages/react-components/src/components/code-editor/json-ast.ts b/packages/react-components/src/components/code-editor/json-ast.ts index ccd3ed23..ad01467b 100644 --- a/packages/react-components/src/components/code-editor/json-ast.ts +++ b/packages/react-components/src/components/code-editor/json-ast.ts @@ -273,6 +273,7 @@ function findStringArrayInObject( const urls: string[] = []; const re = /"([^"]+)"/g; let m: RegExpExecArray | null; + // biome-ignore lint/suspicious/noAssignInExpressions: standard regex exec loop while ((m = re.exec(match[1])) !== null) { if (m[1]) urls.push(m[1]); } diff --git a/packages/react-components/src/components/date-picker-input.tsx b/packages/react-components/src/components/date-picker-input.tsx index 1ced0340..8aa3f07b 100644 --- a/packages/react-components/src/components/date-picker-input.tsx +++ b/packages/react-components/src/components/date-picker-input.tsx @@ -48,9 +48,7 @@ function DatePickerInput({ }: DatePickerInputProps) { const [open, setOpen] = React.useState(false); const selectedDate = parseDate(value); - const [month, setMonth] = React.useState( - selectedDate ?? new Date(), - ); + const [month, setMonth] = React.useState(selectedDate ?? new Date()); return (
@@ -83,11 +81,7 @@ function DatePickerInput({ - + { - const d = dragRef.current; - if (!d) return; - const dx = e.clientX - d.startX; - if (pendingRef.current) { - if (Math.abs(dx) < DRAG_THRESHOLD) return; - pendingRef.current = false; - } - const draggedLeft = d.lefts[d.index] ?? 0; - const draggedWidth = d.widths[d.index] ?? 0; - const draggedRightEdge = draggedLeft + draggedWidth + dx; - const draggedLeftEdge = draggedLeft + dx; - let newIndex = d.index; - const TRIGGER_RATIO = 0.3; - for (let i = 0; i < d.lefts.length; i++) { - if (i === d.index) continue; - const left = d.lefts[i] ?? 0; - const width = d.widths[i] ?? 0; - if (i > d.index) { - // Dragging right: trigger when right edge enters 30% of target - if (draggedRightEdge > left + width * TRIGGER_RATIO) newIndex = i; - } else { - // Dragging left: trigger when left edge enters 30% from right - if (draggedLeftEdge < left + width * (1 - TRIGGER_RATIO)) - newIndex = Math.min(newIndex, i); - } + const handlePointerMove = React.useCallback((e: React.PointerEvent) => { + const d = dragRef.current; + if (!d) return; + const dx = e.clientX - d.startX; + if (pendingRef.current) { + if (Math.abs(dx) < DRAG_THRESHOLD) return; + pendingRef.current = false; + } + const draggedLeft = d.lefts[d.index] ?? 0; + const draggedWidth = d.widths[d.index] ?? 0; + const draggedRightEdge = draggedLeft + draggedWidth + dx; + const draggedLeftEdge = draggedLeft + dx; + let newIndex = d.index; + const TRIGGER_RATIO = 0.3; + for (let i = 0; i < d.lefts.length; i++) { + if (i === d.index) continue; + const left = d.lefts[i] ?? 0; + const width = d.widths[i] ?? 0; + if (i > d.index) { + // Dragging right: trigger when right edge enters 30% of target + if (draggedRightEdge > left + width * TRIGGER_RATIO) newIndex = i; + } else { + // Dragging left: trigger when left edge enters 30% from right + if (draggedLeftEdge < left + width * (1 - TRIGGER_RATIO)) + newIndex = Math.min(newIndex, i); } - const next: DragState = { ...d, offsetX: dx, currentIndex: newIndex }; - dragRef.current = next; - setDrag(next); - }, - [], - ); + } + const next: DragState = { ...d, offsetX: dx, currentIndex: newIndex }; + dragRef.current = next; + setDrag(next); + }, []); const handlePointerUp = React.useCallback(() => { const d = dragRef.current; @@ -564,7 +561,14 @@ function useTabReorder( [drag], ); - return { drag, itemsRef, handlePointerDown, handlePointerMove, handlePointerUp, getTransform }; + return { + drag, + itemsRef, + handlePointerDown, + handlePointerMove, + handlePointerUp, + getTransform, + }; } function TabsBrowserList({ @@ -581,25 +585,32 @@ function TabsBrowserList({ const [canScrollLeft, setCanScrollLeft] = React.useState(false); const [canScrollRight, setCanScrollRight] = React.useState(false); - const { drag, itemsRef, handlePointerDown, handlePointerMove, handlePointerUp, getTransform } = - useTabReorder(onReorder); - - const wrappedChildren = onReorder && React.Children.count(children) > 1 - ? React.Children.map(children, (child, index) => ( -
{ itemsRef.current[index] = el; }} - style={getTransform(index)} - onPointerDown={(e) => handlePointerDown(e, index)} - onPointerMove={handlePointerMove} - onPointerUp={handlePointerUp} - className={cn( - drag?.index === index && "cursor-grabbing", - )} - > - {child} -
- )) - : children; + const { + drag, + itemsRef, + handlePointerDown, + handlePointerMove, + handlePointerUp, + getTransform, + } = useTabReorder(onReorder); + + const wrappedChildren = + onReorder && React.Children.count(children) > 1 + ? React.Children.map(children, (child, index) => ( +
{ + itemsRef.current[index] = el; + }} + style={getTransform(index)} + onPointerDown={(e) => handlePointerDown(e, index)} + onPointerMove={handlePointerMove} + onPointerUp={handlePointerUp} + className={cn(drag?.index === index && "cursor-grabbing")} + > + {child} +
+ )) + : children; return ( diff --git a/packages/react-components/tsconfig.app.json b/packages/react-components/tsconfig.app.json index e4926616..5a56b728 100644 --- a/packages/react-components/tsconfig.app.json +++ b/packages/react-components/tsconfig.app.json @@ -35,5 +35,10 @@ } }, "include": ["src"], - "exclude": ["src/**/*.stories.tsx", "src/**/*.stories.ts", "src/**/*.test.ts", "src/**/*.test.tsx"] + "exclude": [ + "src/**/*.stories.tsx", + "src/**/*.stories.ts", + "src/**/*.test.ts", + "src/**/*.test.tsx" + ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f64e636..72d3969a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,9 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + fast-xml-parser: '>=5.5.6' + importers: .: @@ -3066,8 +3069,11 @@ packages: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} - fast-xml-parser@5.3.9: - resolution: {integrity: sha512-zU0KUuO9U+fLGduTDdxQ6qsQLIxRg4EK5AMduwBNGNCSfCGRSbNS7OpH343NFQlLDg1jxoH68JSbOPAGksIGvg==} + fast-xml-builder@1.1.4: + resolution: {integrity: sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==} + + fast-xml-parser@5.5.8: + resolution: {integrity: sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==} hasBin: true fastq@1.20.1: @@ -3505,6 +3511,10 @@ packages: resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==} engines: {node: '>=12.20'} + path-expression-matcher@1.2.0: + resolution: {integrity: sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==} + engines: {node: '>=14.0.0'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -4285,7 +4295,7 @@ snapshots: '@atomic-ehr/fhir-canonical-manager': 0.0.11(typescript@5.9.3) '@atomic-ehr/fhirschema': 0.0.2(typescript@5.9.3) '@atomic-ehr/ucum': 0.2.5(typescript@5.9.3) - fast-xml-parser: 5.3.9 + fast-xml-parser: 5.5.8 typescript: 5.9.3 '@atomic-ehr/fhirschema@0.0.2(typescript@5.9.3)': @@ -6883,8 +6893,14 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 - fast-xml-parser@5.3.9: + fast-xml-builder@1.1.4: dependencies: + path-expression-matcher: 1.2.0 + + fast-xml-parser@5.5.8: + dependencies: + fast-xml-builder: 1.1.4 + path-expression-matcher: 1.2.0 strnum: 2.2.0 fastq@1.20.1: @@ -7225,6 +7241,8 @@ snapshots: p-cancelable@3.0.0: {} + path-expression-matcher@1.2.0: {} + path-key@3.1.1: {} path-parse@1.0.7: {} From 4390f6c4e15b4224266325cb33319f0b88378f14 Mon Sep 17 00:00:00 2001 From: Panthevm Date: Mon, 30 Mar 2026 12:16:15 +0300 Subject: [PATCH 22/55] Fix code editor gutter background for horizontal scroll Set gutter backgroundColor to --color-bg-primary instead of transparent so that editor content does not show through line numbers when scrolling horizontally. --- packages/react-components/src/components/code-editor/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-components/src/components/code-editor/index.tsx b/packages/react-components/src/components/code-editor/index.tsx index 61688c68..f1b35bf7 100644 --- a/packages/react-components/src/components/code-editor/index.tsx +++ b/packages/react-components/src/components/code-editor/index.tsx @@ -349,7 +349,7 @@ const baseTheme = EditorView.theme({ fontFamily: "var(--font-family-mono)", }, ".cm-gutters": { - backgroundColor: "transparent", + backgroundColor: "var(--color-bg-primary)", border: "none", }, ".cm-lineNumbers": { From baeebad7f19a94b66d33b72b6916fbf5c3afcd24 Mon Sep 17 00:00:00 2001 From: Panthevm Date: Mon, 30 Mar 2026 18:35:26 +0300 Subject: [PATCH 23/55] Fix audit vulnerabilities: override picomatch, file-type, brace-expansion, yaml --- package.json | 6 +- packages/aidbox-client/package.json | 2 +- pnpm-lock.yaml | 192 +++++++++++++--------------- 3 files changed, 92 insertions(+), 108 deletions(-) diff --git a/package.json b/package.json index d00e3f83..a5a69173 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,11 @@ "packageManager": "pnpm@10.21.0", "pnpm": { "overrides": { - "fast-xml-parser": ">=5.5.6" + "fast-xml-parser": ">=5.5.6", + "yaml": ">=2.8.3", + "picomatch": ">=4.0.4", + "file-type": ">=21.3.2", + "brace-expansion": ">=2.0.3" } }, "devDependencies": { diff --git a/packages/aidbox-client/package.json b/packages/aidbox-client/package.json index ca00311a..f1df5485 100644 --- a/packages/aidbox-client/package.json +++ b/packages/aidbox-client/package.json @@ -33,7 +33,7 @@ "dependencies": { "@types/json-patch": "^0.0.33", "oauth4webapi": "^3.8.5", - "yaml": "^2.8.2" + "yaml": "^2.8.3" }, "publishConfig": { "access": "public" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 72d3969a..e7add400 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,6 +6,10 @@ settings: overrides: fast-xml-parser: '>=5.5.6' + yaml: '>=2.8.3' + picomatch: '>=4.0.4' + file-type: '>=21.3.2' + brace-expansion: '>=2.0.3' importers: @@ -30,8 +34,8 @@ importers: specifier: ^3.8.5 version: 3.8.5 yaml: - specifier: ^2.8.2 - version: 2.8.2 + specifier: '>=2.8.3' + version: 2.8.3 devDependencies: '@atomic-ehr/codegen': specifier: ^0.0.8 @@ -50,7 +54,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + version: 4.0.18(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.3) packages/aidbox-fhirpath-lsp: dependencies: @@ -120,10 +124,10 @@ importers: version: 5.9.3 vite: specifier: ^7.3.1 - version: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + version: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: ^4.0.18 - version: 4.0.18(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + version: 4.0.18(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.3) vscode-languageserver-types: specifier: ^3.17.5 version: 3.17.5 @@ -331,10 +335,10 @@ importers: version: 1.8.0 '@storybook/addon-docs': specifier: ^10.2.17 - version: 10.2.17(@types/react@19.2.14)(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 10.2.17(@types/react@19.2.14)(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.3)) '@storybook/react-vite': specifier: ^10.2.17 - version: 10.2.17(esbuild@0.27.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.59.0)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 10.2.17(esbuild@0.27.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.59.0)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.3)) '@swc/cli': specifier: ^0.8.0 version: 0.8.0(@swc/core@1.15.18)(chokidar@5.0.0) @@ -346,7 +350,7 @@ importers: version: 4.2.1 '@tailwindcss/vite': specifier: ^4.2.1 - version: 4.2.1(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.2.1(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.3)) '@types/node': specifier: ^25.4.0 version: 25.4.0 @@ -358,7 +362,7 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react-swc': specifier: ^4.2.3 - version: 4.2.3(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.2.3(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.3)) chokidar: specifier: ^5.0.0 version: 5.0.0 @@ -382,10 +386,10 @@ importers: version: 5.9.3 vite: specifier: ^7.3.1 - version: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + version: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: ^4.0.18 - version: 4.0.18(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + version: 4.0.18(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.3) packages: @@ -2415,8 +2419,8 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' - '@tokenizer/inflate@0.2.7': - resolution: {integrity: sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==} + '@tokenizer/inflate@0.4.1': + resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} engines: {node: '>=18'} '@tokenizer/token@0.3.0': @@ -2671,9 +2675,6 @@ packages: react-native-b4a: optional: true - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - balanced-match@4.0.4: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} @@ -2739,11 +2740,8 @@ packages: birpc@4.0.0: resolution: {integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==} - brace-expansion@2.0.2: - resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} - - brace-expansion@5.0.4: - resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} engines: {node: 18 || 20 || >=22} braces@3.0.3: @@ -3083,17 +3081,14 @@ packages: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} peerDependencies: - picomatch: ^3 || ^4 + picomatch: '>=4.0.4' peerDependenciesMeta: picomatch: optional: true - fflate@0.8.2: - resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} - - file-type@20.5.0: - resolution: {integrity: sha512-BfHZtG/l9iMm4Ecianu7P8HRD2tBHLtjXinm4X62XBOYzi7CYA7jyqfJzOvXHqzVrVPYqBo2/GvbARMaaJkKVg==} - engines: {node: '>=18'} + file-type@22.0.0: + resolution: {integrity: sha512-cmBmnYo8Zymabm2+qAP7jTFbKF10bQpYmxoGfuZbRFRcq00BRddJdGNH/P7GA1EMpJy5yQbqa9B7yROb3z8Ziw==} + engines: {node: '>=22'} filename-reserved-regex@3.0.0: resolution: {integrity: sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==} @@ -3543,12 +3538,8 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - - picomatch@4.0.3: - resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} piscina@4.9.2: @@ -3890,8 +3881,8 @@ packages: strnum@2.2.0: resolution: {integrity: sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==} - strtok3@10.3.4: - resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==} + strtok3@10.3.5: + resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==} engines: {node: '>=18'} style-mod@4.1.3: @@ -4110,7 +4101,7 @@ packages: sugarss: ^5.0.0 terser: ^5.16.0 tsx: ^4.8.1 - yaml: ^2.4.2 + yaml: '>=2.8.3' peerDependenciesMeta: '@types/node': optional: true @@ -4229,8 +4220,8 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yaml@2.8.2: - resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} engines: {node: '>= 14.6'} hasBin: true @@ -4260,7 +4251,7 @@ snapshots: mustache: 4.2.0 picocolors: 1.1.1 tinyglobby: 0.2.15 - yaml: 2.8.2 + yaml: 2.8.3 yargs: 18.0.0 transitivePeerDependencies: - typescript @@ -4693,11 +4684,11 @@ snapshots: '@standard-schema/utils': 0.3.0 react-hook-form: 7.71.2(react@19.2.4) - '@joshwooding/vite-plugin-react-docgen-typescript@0.6.4(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))': + '@joshwooding/vite-plugin-react-docgen-typescript@0.6.4(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: glob: 13.0.6 react-docgen-typescript: 2.4.0(typescript@5.9.3) - vite: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.3) optionalDependencies: typescript: 5.9.3 @@ -4892,7 +4883,7 @@ snapshots: detect-libc: 2.1.2 is-glob: 4.0.3 node-addon-api: 7.1.1 - picomatch: 4.0.3 + picomatch: 4.0.4 optionalDependencies: '@parcel/watcher-android-arm64': 2.5.6 '@parcel/watcher-darwin-arm64': 2.5.6 @@ -5806,7 +5797,7 @@ snapshots: dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 - picomatch: 4.0.3 + picomatch: 4.0.4 optionalDependencies: rollup: 4.59.0 @@ -5911,10 +5902,10 @@ snapshots: '@standard-schema/utils@0.3.0': {} - '@storybook/addon-docs@10.2.17(@types/react@19.2.14)(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))': + '@storybook/addon-docs@10.2.17(@types/react@19.2.14)(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@mdx-js/react': 3.1.1(@types/react@19.2.14)(react@19.2.4) - '@storybook/csf-plugin': 10.2.17(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + '@storybook/csf-plugin': 10.2.17(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.3)) '@storybook/icons': 2.0.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@storybook/react-dom-shim': 10.2.17(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) react: 19.2.4 @@ -5928,25 +5919,25 @@ snapshots: - vite - webpack - '@storybook/builder-vite@10.2.17(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))': + '@storybook/builder-vite@10.2.17(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - '@storybook/csf-plugin': 10.2.17(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + '@storybook/csf-plugin': 10.2.17(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.3)) storybook: 10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) ts-dedent: 2.2.0 - vite: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - esbuild - rollup - webpack - '@storybook/csf-plugin@10.2.17(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))': + '@storybook/csf-plugin@10.2.17(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: storybook: 10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) unplugin: 2.3.11 optionalDependencies: esbuild: 0.27.3 rollup: 4.59.0 - vite: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.3) '@storybook/global@5.0.0': {} @@ -5961,11 +5952,11 @@ snapshots: react-dom: 19.2.4(react@19.2.4) storybook: 10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@storybook/react-vite@10.2.17(esbuild@0.27.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.59.0)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))': + '@storybook/react-vite@10.2.17(esbuild@0.27.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.59.0)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.4(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.4(typescript@5.9.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.3)) '@rollup/pluginutils': 5.3.0(rollup@4.59.0) - '@storybook/builder-vite': 10.2.17(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + '@storybook/builder-vite': 10.2.17(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.3)) '@storybook/react': 10.2.17(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) empathic: 2.0.0 magic-string: 0.30.21 @@ -5975,7 +5966,7 @@ snapshots: resolve: 1.22.11 storybook: 10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) tsconfig-paths: 4.2.0 - vite: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - esbuild - rollup @@ -6145,12 +6136,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.2.1 '@tailwindcss/oxide-win32-x64-msvc': 4.2.1 - '@tailwindcss/vite@4.2.1(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))': + '@tailwindcss/vite@4.2.1(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@tailwindcss/node': 4.2.1 '@tailwindcss/oxide': 4.2.1 tailwindcss: 4.2.1 - vite: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.3) '@tanstack/react-table@8.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: @@ -6184,10 +6175,9 @@ snapshots: dependencies: '@testing-library/dom': 10.4.1 - '@tokenizer/inflate@0.2.7': + '@tokenizer/inflate@0.4.1': dependencies: debug: 4.4.3 - fflate: 0.8.2 token-types: 6.1.2 transitivePeerDependencies: - supports-color @@ -6287,11 +6277,11 @@ snapshots: '@types/use-sync-external-store@0.0.6': {} - '@vitejs/plugin-react-swc@4.2.3(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))': + '@vitejs/plugin-react-swc@4.2.3(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.2 '@swc/core': 1.15.18 - vite: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - '@swc/helpers' @@ -6312,13 +6302,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.3) '@vitest/pretty-format@3.2.4': dependencies: @@ -6358,7 +6348,7 @@ snapshots: '@xhmikosr/archive-type@7.1.0': dependencies: - file-type: 20.5.0 + file-type: 22.0.0 transitivePeerDependencies: - supports-color @@ -6381,7 +6371,7 @@ snapshots: '@xhmikosr/decompress-tar@8.1.0': dependencies: - file-type: 20.5.0 + file-type: 22.0.0 is-stream: 2.0.1 tar-stream: 3.1.8 transitivePeerDependencies: @@ -6393,7 +6383,7 @@ snapshots: '@xhmikosr/decompress-tarbz2@8.1.0': dependencies: '@xhmikosr/decompress-tar': 8.1.0 - file-type: 20.5.0 + file-type: 22.0.0 is-stream: 2.0.1 seek-bzip: 2.0.0 unbzip2-stream: 1.4.3 @@ -6406,7 +6396,7 @@ snapshots: '@xhmikosr/decompress-targz@8.1.0': dependencies: '@xhmikosr/decompress-tar': 8.1.0 - file-type: 20.5.0 + file-type: 22.0.0 is-stream: 2.0.1 transitivePeerDependencies: - bare-abort-controller @@ -6416,7 +6406,7 @@ snapshots: '@xhmikosr/decompress-unzip@7.1.0': dependencies: - file-type: 20.5.0 + file-type: 22.0.0 get-stream: 6.0.1 yauzl: 3.2.1 transitivePeerDependencies: @@ -6443,7 +6433,7 @@ snapshots: content-disposition: 0.5.4 defaults: 2.0.2 ext-name: 5.0.0 - file-type: 20.5.0 + file-type: 22.0.0 filenamify: 6.0.0 get-stream: 6.0.1 got: 13.0.0 @@ -6472,7 +6462,7 @@ snapshots: anymatch@3.1.3: dependencies: normalize-path: 3.0.0 - picomatch: 2.3.1 + picomatch: 4.0.4 arch@3.0.0: {} @@ -6504,8 +6494,6 @@ snapshots: b4a@1.8.0: {} - balanced-match@1.0.2: {} - balanced-match@4.0.4: {} bare-events@2.8.2: {} @@ -6560,11 +6548,7 @@ snapshots: birpc@4.0.0: {} - brace-expansion@2.0.2: - dependencies: - balanced-match: 1.0.2 - - brace-expansion@5.0.4: + brace-expansion@5.0.5: dependencies: balanced-match: 4.0.4 @@ -6907,16 +6891,14 @@ snapshots: dependencies: reusify: 1.1.0 - fdir@6.5.0(picomatch@4.0.3): + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: - picomatch: 4.0.3 + picomatch: 4.0.4 - fflate@0.8.2: {} - - file-type@20.5.0: + file-type@22.0.0: dependencies: - '@tokenizer/inflate': 0.2.7 - strtok3: 10.3.4 + '@tokenizer/inflate': 0.4.1 + strtok3: 10.3.5 token-types: 6.1.2 uint8array-extras: 1.5.0 transitivePeerDependencies: @@ -7173,7 +7155,7 @@ snapshots: micromatch@4.0.8: dependencies: braces: 3.0.3 - picomatch: 2.3.1 + picomatch: 4.0.4 mime-db@1.54.0: {} @@ -7187,11 +7169,11 @@ snapshots: minimatch@10.2.4: dependencies: - brace-expansion: 5.0.4 + brace-expansion: 5.0.5 minimatch@9.0.9: dependencies: - brace-expansion: 2.0.2 + brace-expansion: 5.0.5 minimist@1.2.8: {} @@ -7262,9 +7244,7 @@ snapshots: picocolors@1.1.1: {} - picomatch@2.3.1: {} - - picomatch@4.0.3: {} + picomatch@4.0.4: {} piscina@4.9.2: optionalDependencies: @@ -7444,7 +7424,7 @@ snapshots: readdirp@3.6.0: dependencies: - picomatch: 2.3.1 + picomatch: 4.0.4 readdirp@5.0.0: {} @@ -7690,7 +7670,7 @@ snapshots: strnum@2.2.0: {} - strtok3@10.3.4: + strtok3@10.3.5: dependencies: '@tokenizer/token': 0.3.0 @@ -7738,8 +7718,8 @@ snapshots: tinyglobby@0.2.15: dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 tinyrainbow@2.0.0: {} @@ -7786,7 +7766,7 @@ snapshots: hookable: 6.0.1 import-without-cache: 0.2.5 obug: 2.1.1 - picomatch: 4.0.3 + picomatch: 4.0.4 rolldown: 1.0.0-rc.9 rolldown-plugin-dts: 0.22.5(rolldown@1.0.0-rc.9)(typescript@5.9.3) semver: 7.7.4 @@ -7822,7 +7802,7 @@ snapshots: markdown-it: 14.1.1 minimatch: 9.0.9 typescript: 5.9.3 - yaml: 2.8.2 + yaml: 2.8.3 typescript@5.9.3: {} @@ -7846,7 +7826,7 @@ snapshots: dependencies: '@jridgewell/remapping': 2.3.5 acorn: 8.16.0 - picomatch: 4.0.3 + picomatch: 4.0.4 webpack-virtual-modules: 0.6.2 unrun@0.2.32: @@ -7904,11 +7884,11 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2): + vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.3): dependencies: esbuild: 0.27.3 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 postcss: 8.5.8 rollup: 4.59.0 tinyglobby: 0.2.15 @@ -7918,12 +7898,12 @@ snapshots: jiti: 2.6.1 lightningcss: 1.31.1 tsx: 4.21.0 - yaml: 2.8.2 + yaml: 2.8.3 - vitest@4.0.18(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2): + vitest@4.0.18(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.3): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -7934,13 +7914,13 @@ snapshots: magic-string: 0.30.21 obug: 2.1.1 pathe: 2.0.3 - picomatch: 4.0.3 + picomatch: 4.0.4 std-env: 3.10.0 tinybench: 2.9.0 tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.1.0 - vite: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.4.0 @@ -8001,7 +7981,7 @@ snapshots: yallist@3.1.1: {} - yaml@2.8.2: {} + yaml@2.8.3: {} yargs-parser@22.0.0: {} From 9eaf19df86ff27e3e561d889d5e37b365c7fe199 Mon Sep 17 00:00:00 2001 From: Panthevm Date: Thu, 2 Apr 2026 18:45:34 +0300 Subject: [PATCH 24/55] Fix CodeMirror SQL & HTTP autocomplete --- .../components/code-editor/fhir-autocomplete.ts | 15 ++++++++------- .../src/components/code-editor/index.tsx | 5 ++++- .../src/components/code-editor/sql-completion.ts | 5 +++++ 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/packages/react-components/src/components/code-editor/fhir-autocomplete.ts b/packages/react-components/src/components/code-editor/fhir-autocomplete.ts index 9a3b1c28..37b42f46 100644 --- a/packages/react-components/src/components/code-editor/fhir-autocomplete.ts +++ b/packages/react-components/src/components/code-editor/fhir-autocomplete.ts @@ -1068,6 +1068,7 @@ async function fhirComplete( // boundary detection — the inner RT will be picked up via getScope later. if (!resourceType) { resourceType = resourceTypeHint; + rtScope = ctx.getScope(ctx.fullPath.length); } else if (resourceTypeHint && ctx.fullPath.length > 0) { // Check if the found RT is actually from an inner scope, not the root. // If the root scope has no RT but hint is available, use hint as the @@ -2128,13 +2129,13 @@ function buildFhirValidationPlugin( if (destroyed) return; if (view.state.doc.toString() !== currentDoc) return; - for (const es of emptyStrings) { - rawDiags.push({ - from: es.from, - to: es.to, - message: "Value must not be empty", - }); - } + // for (const es of emptyStrings) { + // rawDiags.push({ + // from: es.from, + // to: es.to, + // message: "Value must not be empty", + // }); + // } const diags: FhirDiagnosticWithLine[] = rawDiags.map((d) => ({ ...d, diff --git a/packages/react-components/src/components/code-editor/index.tsx b/packages/react-components/src/components/code-editor/index.tsx index f1b35bf7..4767e3fd 100644 --- a/packages/react-components/src/components/code-editor/index.tsx +++ b/packages/react-components/src/components/code-editor/index.tsx @@ -62,6 +62,7 @@ import { ChevronDown, ChevronsRight, ChevronUp, + Columns2, Heading, Table2, Terminal, @@ -1495,6 +1496,7 @@ const KeywordIcon = () => ; const OperatorIcon = () => ; const TableIcon = () => ; const HeaderIcon = () => ; +const ColumnIcon = () => ; function getCompletionIcon(completion: Completion): React.FC | null { if (completion.type === "function") return SquareFunctionIcon; @@ -1507,9 +1509,10 @@ function getCompletionIcon(completion: Completion): React.FC | null { if (completion.type === "search-param") return null; const detail = completion.detail; if (!detail) { - if (completion.type === "variable") return SquareFunctionIcon; + if (completion.type === "variable") return ColumnIcon; return TypCodeIcon; } + if (completion.type === "variable") return ColumnIcon; const typeName = detail.replace(/\[\]$/, ""); if (!typeName) return TypCodeIcon; // Search param types (TOKEN, REFERENCE) — no icon diff --git a/packages/react-components/src/components/code-editor/sql-completion.ts b/packages/react-components/src/components/code-editor/sql-completion.ts index b3fc4441..3cb591a5 100644 --- a/packages/react-components/src/components/code-editor/sql-completion.ts +++ b/packages/react-components/src/components/code-editor/sql-completion.ts @@ -883,6 +883,11 @@ function sqlCompletionOverride(): Extension { }; } + const aliasColumnResult = results.find((r) => + r.options.length > 0 && r.options.every((o) => o.type === "variable"), + ); + if (aliasColumnResult) return aliasColumnResult; + const hasTableResults = results.some((r) => r.options.some((o) => o.type === "table"), ); From 84d713192568d9776fe938dbeed1e21728125462 Mon Sep 17 00:00:00 2001 From: Panthevm Date: Fri, 3 Apr 2026 15:46:58 +0300 Subject: [PATCH 25/55] Update tokens --- packages/react-components/src/index.css | 44 +++++++++---------- .../src/shadcn/components/ui/button.tsx | 2 +- .../src/shadcn/components/ui/input.tsx | 4 +- .../src/shadcn/components/ui/sidebar.tsx | 6 +-- .../src/shadcn/components/ui/table.tsx | 4 +- packages/react-components/src/tokens.css | 25 ++++++++--- 6 files changed, 49 insertions(+), 36 deletions(-) diff --git a/packages/react-components/src/index.css b/packages/react-components/src/index.css index 9ce08fee..81dcbabf 100644 --- a/packages/react-components/src/index.css +++ b/packages/react-components/src/index.css @@ -136,14 +136,14 @@ --hs-text-secondary-hover: var(--color-neutral-600); --hs-text-primary-on-brand: var(--color-neutral-50); --hs-text-secondary-on-brand: var(--color-neutral-100); - --hs-text-tertiary: var(--color-neutral-400); + --hs-text-tertiary: var(--color-neutral-500); --hs-text-tertiary-hover: var(--color-neutral-500); - --hs-text-quaternary: var(--color-neutral-400); - --hs-text-quaternary-hover: var(--color-neutral-500); + --hs-text-quaternary: var(--color-neutral-500); + --hs-text-quaternary-hover: var(--color-neutral-600); --hs-text-quaternary-on-brand: var(--color-neutral-200); --hs-text-disabled: var(--color-neutral-300); --hs-text-link: var(--color-blue-500); - --hs-text-link-hover: var(--color-blue-700); + --hs-text-link-hover: var(--color-blue-600); --hs-text-link-disabled: var(--color-blue-400); --hs-text-brand-primary: var(--color-brand-500); --hs-text-brand-secondary: var(--color-brand-300); @@ -187,7 +187,7 @@ --hs-bg-success-primary-inverse: var(--color-green-500); --hs-bg-success-secondary: var(--color-green-100); --hs-bg-warning-primary: var(--color-yellow-50); - --hs-bg-warning-primary-inverse: #f4cb00; + --hs-bg-warning-primary-inverse: var(--color-yellow-500); --hs-bg-warning-secondary: var(--color-yellow-100); --hs-bg-info-primary: var(--color-blue-100); --hs-bg-info-primary-inverse: var(--color-blue-500); @@ -196,23 +196,23 @@ --hs-fg-primary: var(--color-neutral-900); --hs-fg-primary-inverse: #ffffff; --hs-fg-primary-on-brand: var(--color-neutral-50); - --hs-fg-secondary: var(--color-neutral-700); + --hs-fg-secondary: var(--color-neutral-500); --hs-fg-secondary-inverse: var(--color-neutral-400); - --hs-fg-secondary-hover: var(--color-neutral-800); + --hs-fg-secondary-hover: var(--color-neutral-600); --hs-fg-secondary-on-brand: var(--color-neutral-100); --hs-fg-tertiary: var(--color-neutral-500); --hs-fg-tertiary-hover: var(--color-neutral-600); --hs-fg-tertiary-on-brand: var(--color-neutral-200); --hs-fg-disabled: var(--color-neutral-300); - --hs-fg-brand-primary: var(--color-brand-500); + --hs-fg-brand-primary: var(--color-red-400); --hs-fg-brand-secondary: var(--color-brand-300); --hs-fg-link: var(--color-blue-500); --hs-fg-error-primary: var(--color-red-500); --hs-fg-error-secondary: var(--color-red-400); --hs-fg-success-primary: var(--color-green-500); --hs-fg-success-secondary: var(--color-green-400); - --hs-fg-warning-primary: var(--color-yellow-700); - --hs-fg-warning-secondary: var(--color-yellow-400); + --hs-fg-warning-primary: var(--color-yellow-500); + --hs-fg-warning-secondary: var(--color-yellow-500); --hs-fg-info-primary: var(--color-blue-600); --hs-fg-neutral-primary: var(--color-neutral-500); --hs-syntax-property: #ea4a35; @@ -259,10 +259,10 @@ --hs-text-brand-primary: var(--color-brand-400); --hs-text-brand-secondary: var(--color-brand-300); --hs-text-brand-secondary-hover: var(--color-brand-200); - --hs-text-error-primary: var(--color-red-400); + --hs-text-error-primary: var(--color-red-300); --hs-text-error-secondary: var(--color-red-300); --hs-text-success-primary: var(--color-green-400); - --hs-text-warning-primary: var(--color-yellow-500); + --hs-text-warning-primary: var(--color-yellow-400); --hs-text-info-primary: var(--color-blue-400); /* Borders */ @@ -274,7 +274,7 @@ --hs-border-dark: oklch(0.556 0 0); /* Backgrounds — primary/secondary swapped */ - --hs-bg-primary: #1a1a1a; + --hs-bg-primary: #1a1b1e; --hs-bg-primary-inverse: oklch(0.985 0 0); --hs-bg-secondary: oklch(0.188 0 0); --hs-bg-secondary-inverse: oklch(0.556 0 0); @@ -286,22 +286,22 @@ --hs-bg-disabled: oklch(0.269 0 0); --hs-bg-hover: var(--color-blue-900); --hs-bg-dark-tertiary: oklch(0.371 0 0); - --hs-bg-brand-primary: var(--color-brand-950); + --hs-bg-brand-primary: var(--color-brand-900); --hs-bg-brand-primary-inverse: var(--color-brand-400); --hs-bg-brand-secondary: var(--color-brand-900); --hs-bg-brand-tertiary: var(--color-brand-800); --hs-bg-link: var(--color-blue-500); --hs-bg-link-hover: var(--color-blue-400); - --hs-bg-link-disabled: var(--color-blue-600); - --hs-bg-error-primary: var(--color-red-950); - --hs-bg-error-primary-inverse: var(--color-red-500); - --hs-bg-error-primary-inverse-hover: var(--color-red-400); + --hs-bg-link-disabled: var(--color-blue-800); + --hs-bg-error-primary: var(--color-red-900); + --hs-bg-error-primary-inverse: var(--color-red-400); + --hs-bg-error-primary-inverse-hover: var(--color-red-300); --hs-bg-error-secondary: var(--color-red-900); --hs-bg-error-tertiary: var(--color-red-800); --hs-bg-success-primary: var(--color-green-950); --hs-bg-success-primary-inverse: var(--color-green-500); - --hs-bg-success-secondary: var(--color-green-900); - --hs-bg-warning-primary: var(--color-yellow-950); + --hs-bg-success-secondary: var(--color-green-800); + --hs-bg-warning-primary: var(--color-yellow-900); --hs-bg-warning-primary-inverse: var(--color-yellow-500); --hs-bg-warning-secondary: var(--color-yellow-900); --hs-bg-info-primary: var(--color-blue-900); @@ -324,12 +324,12 @@ --hs-fg-brand-primary: var(--color-brand-400); --hs-fg-brand-secondary: var(--color-brand-300); --hs-fg-link: var(--color-blue-400); - --hs-fg-error-primary: var(--color-red-400); + --hs-fg-error-primary: var(--color-red-300); --hs-fg-error-secondary: var(--color-red-300); --hs-fg-success-primary: var(--color-green-400); --hs-fg-success-secondary: var(--color-green-300); --hs-fg-warning-primary: var(--color-yellow-500); - --hs-fg-warning-secondary: var(--color-yellow-400); + --hs-fg-warning-secondary: var(--color-yellow-500); --hs-fg-info-primary: var(--color-blue-400); --hs-fg-neutral-primary: oklch(0.556 0 0); --hs-syntax-property: #f87171; diff --git a/packages/react-components/src/shadcn/components/ui/button.tsx b/packages/react-components/src/shadcn/components/ui/button.tsx index 8d16ffac..b7d36fad 100644 --- a/packages/react-components/src/shadcn/components/ui/button.tsx +++ b/packages/react-components/src/shadcn/components/ui/button.tsx @@ -58,7 +58,7 @@ const buttonVariants = cva(baseButtonStyles, { ), }, size: { - regular: cn("h-9", "px-4", "typo-label"), + regular: cn("h-9", "px-4", "typo-body"), small: cn("h-6", "px-2", "gap-1", "typo-button-label-xs"), }, danger: { diff --git a/packages/react-components/src/shadcn/components/ui/input.tsx b/packages/react-components/src/shadcn/components/ui/input.tsx index bcac0023..63d93a4e 100644 --- a/packages/react-components/src/shadcn/components/ui/input.tsx +++ b/packages/react-components/src/shadcn/components/ui/input.tsx @@ -113,8 +113,8 @@ const prefixClasses = cn( "border-r-0", // Background & Colors - "bg-bg-tertiary", - "text-text-tertiary", + "bg-bg-primary", + "text-text-secondary", // Layout & Flexbox "flex", diff --git a/packages/react-components/src/shadcn/components/ui/sidebar.tsx b/packages/react-components/src/shadcn/components/ui/sidebar.tsx index 8b9915c3..487e5b57 100644 --- a/packages/react-components/src/shadcn/components/ui/sidebar.tsx +++ b/packages/react-components/src/shadcn/components/ui/sidebar.tsx @@ -492,7 +492,7 @@ const baseSidebarMenuButtonStyles = cn( "px-[0.44rem]", // Typography "typo-body", - "text-text-primary", + "text-fg-secondary", // Interaction "outline-hidden", "transition-all", @@ -505,9 +505,9 @@ const baseSidebarMenuButtonStyles = cn( "hover:text-text-primary", // Active "active:bg-bg-quaternary", - "data-[active=true]:bg-bg-brand-secondary", + "data-[active=true]:bg-bg-brand-primary", "data-[active=true]:text-text-primary", - "data-[active=true]:[&>svg]:text-text-brand-primary", + "data-[active=true]:[&>svg]:text-fg-brand-primary", // Open state "data-[state=open]:hover:bg-bg-secondary", "data-[state=open]:hover:text-text-primary", diff --git a/packages/react-components/src/shadcn/components/ui/table.tsx b/packages/react-components/src/shadcn/components/ui/table.tsx index d1a686d2..8d1ff534 100644 --- a/packages/react-components/src/shadcn/components/ui/table.tsx +++ b/packages/react-components/src/shadcn/components/ui/table.tsx @@ -58,7 +58,7 @@ const tableHeadStyles = cn( "py-2", "text-left", "align-middle", - "typo-label-xs", + "typo-body-xs", "whitespace-nowrap", "transition-colors", "duration-150", @@ -81,7 +81,7 @@ const tableCellStyles = cn( "align-middle", "whitespace-nowrap", "text-sm", - "text-text-primary", + "text-grey-700", "[&:has([role=checkbox])]:pr-0", ); diff --git a/packages/react-components/src/tokens.css b/packages/react-components/src/tokens.css index 93c199e9..378c3fe0 100644 --- a/packages/react-components/src/tokens.css +++ b/packages/react-components/src/tokens.css @@ -40,8 +40,8 @@ --color-red-50: #fef9f9; --color-red-100: #fff6f5; --color-red-200: #fdedea; - --color-red-300: #f4a499; - --color-red-400: #ea4a35; + --color-red-300: #f18b7e; + --color-red-400: #db2e17; --color-red-500: #d7270f; --color-red-600: #d7270f; --color-red-700: #c31b03; @@ -54,7 +54,7 @@ --color-blue-200: #e9f2fc; --color-blue-300: #d0e2f8; --color-blue-400: #a7c9f3; - --color-blue-500: #2378e1; + --color-blue-500: #1e71d9; --color-blue-600: #045ac3; --color-blue-700: #014391; --color-blue-800: #053775; @@ -66,25 +66,38 @@ --color-green-200: #f1f8e6; --color-green-300: #e3efcb; --color-green-400: #c9e19b; - --color-green-500: #78b506; + --color-green-500: #547f04; --color-green-600: #558300; --color-green-700: #334e02; --color-green-800: #1d2b03; --color-green-900: #090d04; --color-green-950: #090d04; + --color-green-550: #6aa300; --color-yellow-50: #fffdf2; --color-yellow-100: #fffbe5; --color-yellow-200: #fff9d9; --color-yellow-300: #fff4bf; --color-yellow-400: #ffea80; - --color-yellow-500: #ffd400; - --color-yellow-600: #dfa400; + --color-yellow-500: #f4cb00; + --color-yellow-600: #946c01; --color-yellow-700: #855600; --color-yellow-800: #562a00; --color-yellow-900: #341900; --color-yellow-950: #200900; + --color-violet-50: #fdf9fd; + --color-violet-100: #fcf2fb; + --color-violet-200: #faeaf8; + --color-violet-300: #f5d4f0; + --color-violet-400: #eba9e2; + --color-violet-500: #cc29b6; + --color-violet-600: #931e83; + --color-violet-700: #58124e; + --color-violet-800: #310a2c; + --color-violet-900: #10030f; + --color-violet-950: #080207; + --font-size-xxs: 10px; --font-size-xs: 12px; --font-size-sm: 14px; From 9785ca4a893b5908fa77edabc2b4b2b54e93dead Mon Sep 17 00:00:00 2001 From: Panthevm Date: Mon, 6 Apr 2026 15:42:03 +0300 Subject: [PATCH 26/55] Update SQL code editor autocomplete keywords --- .../src/components/code-editor/index.tsx | 315 ++++++++++++++++++ 1 file changed, 315 insertions(+) diff --git a/packages/react-components/src/components/code-editor/index.tsx b/packages/react-components/src/components/code-editor/index.tsx index 4767e3fd..ca567183 100644 --- a/packages/react-components/src/components/code-editor/index.tsx +++ b/packages/react-components/src/components/code-editor/index.tsx @@ -777,6 +777,8 @@ const SQL_KEYWORDS = [ "in", "between", "like", + "ilike", + "similar", "insert", "update", "delete", @@ -785,12 +787,26 @@ const SQL_KEYWORDS = [ "alter", "table", "index", + "view", + "materialized", + "schema", + "sequence", + "type", + "extension", + "function", + "procedure", + "trigger", "join", "inner", "left", "right", "outer", + "full", + "cross", + "lateral", + "natural", "on", + "using", "as", "order", "by", @@ -798,12 +814,19 @@ const SQL_KEYWORDS = [ "having", "limit", "offset", + "fetch", + "first", + "next", + "rows", + "only", "union", "intersect", "except", "distinct", "all", "exists", + "any", + "some", "case", "when", "then", @@ -813,41 +836,333 @@ const SQL_KEYWORDS = [ "true", "false", "is", + "isnull", + "notnull", "asc", "desc", + "nulls", + "with", + "recursive", + "returning", + "into", + "values", + "set", + "default", + "begin", + "commit", + "rollback", + "savepoint", + "release", + "transaction", + "explain", + "analyze", + "verbose", + "costs", + "buffers", + "format", + "grant", + "revoke", + "truncate", + "cascade", + "restrict", + "vacuum", + "reindex", + "cluster", + "copy", + "do", + "perform", + "raise", + "notice", + "exception", + "if", + "elsif", + "loop", + "while", + "for", + "foreach", + "return", + "returns", + "language", + "plpgsql", + "declare", + "primary", + "key", + "foreign", + "references", + "unique", + "check", + "constraint", + "not", + "null", + "add", + "column", + "rename", + "to", + "owner", + "tablespace", + "temporary", + "temp", + "unlogged", + "if", + "replace", + "or", + "conflict", + "nothing", + "window", + "partition", + "over", + "range", + "unbounded", + "preceding", + "following", + "current", + "row", + "groups", + "exclude", + "ties", + "filter", + "within", ]; const SQL_BUILTIN = [ + // types "varchar", "char", "text", "integer", "int", + "smallint", "bigint", "decimal", "numeric", "float", "real", + "double", + "precision", "boolean", + "bool", "date", "time", "timestamp", + "timestamptz", + "interval", "uuid", + "json", + "jsonb", + "bytea", + "serial", + "bigserial", + "smallserial", + "money", + "inet", + "cidr", + "macaddr", + "point", + "line", + "lseg", + "box", + "path", + "polygon", + "circle", + "tsquery", + "tsvector", + "xml", + "oid", + "regclass", + "regtype", + // aggregate functions "count", "sum", "avg", "min", "max", + "array_agg", + "string_agg", + "json_agg", + "jsonb_agg", + "json_object_agg", + "jsonb_object_agg", + "bool_and", + "bool_or", + "every", + "bit_and", + "bit_or", + // window functions + "row_number", + "rank", + "dense_rank", + "percent_rank", + "cume_dist", + "ntile", + "lag", + "lead", + "first_value", + "last_value", + "nth_value", + // string functions "coalesce", + "nullif", + "greatest", + "least", "concat", + "concat_ws", "substring", "upper", "lower", "trim", + "ltrim", + "rtrim", "length", + "char_length", + "octet_length", + "position", + "replace", + "translate", + "left", + "right", + "repeat", + "reverse", + "split_part", + "regexp_match", + "regexp_matches", + "regexp_replace", + "regexp_split_to_array", + "regexp_split_to_table", + "format", + "encode", + "decode", + "md5", + "starts_with", + // date/time functions "now", "current_date", "current_time", + "current_timestamp", + "localtime", + "localtimestamp", + "clock_timestamp", + "statement_timestamp", + "transaction_timestamp", + "timeofday", + "age", + "date_part", + "date_trunc", + "extract", + "make_date", + "make_time", + "make_timestamp", + "make_timestamptz", + "make_interval", + "to_char", + "to_date", + "to_timestamp", + "to_number", + // json/jsonb functions + "json_build_object", + "jsonb_build_object", + "json_build_array", + "jsonb_build_array", + "json_extract_path", + "jsonb_extract_path", + "json_extract_path_text", + "jsonb_extract_path_text", + "jsonb_set", + "jsonb_insert", + "jsonb_strip_nulls", + "jsonb_pretty", + "jsonb_typeof", + "jsonb_each", + "jsonb_each_text", + "jsonb_array_elements", + "jsonb_array_elements_text", + "jsonb_array_length", + "jsonb_object_keys", + "jsonb_to_record", + "jsonb_to_recordset", + "jsonb_populate_record", + "jsonb_populate_recordset", + "jsonb_path_query", + "jsonb_path_query_array", + "jsonb_path_query_first", + "jsonb_path_exists", + "to_json", + "to_jsonb", + "row_to_json", + // array functions + "array_length", + "array_dims", + "array_lower", + "array_upper", + "array_append", + "array_prepend", + "array_cat", + "array_remove", + "array_replace", + "array_position", + "array_positions", + "array_to_string", + "string_to_array", + "unnest", + "cardinality", + // set-returning functions + "generate_series", + "generate_subscripts", + // math functions + "abs", + "ceil", + "ceiling", + "floor", + "round", + "trunc", + "sign", + "sqrt", + "cbrt", + "power", + "exp", + "ln", + "log", + "mod", + "random", + "setseed", + "pi", + "degrees", + "radians", + // system functions + "pg_typeof", + "pg_size_pretty", + "pg_table_size", + "pg_indexes_size", + "pg_total_relation_size", + "pg_relation_size", + "pg_database_size", + "pg_cancel_backend", + "pg_terminate_backend", + "pg_stat_activity", + "pg_stat_statements", + "pg_advisory_lock", + "pg_advisory_unlock", + "pg_try_advisory_lock", + // cast/conversion + "cast", + "pg_get_functiondef", + "pg_get_viewdef", + "pg_get_indexdef", + // misc + "exists", + "in", + "between", + "like", + "ilike", + "similar", + "any", + "some", + "row", + "array", + "nextval", + "currval", + "setval", + "lastval", + "txid_current", ]; const customSQLDialect = SQLDialect.define({ From 58f430243c470ec91ff65a4c791f29d073c5c351 Mon Sep 17 00:00:00 2001 From: Panthevm Date: Tue, 7 Apr 2026 19:23:32 +0300 Subject: [PATCH 27/55] make OperationOutcomeView severity header sticky Co-authored-by: Andrey Listopadov --- .../react-components/src/components/operation-outcome-view.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-components/src/components/operation-outcome-view.tsx b/packages/react-components/src/components/operation-outcome-view.tsx index 4eaae6fc..f8480d7b 100644 --- a/packages/react-components/src/components/operation-outcome-view.tsx +++ b/packages/react-components/src/components/operation-outcome-view.tsx @@ -191,6 +191,7 @@ export function OperationOutcomeView({ "flex items-center gap-2 px-4 h-8", "[&>svg]:size-4 [&>svg]:shrink-0", "typo-body", + "sticky top-0 z-10", config.header, )} > From ba370dd8fc87fce1d6052a1b1d6cd10347c122d4 Mon Sep 17 00:00:00 2001 From: Andrey Listopadov Date: Wed, 8 Apr 2026 17:18:15 +0300 Subject: [PATCH 28/55] update lockfile --- pnpm-lock.yaml | 141 ++++++++++++++++++++++--------------------------- 1 file changed, 63 insertions(+), 78 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 606250ca..77965d24 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,7 +6,10 @@ settings: overrides: fast-xml-parser: '>=5.5.6' + yaml: '>=2.8.3' picomatch: '>=4.0.4' + file-type: '>=21.3.2' + brace-expansion: '>=2.0.3' defu: '>=6.1.5' vite: '>=7.3.2' @@ -33,8 +36,8 @@ importers: specifier: ^3.8.5 version: 3.8.5 yaml: - specifier: ^2.8.2 - version: 2.8.2 + specifier: '>=2.8.3' + version: 2.8.3 devDependencies: '@atomic-ehr/codegen': specifier: ^0.0.8 @@ -53,7 +56,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + version: 4.0.18(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) packages/aidbox-fhirpath-lsp: dependencies: @@ -123,10 +126,10 @@ importers: version: 5.9.3 vite: specifier: '>=7.3.2' - version: 8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + version: 8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: ^4.0.18 - version: 4.0.18(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + version: 4.0.18(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) vscode-languageserver-types: specifier: ^3.17.5 version: 3.17.5 @@ -336,10 +339,10 @@ importers: version: 1.8.0 '@storybook/addon-docs': specifier: ^10.2.17 - version: 10.2.17(@types/react@19.2.14)(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 10.2.17(@types/react@19.2.14)(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) '@storybook/react-vite': specifier: ^10.2.17 - version: 10.2.17(esbuild@0.27.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.59.0)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(vite@8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 10.2.17(esbuild@0.27.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.59.0)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(vite@8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) '@swc/cli': specifier: ^0.8.0 version: 0.8.0(@swc/core@1.15.18)(chokidar@5.0.0) @@ -351,7 +354,7 @@ importers: version: 4.2.1 '@tailwindcss/vite': specifier: ^4.2.1 - version: 4.2.1(vite@8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.2.1(vite@8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) '@types/node': specifier: ^25.4.0 version: 25.4.0 @@ -363,7 +366,7 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react-swc': specifier: ^4.2.3 - version: 4.2.3(vite@8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.2.3(vite@8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) chokidar: specifier: ^5.0.0 version: 5.0.0 @@ -387,10 +390,10 @@ importers: version: 5.9.3 vite: specifier: '>=7.3.2' - version: 8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + version: 8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: ^4.0.18 - version: 4.0.18(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + version: 4.0.18(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) packages: @@ -2527,8 +2530,8 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' - '@tokenizer/inflate@0.2.7': - resolution: {integrity: sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==} + '@tokenizer/inflate@0.4.1': + resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} engines: {node: '>=18'} '@tokenizer/token@0.3.0': @@ -2783,9 +2786,6 @@ packages: react-native-b4a: optional: true - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - balanced-match@4.0.4: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} @@ -2851,9 +2851,6 @@ packages: birpc@4.0.0: resolution: {integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==} - brace-expansion@2.0.2: - resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} - brace-expansion@5.0.4: resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} engines: {node: 18 || 20 || >=22} @@ -3200,12 +3197,9 @@ packages: picomatch: optional: true - fflate@0.8.2: - resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} - - file-type@20.5.0: - resolution: {integrity: sha512-BfHZtG/l9iMm4Ecianu7P8HRD2tBHLtjXinm4X62XBOYzi7CYA7jyqfJzOvXHqzVrVPYqBo2/GvbARMaaJkKVg==} - engines: {node: '>=18'} + file-type@22.0.0: + resolution: {integrity: sha512-cmBmnYo8Zymabm2+qAP7jTFbKF10bQpYmxoGfuZbRFRcq00BRddJdGNH/P7GA1EMpJy5yQbqa9B7yROb3z8Ziw==} + engines: {node: '>=22'} filename-reserved-regex@3.0.0: resolution: {integrity: sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==} @@ -4073,8 +4067,8 @@ packages: strnum@2.2.0: resolution: {integrity: sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==} - strtok3@10.3.4: - resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==} + strtok3@10.3.5: + resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==} engines: {node: '>=18'} style-mod@4.1.3: @@ -4294,7 +4288,7 @@ packages: sugarss: ^5.0.0 terser: ^5.16.0 tsx: ^4.8.1 - yaml: ^2.4.2 + yaml: '>=2.8.3' peerDependenciesMeta: '@types/node': optional: true @@ -4415,8 +4409,8 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yaml@2.8.2: - resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} engines: {node: '>= 14.6'} hasBin: true @@ -4446,7 +4440,7 @@ snapshots: mustache: 4.2.0 picocolors: 1.1.1 tinyglobby: 0.2.15 - yaml: 2.8.2 + yaml: 2.8.3 yargs: 18.0.0 transitivePeerDependencies: - typescript @@ -4890,11 +4884,11 @@ snapshots: '@standard-schema/utils': 0.3.0 react-hook-form: 7.71.2(react@19.2.4) - '@joshwooding/vite-plugin-react-docgen-typescript@0.6.4(typescript@5.9.3)(vite@8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))': + '@joshwooding/vite-plugin-react-docgen-typescript@0.6.4(typescript@5.9.3)(vite@8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: glob: 13.0.6 react-docgen-typescript: 2.4.0(typescript@5.9.3) - vite: 8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) optionalDependencies: typescript: 5.9.3 @@ -6168,10 +6162,10 @@ snapshots: '@standard-schema/utils@0.3.0': {} - '@storybook/addon-docs@10.2.17(@types/react@19.2.14)(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))': + '@storybook/addon-docs@10.2.17(@types/react@19.2.14)(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@mdx-js/react': 3.1.1(@types/react@19.2.14)(react@19.2.4) - '@storybook/csf-plugin': 10.2.17(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) + '@storybook/csf-plugin': 10.2.17(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) '@storybook/icons': 2.0.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@storybook/react-dom-shim': 10.2.17(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) react: 19.2.4 @@ -6185,25 +6179,25 @@ snapshots: - vite - webpack - '@storybook/builder-vite@10.2.17(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))': + '@storybook/builder-vite@10.2.17(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - '@storybook/csf-plugin': 10.2.17(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) + '@storybook/csf-plugin': 10.2.17(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) storybook: 10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) ts-dedent: 2.2.0 - vite: 8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - esbuild - rollup - webpack - '@storybook/csf-plugin@10.2.17(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))': + '@storybook/csf-plugin@10.2.17(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: storybook: 10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) unplugin: 2.3.11 optionalDependencies: esbuild: 0.27.3 rollup: 4.59.0 - vite: 8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) '@storybook/global@5.0.0': {} @@ -6218,11 +6212,11 @@ snapshots: react-dom: 19.2.4(react@19.2.4) storybook: 10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@storybook/react-vite@10.2.17(esbuild@0.27.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.59.0)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(vite@8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))': + '@storybook/react-vite@10.2.17(esbuild@0.27.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.59.0)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(vite@8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.4(typescript@5.9.3)(vite@8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.4(typescript@5.9.3)(vite@8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) '@rollup/pluginutils': 5.3.0(rollup@4.59.0) - '@storybook/builder-vite': 10.2.17(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) + '@storybook/builder-vite': 10.2.17(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) '@storybook/react': 10.2.17(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) empathic: 2.0.0 magic-string: 0.30.21 @@ -6232,7 +6226,7 @@ snapshots: resolve: 1.22.11 storybook: 10.2.17(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) tsconfig-paths: 4.2.0 - vite: 8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - esbuild - rollup @@ -6402,12 +6396,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.2.1 '@tailwindcss/oxide-win32-x64-msvc': 4.2.1 - '@tailwindcss/vite@4.2.1(vite@8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))': + '@tailwindcss/vite@4.2.1(vite@8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@tailwindcss/node': 4.2.1 '@tailwindcss/oxide': 4.2.1 tailwindcss: 4.2.1 - vite: 8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) '@tanstack/react-table@8.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: @@ -6441,10 +6435,9 @@ snapshots: dependencies: '@testing-library/dom': 10.4.1 - '@tokenizer/inflate@0.2.7': + '@tokenizer/inflate@0.4.1': dependencies: debug: 4.4.3 - fflate: 0.8.2 token-types: 6.1.2 transitivePeerDependencies: - supports-color @@ -6544,11 +6537,11 @@ snapshots: '@types/use-sync-external-store@0.0.6': {} - '@vitejs/plugin-react-swc@4.2.3(vite@8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))': + '@vitejs/plugin-react-swc@4.2.3(vite@8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.2 '@swc/core': 1.15.18 - vite: 8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - '@swc/helpers' @@ -6569,13 +6562,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.0.18(vite@8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/mocker@4.0.18(vite@8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) '@vitest/pretty-format@3.2.4': dependencies: @@ -6615,7 +6608,7 @@ snapshots: '@xhmikosr/archive-type@7.1.0': dependencies: - file-type: 20.5.0 + file-type: 22.0.0 transitivePeerDependencies: - supports-color @@ -6638,7 +6631,7 @@ snapshots: '@xhmikosr/decompress-tar@8.1.0': dependencies: - file-type: 20.5.0 + file-type: 22.0.0 is-stream: 2.0.1 tar-stream: 3.1.8 transitivePeerDependencies: @@ -6650,7 +6643,7 @@ snapshots: '@xhmikosr/decompress-tarbz2@8.1.0': dependencies: '@xhmikosr/decompress-tar': 8.1.0 - file-type: 20.5.0 + file-type: 22.0.0 is-stream: 2.0.1 seek-bzip: 2.0.0 unbzip2-stream: 1.4.3 @@ -6663,7 +6656,7 @@ snapshots: '@xhmikosr/decompress-targz@8.1.0': dependencies: '@xhmikosr/decompress-tar': 8.1.0 - file-type: 20.5.0 + file-type: 22.0.0 is-stream: 2.0.1 transitivePeerDependencies: - bare-abort-controller @@ -6673,7 +6666,7 @@ snapshots: '@xhmikosr/decompress-unzip@7.1.0': dependencies: - file-type: 20.5.0 + file-type: 22.0.0 get-stream: 6.0.1 yauzl: 3.2.1 transitivePeerDependencies: @@ -6700,7 +6693,7 @@ snapshots: content-disposition: 0.5.4 defaults: 2.0.2 ext-name: 5.0.0 - file-type: 20.5.0 + file-type: 22.0.0 filenamify: 6.0.0 get-stream: 6.0.1 got: 13.0.0 @@ -6761,8 +6754,6 @@ snapshots: b4a@1.8.0: {} - balanced-match@1.0.2: {} - balanced-match@4.0.4: {} bare-events@2.8.2: {} @@ -6817,10 +6808,6 @@ snapshots: birpc@4.0.0: {} - brace-expansion@2.0.2: - dependencies: - balanced-match: 1.0.2 - brace-expansion@5.0.4: dependencies: balanced-match: 4.0.4 @@ -7168,12 +7155,10 @@ snapshots: optionalDependencies: picomatch: 4.0.4 - fflate@0.8.2: {} - - file-type@20.5.0: + file-type@22.0.0: dependencies: - '@tokenizer/inflate': 0.2.7 - strtok3: 10.3.4 + '@tokenizer/inflate': 0.4.1 + strtok3: 10.3.5 token-types: 6.1.2 uint8array-extras: 1.5.0 transitivePeerDependencies: @@ -7497,7 +7482,7 @@ snapshots: minimatch@9.0.9: dependencies: - brace-expansion: 2.0.2 + brace-expansion: 5.0.4 minimist@1.2.8: {} @@ -8016,7 +8001,7 @@ snapshots: strnum@2.2.0: {} - strtok3@10.3.4: + strtok3@10.3.5: dependencies: '@tokenizer/token': 0.3.0 @@ -8148,7 +8133,7 @@ snapshots: markdown-it: 14.1.1 minimatch: 9.0.9 typescript: 5.9.3 - yaml: 2.8.2 + yaml: 2.8.3 typescript@5.9.3: {} @@ -8230,7 +8215,7 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite@8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2): + vite@8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -8243,12 +8228,12 @@ snapshots: fsevents: 2.3.3 jiti: 2.6.1 tsx: 4.21.0 - yaml: 2.8.2 + yaml: 2.8.3 - vitest@4.0.18(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2): + vitest@4.0.18(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/mocker': 4.0.18(vite@8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -8265,7 +8250,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.1.0 - vite: 8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 8.0.7(@types/node@25.4.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.4.0 @@ -8327,7 +8312,7 @@ snapshots: yallist@3.1.1: {} - yaml@2.8.2: {} + yaml@2.8.3: {} yargs-parser@22.0.0: {} From e17a1d62660ebbf0a5090f4864273d74c735c51f Mon Sep 17 00:00:00 2001 From: Andrey Listopadov Date: Wed, 8 Apr 2026 17:19:32 +0300 Subject: [PATCH 29/55] fix lint --- .../src/components/code-editor/sql-completion.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/react-components/src/components/code-editor/sql-completion.ts b/packages/react-components/src/components/code-editor/sql-completion.ts index 3cb591a5..b4026dc4 100644 --- a/packages/react-components/src/components/code-editor/sql-completion.ts +++ b/packages/react-components/src/components/code-editor/sql-completion.ts @@ -883,8 +883,10 @@ function sqlCompletionOverride(): Extension { }; } - const aliasColumnResult = results.find((r) => - r.options.length > 0 && r.options.every((o) => o.type === "variable"), + const aliasColumnResult = results.find( + (r) => + r.options.length > 0 && + r.options.every((o) => o.type === "variable"), ); if (aliasColumnResult) return aliasColumnResult; From 83b3e270e4c06446308dbd5c98763ecacf9f67aa Mon Sep 17 00:00:00 2001 From: Andrey Listopadov Date: Wed, 8 Apr 2026 17:22:04 +0300 Subject: [PATCH 30/55] fix doc pipeline --- package.json | 2 +- pnpm-lock.yaml | 22 ++++++++++------------ 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index c5e68ceb..fe06a76b 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "yaml": ">=2.8.3", "picomatch": ">=4.0.4", "file-type": ">=21.3.2", - "brace-expansion": ">=2.0.3", + "brace-expansion": ">=2.0.3 <3", "defu": ">=6.1.5", "vite": ">=7.3.2" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 77965d24..2cc07c3a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,7 +9,7 @@ overrides: yaml: '>=2.8.3' picomatch: '>=4.0.4' file-type: '>=21.3.2' - brace-expansion: '>=2.0.3' + brace-expansion: '>=2.0.3 <3' defu: '>=6.1.5' vite: '>=7.3.2' @@ -2786,9 +2786,8 @@ packages: react-native-b4a: optional: true - balanced-match@4.0.4: - resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} - engines: {node: 18 || 20 || >=22} + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} bare-events@2.8.2: resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==} @@ -2851,9 +2850,8 @@ packages: birpc@4.0.0: resolution: {integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==} - brace-expansion@5.0.4: - resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} - engines: {node: 18 || 20 || >=22} + brace-expansion@2.0.3: + resolution: {integrity: sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} @@ -6754,7 +6752,7 @@ snapshots: b4a@1.8.0: {} - balanced-match@4.0.4: {} + balanced-match@1.0.2: {} bare-events@2.8.2: {} @@ -6808,9 +6806,9 @@ snapshots: birpc@4.0.0: {} - brace-expansion@5.0.4: + brace-expansion@2.0.3: dependencies: - balanced-match: 4.0.4 + balanced-match: 1.0.2 braces@3.0.3: dependencies: @@ -7478,11 +7476,11 @@ snapshots: minimatch@10.2.4: dependencies: - brace-expansion: 5.0.4 + brace-expansion: 2.0.3 minimatch@9.0.9: dependencies: - brace-expansion: 5.0.4 + brace-expansion: 2.0.3 minimist@1.2.8: {} From 8b7e333a099a3172773cd1092e0340904aa3d9c3 Mon Sep 17 00:00:00 2001 From: Andrey Listopadov Date: Mon, 27 Apr 2026 16:07:25 +0300 Subject: [PATCH 31/55] bump pnpm to fix audit pipeline --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fe06a76b..51b748c7 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "workspaces": [ "packages/*" ], - "packageManager": "pnpm@10.21.0", + "packageManager": "pnpm@10.33.2", "pnpm": { "overrides": { "fast-xml-parser": ">=5.5.6", From 3271d88ce82b8085ef80262ec87f5b445ba214e6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 04:38:50 +0000 Subject: [PATCH 32/55] chore(deps): bump pnpm/action-setup from 4 to 6 Bumps [pnpm/action-setup](https://github.com/pnpm/action-setup) from 4 to 6. - [Release notes](https://github.com/pnpm/action-setup/releases) - [Commits](https://github.com/pnpm/action-setup/compare/v4...v6) --- updated-dependencies: - dependency-name: pnpm/action-setup dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/aidbox-client.yaml | 2 +- .github/workflows/common.yaml | 6 +++--- .github/workflows/pages.yaml | 2 +- .github/workflows/release.yaml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/aidbox-client.yaml b/.github/workflows/aidbox-client.yaml index d1d8d7db..be4e465d 100644 --- a/.github/workflows/aidbox-client.yaml +++ b/.github/workflows/aidbox-client.yaml @@ -16,7 +16,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 - name: Install node uses: actions/setup-node@v6 with: diff --git a/.github/workflows/common.yaml b/.github/workflows/common.yaml index fbc5d02e..709bf7c0 100644 --- a/.github/workflows/common.yaml +++ b/.github/workflows/common.yaml @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/checkout@v6 - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 - name: Install node uses: actions/setup-node@v6 with: @@ -25,7 +25,7 @@ jobs: steps: - uses: actions/checkout@v6 - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 - name: Install node uses: actions/setup-node@v6 with: @@ -40,7 +40,7 @@ jobs: steps: - uses: actions/checkout@v6 - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 - name: Install node uses: actions/setup-node@v6 with: diff --git a/.github/workflows/pages.yaml b/.github/workflows/pages.yaml index 99c20d72..d6f2cbb6 100644 --- a/.github/workflows/pages.yaml +++ b/.github/workflows/pages.yaml @@ -9,7 +9,7 @@ jobs: - uses: actions/checkout@v6 - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 - name: Install node uses: actions/setup-node@v6 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 49be30c4..35291f91 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -23,7 +23,7 @@ jobs: steps: - uses: actions/checkout@v6 - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 - name: Install node uses: actions/setup-node@v6 with: From c9125144e8cb17977a14327fb4135157b9196230 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 04:38:45 +0000 Subject: [PATCH 33/55] chore(deps): bump actions/upload-pages-artifact from 4 to 5 Bumps [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) from 4 to 5. - [Release notes](https://github.com/actions/upload-pages-artifact/releases) - [Commits](https://github.com/actions/upload-pages-artifact/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/upload-pages-artifact dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/pages.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pages.yaml b/.github/workflows/pages.yaml index d6f2cbb6..c7746440 100644 --- a/.github/workflows/pages.yaml +++ b/.github/workflows/pages.yaml @@ -37,7 +37,7 @@ jobs: - name: Upload Pages artifact if: success() && github.ref == 'refs/heads/master' - uses: actions/upload-pages-artifact@v4 + uses: actions/upload-pages-artifact@v5 with: path: site-out From f369766852a0309958a3d083c092dac8e0327cee Mon Sep 17 00:00:00 2001 From: Andrey Listopadov Date: Thu, 30 Apr 2026 15:22:26 +0300 Subject: [PATCH 34/55] concat base path without dropping parts of it (#115) --- packages/aidbox-client/src/auth-providers.ts | 8 ++++---- packages/aidbox-client/src/client.ts | 6 +++--- packages/aidbox-client/src/utils.ts | 12 ++++++++++++ 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/packages/aidbox-client/src/auth-providers.ts b/packages/aidbox-client/src/auth-providers.ts index 01df4732..ebd41e45 100644 --- a/packages/aidbox-client/src/auth-providers.ts +++ b/packages/aidbox-client/src/auth-providers.ts @@ -6,11 +6,11 @@ export class BrowserAuthProvider implements AuthProvider { public baseUrl: string; constructor(baseUrl: string) { - this.baseUrl = baseUrl; + this.baseUrl = baseUrl.replace(/\/+$/, ""); } async #checkSession() { - const response = await fetch(new URL("/auth/userinfo", this.baseUrl), { + const response = await fetch(`${this.baseUrl}/auth/userinfo`, { method: "GET", headers: { "content-type": "application/json", @@ -36,7 +36,7 @@ export class BrowserAuthProvider implements AuthProvider { * Sends a POST request to `baseurl/auth/logout`. */ public async revokeSession() { - await fetch(new URL("/auth/logout", this.baseUrl), { + await fetch(`${this.baseUrl}/auth/logout`, { method: "POST", headers: { "content-type": "application/json", @@ -78,7 +78,7 @@ export class BasicAuthProvider implements AuthProvider { #authHeader: string; constructor(baseUrl: string, username: string, password: string) { - this.baseUrl = baseUrl; + this.baseUrl = baseUrl.replace(/\/+$/, ""); // Create Base64-encoded credentials for Basic Auth header (RFC 7617: UTF-8 encoded) const credentials = `${username}:${password}`; const utf8Bytes = new TextEncoder().encode(credentials); diff --git a/packages/aidbox-client/src/client.ts b/packages/aidbox-client/src/client.ts index a30ceca6..caa78dfc 100644 --- a/packages/aidbox-client/src/client.ts +++ b/packages/aidbox-client/src/client.ts @@ -31,7 +31,7 @@ import type { VReadOptions, } from "./types"; import { ErrorResponse, RequestError } from "./types"; -import { coerceBody } from "./utils"; +import { coerceBody, joinUrl } from "./utils"; type InternalAidboxErrorResponse = { error?: unknown; @@ -91,7 +91,7 @@ export class AidboxClient< public authProvider: AuthProvider; constructor(baseUrl: string, authProvider: AuthProvider) { - this.baseUrl = baseUrl; + this.baseUrl = baseUrl.replace(/\/+$/, ""); this.authProvider = authProvider; } @@ -113,7 +113,7 @@ export class AidboxClient< const { method, url, headers = {}, params = [], body } = requestParams; - const urlObj = new URL(url, baseUrl); + const urlObj = joinUrl(baseUrl, url); params.forEach(([key, value]) => { urlObj.searchParams.append(key, value); diff --git a/packages/aidbox-client/src/utils.ts b/packages/aidbox-client/src/utils.ts index 4aa47fdb..2c882f8b 100644 --- a/packages/aidbox-client/src/utils.ts +++ b/packages/aidbox-client/src/utils.ts @@ -2,6 +2,18 @@ import YAML from "yaml"; import type { ResponseWithMeta } from "./types"; import { ErrorResponse } from "./types"; +/** + * Join a base URL (which may include a path component) with a path. + * + * `new URL("/foo", "http://host/bar")` drops `/bar` because absolute paths + * replace the entire path of the base. This preserves the base path. + */ +export function joinUrl(baseUrl: string, path: string): URL { + const base = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl; + const suffix = path.startsWith("/") ? path : `/${path}`; + return new URL(`${base}${suffix}`); +} + /** * Validate that fetch input URL starts with baseUrl. * Throws if the URL doesn't match baseUrl. From 3e32977b879276b1efe5ea27b4a2439ff8cc9fb8 Mon Sep 17 00:00:00 2001 From: Andrey Listopadov Date: Fri, 1 May 2026 16:53:26 +0300 Subject: [PATCH 35/55] bump deps --- package.json | 4 +- pnpm-lock.yaml | 108 +++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 90 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index 51b748c7..080f6231 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,9 @@ "file-type": ">=21.3.2", "brace-expansion": ">=2.0.3 <3", "defu": ">=6.1.5", - "vite": ">=7.3.2" + "vite": ">=7.3.2", + "fast-xml-parser@<5.7.0": ">=5.7.0", + "postcss@<8.5.10": ">=8.5.10" } }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2cc07c3a..1dba51dc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,6 +12,8 @@ overrides: brace-expansion: '>=2.0.3 <3' defu: '>=6.1.5' vite: '>=7.3.2' + fast-xml-parser@<5.7.0: '>=5.7.0' + postcss@<8.5.10: '>=8.5.10' importers: @@ -553,24 +555,28 @@ packages: engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [musl] '@biomejs/cli-linux-arm64@2.4.6': resolution: {integrity: sha512-kMLaI7OF5GN1Q8Doymjro1P8rVEoy7BKQALNz6fiR8IC1WKduoNyteBtJlHT7ASIL0Cx2jR6VUOBIbcB1B8pew==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [glibc] '@biomejs/cli-linux-x64-musl@2.4.6': resolution: {integrity: sha512-C9s98IPDu7DYarjlZNuzJKTjVHN03RUnmHV5htvqsx6vEUXCDSJ59DNwjKVD5XYoSS4N+BYhq3RTBAL8X6svEg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [musl] '@biomejs/cli-linux-x64@2.4.6': resolution: {integrity: sha512-oHXmUFEoH8Lql1xfc3QkFLiC1hGR7qedv5eKNlC185or+o4/4HiaU7vYODAH3peRCfsuLr1g6v2fK9dFFOYdyw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [glibc] '@biomejs/cli-win32-arm64@2.4.6': resolution: {integrity: sha512-xzThn87Pf3YrOGTEODFGONmqXpTwUNxovQb72iaUOdcw8sBSY3+3WD8Hm9IhMYLnPi0n32s3L3NWU6+eSjfqFg==} @@ -924,42 +930,49 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@napi-rs/nice-linux-arm64-musl@1.1.1': resolution: {integrity: sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@napi-rs/nice-linux-ppc64-gnu@1.1.1': resolution: {integrity: sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==} engines: {node: '>= 10'} cpu: [ppc64] os: [linux] + libc: [glibc] '@napi-rs/nice-linux-riscv64-gnu@1.1.1': resolution: {integrity: sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] + libc: [glibc] '@napi-rs/nice-linux-s390x-gnu@1.1.1': resolution: {integrity: sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==} engines: {node: '>= 10'} cpu: [s390x] os: [linux] + libc: [glibc] '@napi-rs/nice-linux-x64-gnu@1.1.1': resolution: {integrity: sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@napi-rs/nice-linux-x64-musl@1.1.1': resolution: {integrity: sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@napi-rs/nice-openharmony-arm64@1.1.1': resolution: {integrity: sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==} @@ -998,6 +1011,9 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 + '@nodable/entities@2.1.0': + resolution: {integrity: sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1045,36 +1061,42 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.6': resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.6': resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.6': resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.6': resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.6': resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [musl] '@parcel/watcher-win32-arm64@2.5.6': resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} @@ -1972,72 +1994,84 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.9': resolution: {integrity: sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.0.0-rc.13': resolution: {integrity: sha512-bmRg3O6Z0gq9yodKKWCIpnlH051sEfdVwt+6m5UDffAQMUUqU0xjnQqqAUm+Gu7ofAAly9DqiQDtKu2nPDEABA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@rolldown/binding-linux-arm64-musl@1.0.0-rc.9': resolution: {integrity: sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.13': resolution: {integrity: sha512-8Wtnbw4k7pMYN9B/mOEAsQ8HOiq7AZ31Ig4M9BKn2So4xRaFEhtCSa4ZJaOutOWq50zpgR4N5+L/opnlaCx8wQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9': resolution: {integrity: sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.13': resolution: {integrity: sha512-D/0Nlo8mQuxSMohNJUF2lDXWRsFDsHldfRRgD9bRgktj+EndGPj4DOV37LqDKPYS+osdyhZEH7fTakTAEcW7qg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9': resolution: {integrity: sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.0.0-rc.13': resolution: {integrity: sha512-eRrPvat2YaVQcwwKi/JzOP6MKf1WRnOCr+VaI3cTWz3ZoLcP/654z90lVCJ4dAuMEpPdke0n+qyAqXDZdIC4rA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.0.0-rc.9': resolution: {integrity: sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-musl@1.0.0-rc.13': resolution: {integrity: sha512-PsdONiFRp8hR8KgVjTWjZ9s7uA3uueWL0t74/cKHfM4dR5zXYv4AjB8BvA+QDToqxAFg4ZkcVEqeu5F7inoz5w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@rolldown/binding-linux-x64-musl@1.0.0-rc.9': resolution: {integrity: sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@rolldown/binding-openharmony-arm64@1.0.0-rc.13': resolution: {integrity: sha512-hCNXgC5dI3TVOLrPT++PKFNZ+1EtS0mLQwfXXXSUD/+rGlB65gZDwN/IDuxLpQP4x8RYYHqGomlUXzpO8aVI2w==} @@ -2137,66 +2171,79 @@ packages: resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.59.0': resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.59.0': resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.59.0': resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.59.0': resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.59.0': resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.59.0': resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.59.0': resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.59.0': resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.59.0': resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.59.0': resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.59.0': resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.59.0': resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.59.0': resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} @@ -2351,24 +2398,28 @@ packages: engines: {node: '>=10'} cpu: [arm64] os: [linux] + libc: [glibc] '@swc/core-linux-arm64-musl@1.15.18': resolution: {integrity: sha512-0a+Lix+FSSHBSBOA0XznCcHo5/1nA6oLLjcnocvzXeqtdjnPb+SvchItHI+lfeiuj1sClYPDvPMLSLyXFaiIKw==} engines: {node: '>=10'} cpu: [arm64] os: [linux] + libc: [musl] '@swc/core-linux-x64-gnu@1.15.18': resolution: {integrity: sha512-wG9J8vReUlpaHz4KOD/5UE1AUgirimU4UFT9oZmupUDEofxJKYb1mTA/DrMj0s78bkBiNI+7Fo2EgPuvOJfuAA==} engines: {node: '>=10'} cpu: [x64] os: [linux] + libc: [glibc] '@swc/core-linux-x64-musl@1.15.18': resolution: {integrity: sha512-4nwbVvCphKzicwNWRmvD5iBaZj8JYsRGa4xOxJmOyHlMDpsvvJ2OR2cODlvWyGFH6BYL1MfIAK3qph3hp0Az6g==} engines: {node: '>=10'} cpu: [x64] os: [linux] + libc: [musl] '@swc/core-win32-arm64-msvc@1.15.18': resolution: {integrity: sha512-zk0RYO+LjiBCat2RTMHzAWaMky0cra9loH4oRrLKLLNuL+jarxKLFDA8xTZWEkCPLjUTwlRN7d28eDLLMgtUcQ==} @@ -2453,24 +2504,28 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.2.1': resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.2.1': resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.2.1': resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.2.1': resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==} @@ -3176,11 +3231,11 @@ packages: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} - fast-xml-builder@1.1.4: - resolution: {integrity: sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==} + fast-xml-builder@1.1.5: + resolution: {integrity: sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==} - fast-xml-parser@5.5.8: - resolution: {integrity: sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==} + fast-xml-parser@5.7.2: + resolution: {integrity: sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==} hasBin: true fastq@1.20.1: @@ -3458,48 +3513,56 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-gnu@1.32.0: resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.31.1: resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.31.1: resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.31.1: resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.31.1: resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} @@ -3685,8 +3748,8 @@ packages: resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==} engines: {node: '>=12.20'} - path-expression-matcher@1.2.0: - resolution: {integrity: sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==} + path-expression-matcher@1.5.0: + resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==} engines: {node: '>=14.0.0'} path-key@3.1.1: @@ -3728,8 +3791,8 @@ packages: resolution: {integrity: sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==} engines: {node: '>=12'} - postcss@8.5.8: - resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + postcss@8.5.13: + resolution: {integrity: sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==} engines: {node: ^10 || ^12 || >=14} pretty-format@27.5.1: @@ -4062,8 +4125,8 @@ packages: resolution: {integrity: sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==} engines: {node: '>=12'} - strnum@2.2.0: - resolution: {integrity: sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==} + strnum@2.2.3: + resolution: {integrity: sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==} strtok3@10.3.5: resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==} @@ -4473,7 +4536,7 @@ snapshots: '@atomic-ehr/fhir-canonical-manager': 0.0.11(typescript@5.9.3) '@atomic-ehr/fhirschema': 0.0.2(typescript@5.9.3) '@atomic-ehr/ucum': 0.2.5(typescript@5.9.3) - fast-xml-parser: 5.5.8 + fast-xml-parser: 5.7.2 typescript: 5.9.3 '@atomic-ehr/fhirschema@0.0.2(typescript@5.9.3)': @@ -5030,6 +5093,8 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@nodable/entities@2.1.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -7135,15 +7200,16 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 - fast-xml-builder@1.1.4: + fast-xml-builder@1.1.5: dependencies: - path-expression-matcher: 1.2.0 + path-expression-matcher: 1.5.0 - fast-xml-parser@5.5.8: + fast-xml-parser@5.7.2: dependencies: - fast-xml-builder: 1.1.4 - path-expression-matcher: 1.2.0 - strnum: 2.2.0 + '@nodable/entities': 2.1.0 + fast-xml-builder: 1.1.5 + path-expression-matcher: 1.5.0 + strnum: 2.2.3 fastq@1.20.1: dependencies: @@ -7530,7 +7596,7 @@ snapshots: p-cancelable@3.0.0: {} - path-expression-matcher@1.2.0: {} + path-expression-matcher@1.5.0: {} path-key@3.1.1: {} @@ -7561,7 +7627,7 @@ snapshots: dependencies: queue-lit: 1.5.2 - postcss@8.5.8: + postcss@8.5.13: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 @@ -7997,7 +8063,7 @@ snapshots: strip-indent@4.1.1: {} - strnum@2.2.0: {} + strnum@2.2.3: {} strtok3@10.3.5: dependencies: @@ -8217,7 +8283,7 @@ snapshots: dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 - postcss: 8.5.8 + postcss: 8.5.13 rolldown: 1.0.0-rc.13 tinyglobby: 0.2.15 optionalDependencies: From 1d383c83cb7b8ad891b375fceea3fe22523a11fe Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Mon, 11 May 2026 17:15:59 +0200 Subject: [PATCH 36/55] update @atomic-ehr/codegen to 0.0.14 and regenerate FHIR types --- packages/aidbox-client/package.json | 2 +- .../aidbox-client/scripts/generate-types.ts | 49 +- .../aidbox-client/src/fhir-types/README.md | 2687 +++++++++++++++++ .../fhir-types/hl7-fhir-r4-core/Address.ts | 32 - .../hl7-fhir-r4-core/BackboneElement.ts | 2 +- .../src/fhir-types/hl7-fhir-r4-core/Bundle.ts | 17 +- .../hl7-fhir-r4-core/CodeableConcept.ts | 6 +- .../src/fhir-types/hl7-fhir-r4-core/Coding.ts | 6 +- .../hl7-fhir-r4-core/ContactPoint.ts | 22 - .../hl7-fhir-r4-core/DomainResource.ts | 6 +- .../fhir-types/hl7-fhir-r4-core/Element.ts | 2 +- .../fhir-types/hl7-fhir-r4-core/HumanName.ts | 8 +- .../fhir-types/hl7-fhir-r4-core/Identifier.ts | 4 +- .../src/fhir-types/hl7-fhir-r4-core/Meta.ts | 4 +- .../fhir-types/hl7-fhir-r4-core/Narrative.ts | 2 +- .../hl7-fhir-r4-core/OperationOutcome.ts | 2 +- .../fhir-types/hl7-fhir-r4-core/Patient.ts | 33 +- .../src/fhir-types/hl7-fhir-r4-core/Period.ts | 2 +- .../fhir-types/hl7-fhir-r4-core/Reference.ts | 4 +- .../fhir-types/hl7-fhir-r4-core/Resource.ts | 5 +- .../fhir-types/hl7-fhir-r4-core/Signature.ts | 4 +- .../src/fhir-types/hl7-fhir-r4-core/index.ts | 4 +- pnpm-lock.yaml | 41 +- 23 files changed, 2775 insertions(+), 169 deletions(-) create mode 100644 packages/aidbox-client/src/fhir-types/README.md delete mode 100644 packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Address.ts delete mode 100644 packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/ContactPoint.ts diff --git a/packages/aidbox-client/package.json b/packages/aidbox-client/package.json index 3ff16048..cc10fea8 100644 --- a/packages/aidbox-client/package.json +++ b/packages/aidbox-client/package.json @@ -45,7 +45,7 @@ "directory": "packages/aidbox-client" }, "devDependencies": { - "@atomic-ehr/codegen": "^0.0.8", + "@atomic-ehr/codegen": "latest", "@types/node": "^25.4.0", "tsx": "^4.21.0", "typedoc": "^0.28.17", diff --git a/packages/aidbox-client/scripts/generate-types.ts b/packages/aidbox-client/scripts/generate-types.ts index b13b58a3..3672ff06 100644 --- a/packages/aidbox-client/scripts/generate-types.ts +++ b/packages/aidbox-client/scripts/generate-types.ts @@ -3,34 +3,35 @@ import { APIBuilder } from "@atomic-ehr/codegen"; console.log("📦 Generating FHIR R4 Core Types..."); const builder = new APIBuilder() - .verbose() .throwException() .fromPackage("hl7.fhir.r4.core", "4.0.1") .typescript({ withDebugComment: false, generateProfile: false }) .outputTo("./src/fhir-types") - .treeShake({ - "hl7.fhir.r4.core": { - "http://hl7.org/fhir/StructureDefinition/Patient": { - ignoreFields: [ - "contact", - "communication", - "photo", - "telecom", - "address", - "link", - ], - }, - "http://hl7.org/fhir/StructureDefinition/Resource": {}, - "http://hl7.org/fhir/StructureDefinition/Bundle": {}, - "http://hl7.org/fhir/StructureDefinition/OperationOutcome": {}, - "http://hl7.org/fhir/StructureDefinition/DomainResource": { - ignoreFields: ["extension", "modifierExtension"], - }, - "http://hl7.org/fhir/StructureDefinition/BackboneElement": { - ignoreFields: ["modifierExtension"], - }, - "http://hl7.org/fhir/StructureDefinition/Element": { - ignoreFields: ["extension"], + .typeSchema({ + treeShake: { + "hl7.fhir.r4.core": { + "http://hl7.org/fhir/StructureDefinition/Patient": { + ignoreFields: [ + "contact", + "communication", + "photo", + "telecom", + "address", + "link", + ], + }, + "http://hl7.org/fhir/StructureDefinition/Resource": {}, + "http://hl7.org/fhir/StructureDefinition/Bundle": {}, + "http://hl7.org/fhir/StructureDefinition/OperationOutcome": {}, + "http://hl7.org/fhir/StructureDefinition/DomainResource": { + ignoreFields: ["extension", "modifierExtension"], + }, + "http://hl7.org/fhir/StructureDefinition/BackboneElement": { + ignoreFields: ["modifierExtension"], + }, + "http://hl7.org/fhir/StructureDefinition/Element": { + ignoreFields: ["extension"], + }, }, }, }) diff --git a/packages/aidbox-client/src/fhir-types/README.md b/packages/aidbox-client/src/fhir-types/README.md new file mode 100644 index 00000000..4c642cbb --- /dev/null +++ b/packages/aidbox-client/src/fhir-types/README.md @@ -0,0 +1,2687 @@ +# IR Report + +## Package: `hl7.fhir.r4.core` + +### Modified Canonicals + +#### `http://hl7.org/fhir/StructureDefinition/BackboneElement` + +Skipped fields: + +- `modifierExtension` + +#### `http://hl7.org/fhir/StructureDefinition/DomainResource` + +Skipped fields: + +- `extension` +- `modifierExtension` + +#### `http://hl7.org/fhir/StructureDefinition/Element` + +Skipped fields: + +- `extension` + +#### `http://hl7.org/fhir/StructureDefinition/Patient` + +Skipped fields: + +- `address` +- `communication` +- `contact` +- `link` +- `photo` +- `telecom` + +### Skipped Canonicals + +- `http://fhir-registry.smarthealthit.org/StructureDefinition/capabilities` +- `http://fhir-registry.smarthealthit.org/StructureDefinition/oauth-uris` +- `http://hl7.org/fhir/StructureDefinition/11179-objectClass` +- `http://hl7.org/fhir/StructureDefinition/11179-objectClassProperty` +- `http://hl7.org/fhir/StructureDefinition/11179-permitted-value-conceptmap` +- `http://hl7.org/fhir/StructureDefinition/11179-permitted-value-valueset` +- `http://hl7.org/fhir/StructureDefinition/Account` +- `http://hl7.org/fhir/StructureDefinition/ActivityDefinition` +- `http://hl7.org/fhir/StructureDefinition/Address` +- `http://hl7.org/fhir/StructureDefinition/AdverseEvent` +- `http://hl7.org/fhir/StructureDefinition/Age` +- `http://hl7.org/fhir/StructureDefinition/AllergyIntolerance` +- `http://hl7.org/fhir/StructureDefinition/Annotation` +- `http://hl7.org/fhir/StructureDefinition/Appointment` +- `http://hl7.org/fhir/StructureDefinition/AppointmentResponse` +- `http://hl7.org/fhir/StructureDefinition/Attachment` +- `http://hl7.org/fhir/StructureDefinition/AuditEvent` +- `http://hl7.org/fhir/StructureDefinition/BackboneElement` +- `http://hl7.org/fhir/StructureDefinition/Basic` +- `http://hl7.org/fhir/StructureDefinition/Binary` +- `http://hl7.org/fhir/StructureDefinition/BiologicallyDerivedProduct` +- `http://hl7.org/fhir/StructureDefinition/BodyStructure` +- `http://hl7.org/fhir/StructureDefinition/Bundle` +- `http://hl7.org/fhir/StructureDefinition/CapabilityStatement` +- `http://hl7.org/fhir/StructureDefinition/CarePlan` +- `http://hl7.org/fhir/StructureDefinition/CareTeam` +- `http://hl7.org/fhir/StructureDefinition/CatalogEntry` +- `http://hl7.org/fhir/StructureDefinition/ChargeItem` +- `http://hl7.org/fhir/StructureDefinition/ChargeItemDefinition` +- `http://hl7.org/fhir/StructureDefinition/Claim` +- `http://hl7.org/fhir/StructureDefinition/ClaimResponse` +- `http://hl7.org/fhir/StructureDefinition/ClinicalImpression` +- `http://hl7.org/fhir/StructureDefinition/CodeSystem` +- `http://hl7.org/fhir/StructureDefinition/Communication` +- `http://hl7.org/fhir/StructureDefinition/CommunicationRequest` +- `http://hl7.org/fhir/StructureDefinition/CompartmentDefinition` +- `http://hl7.org/fhir/StructureDefinition/Composition` +- `http://hl7.org/fhir/StructureDefinition/ConceptMap` +- `http://hl7.org/fhir/StructureDefinition/Condition` +- `http://hl7.org/fhir/StructureDefinition/Consent` +- `http://hl7.org/fhir/StructureDefinition/ContactDetail` +- `http://hl7.org/fhir/StructureDefinition/ContactPoint` +- `http://hl7.org/fhir/StructureDefinition/Contract` +- `http://hl7.org/fhir/StructureDefinition/Contributor` +- `http://hl7.org/fhir/StructureDefinition/Count` +- `http://hl7.org/fhir/StructureDefinition/Coverage` +- `http://hl7.org/fhir/StructureDefinition/CoverageEligibilityRequest` +- `http://hl7.org/fhir/StructureDefinition/CoverageEligibilityResponse` +- `http://hl7.org/fhir/StructureDefinition/DataRequirement` +- `http://hl7.org/fhir/StructureDefinition/Definition` +- `http://hl7.org/fhir/StructureDefinition/DetectedIssue` +- `http://hl7.org/fhir/StructureDefinition/Device` +- `http://hl7.org/fhir/StructureDefinition/DeviceDefinition` +- `http://hl7.org/fhir/StructureDefinition/DeviceMetric` +- `http://hl7.org/fhir/StructureDefinition/DeviceRequest` +- `http://hl7.org/fhir/StructureDefinition/DeviceUseStatement` +- `http://hl7.org/fhir/StructureDefinition/DiagnosticReport` +- `http://hl7.org/fhir/StructureDefinition/DiagnosticReport-geneticsAnalysis` +- `http://hl7.org/fhir/StructureDefinition/DiagnosticReport-geneticsAssessedCondition` +- `http://hl7.org/fhir/StructureDefinition/DiagnosticReport-geneticsFamilyMemberHistory` +- `http://hl7.org/fhir/StructureDefinition/DiagnosticReport-geneticsReferences` +- `http://hl7.org/fhir/StructureDefinition/Distance` +- `http://hl7.org/fhir/StructureDefinition/DocumentManifest` +- `http://hl7.org/fhir/StructureDefinition/DocumentReference` +- `http://hl7.org/fhir/StructureDefinition/DomainResource` +- `http://hl7.org/fhir/StructureDefinition/Dosage` +- `http://hl7.org/fhir/StructureDefinition/Duration` +- `http://hl7.org/fhir/StructureDefinition/EffectEvidenceSynthesis` +- `http://hl7.org/fhir/StructureDefinition/Element` +- `http://hl7.org/fhir/StructureDefinition/ElementDefinition` +- `http://hl7.org/fhir/StructureDefinition/Encounter` +- `http://hl7.org/fhir/StructureDefinition/Endpoint` +- `http://hl7.org/fhir/StructureDefinition/EnrollmentRequest` +- `http://hl7.org/fhir/StructureDefinition/EnrollmentResponse` +- `http://hl7.org/fhir/StructureDefinition/EpisodeOfCare` +- `http://hl7.org/fhir/StructureDefinition/Event` +- `http://hl7.org/fhir/StructureDefinition/EventDefinition` +- `http://hl7.org/fhir/StructureDefinition/Evidence` +- `http://hl7.org/fhir/StructureDefinition/EvidenceVariable` +- `http://hl7.org/fhir/StructureDefinition/ExampleScenario` +- `http://hl7.org/fhir/StructureDefinition/ExplanationOfBenefit` +- `http://hl7.org/fhir/StructureDefinition/Expression` +- `http://hl7.org/fhir/StructureDefinition/Extension` +- `http://hl7.org/fhir/StructureDefinition/FamilyMemberHistory` +- `http://hl7.org/fhir/StructureDefinition/FiveWs` +- `http://hl7.org/fhir/StructureDefinition/Flag` +- `http://hl7.org/fhir/StructureDefinition/Goal` +- `http://hl7.org/fhir/StructureDefinition/GraphDefinition` +- `http://hl7.org/fhir/StructureDefinition/Group` +- `http://hl7.org/fhir/StructureDefinition/GuidanceResponse` +- `http://hl7.org/fhir/StructureDefinition/HealthcareService` +- `http://hl7.org/fhir/StructureDefinition/ImagingStudy` +- `http://hl7.org/fhir/StructureDefinition/Immunization` +- `http://hl7.org/fhir/StructureDefinition/ImmunizationEvaluation` +- `http://hl7.org/fhir/StructureDefinition/ImmunizationRecommendation` +- `http://hl7.org/fhir/StructureDefinition/ImplementationGuide` +- `http://hl7.org/fhir/StructureDefinition/InsurancePlan` +- `http://hl7.org/fhir/StructureDefinition/Invoice` +- `http://hl7.org/fhir/StructureDefinition/Library` +- `http://hl7.org/fhir/StructureDefinition/Linkage` +- `http://hl7.org/fhir/StructureDefinition/List` +- `http://hl7.org/fhir/StructureDefinition/Location` +- `http://hl7.org/fhir/StructureDefinition/MarketingStatus` +- `http://hl7.org/fhir/StructureDefinition/Measure` +- `http://hl7.org/fhir/StructureDefinition/MeasureReport` +- `http://hl7.org/fhir/StructureDefinition/Media` +- `http://hl7.org/fhir/StructureDefinition/Medication` +- `http://hl7.org/fhir/StructureDefinition/MedicationAdministration` +- `http://hl7.org/fhir/StructureDefinition/MedicationDispense` +- `http://hl7.org/fhir/StructureDefinition/MedicationKnowledge` +- `http://hl7.org/fhir/StructureDefinition/MedicationRequest` +- `http://hl7.org/fhir/StructureDefinition/MedicationStatement` +- `http://hl7.org/fhir/StructureDefinition/MedicinalProduct` +- `http://hl7.org/fhir/StructureDefinition/MedicinalProductAuthorization` +- `http://hl7.org/fhir/StructureDefinition/MedicinalProductContraindication` +- `http://hl7.org/fhir/StructureDefinition/MedicinalProductIndication` +- `http://hl7.org/fhir/StructureDefinition/MedicinalProductIngredient` +- `http://hl7.org/fhir/StructureDefinition/MedicinalProductInteraction` +- `http://hl7.org/fhir/StructureDefinition/MedicinalProductManufactured` +- `http://hl7.org/fhir/StructureDefinition/MedicinalProductPackaged` +- `http://hl7.org/fhir/StructureDefinition/MedicinalProductPharmaceutical` +- `http://hl7.org/fhir/StructureDefinition/MedicinalProductUndesirableEffect` +- `http://hl7.org/fhir/StructureDefinition/MessageDefinition` +- `http://hl7.org/fhir/StructureDefinition/MessageHeader` +- `http://hl7.org/fhir/StructureDefinition/MetadataResource` +- `http://hl7.org/fhir/StructureDefinition/MolecularSequence` +- `http://hl7.org/fhir/StructureDefinition/Money` +- `http://hl7.org/fhir/StructureDefinition/MoneyQuantity` +- `http://hl7.org/fhir/StructureDefinition/NamingSystem` +- `http://hl7.org/fhir/StructureDefinition/NutritionOrder` +- `http://hl7.org/fhir/StructureDefinition/Observation` +- `http://hl7.org/fhir/StructureDefinition/ObservationDefinition` +- `http://hl7.org/fhir/StructureDefinition/OperationDefinition` +- `http://hl7.org/fhir/StructureDefinition/OperationOutcome` +- `http://hl7.org/fhir/StructureDefinition/Organization` +- `http://hl7.org/fhir/StructureDefinition/OrganizationAffiliation` +- `http://hl7.org/fhir/StructureDefinition/ParameterDefinition` +- `http://hl7.org/fhir/StructureDefinition/Parameters` +- `http://hl7.org/fhir/StructureDefinition/Patient` +- `http://hl7.org/fhir/StructureDefinition/PaymentNotice` +- `http://hl7.org/fhir/StructureDefinition/PaymentReconciliation` +- `http://hl7.org/fhir/StructureDefinition/Person` +- `http://hl7.org/fhir/StructureDefinition/PlanDefinition` +- `http://hl7.org/fhir/StructureDefinition/Population` +- `http://hl7.org/fhir/StructureDefinition/Practitioner` +- `http://hl7.org/fhir/StructureDefinition/PractitionerRole` +- `http://hl7.org/fhir/StructureDefinition/Procedure` +- `http://hl7.org/fhir/StructureDefinition/ProdCharacteristic` +- `http://hl7.org/fhir/StructureDefinition/ProductShelfLife` +- `http://hl7.org/fhir/StructureDefinition/Provenance` +- `http://hl7.org/fhir/StructureDefinition/Quantity` +- `http://hl7.org/fhir/StructureDefinition/Questionnaire` +- `http://hl7.org/fhir/StructureDefinition/QuestionnaireResponse` +- `http://hl7.org/fhir/StructureDefinition/Range` +- `http://hl7.org/fhir/StructureDefinition/Ratio` +- `http://hl7.org/fhir/StructureDefinition/RelatedArtifact` +- `http://hl7.org/fhir/StructureDefinition/RelatedPerson` +- `http://hl7.org/fhir/StructureDefinition/Request` +- `http://hl7.org/fhir/StructureDefinition/RequestGroup` +- `http://hl7.org/fhir/StructureDefinition/ResearchDefinition` +- `http://hl7.org/fhir/StructureDefinition/ResearchElementDefinition` +- `http://hl7.org/fhir/StructureDefinition/ResearchStudy` +- `http://hl7.org/fhir/StructureDefinition/ResearchSubject` +- `http://hl7.org/fhir/StructureDefinition/Resource` +- `http://hl7.org/fhir/StructureDefinition/RiskAssessment` +- `http://hl7.org/fhir/StructureDefinition/RiskEvidenceSynthesis` +- `http://hl7.org/fhir/StructureDefinition/SampledData` +- `http://hl7.org/fhir/StructureDefinition/Schedule` +- `http://hl7.org/fhir/StructureDefinition/SearchParameter` +- `http://hl7.org/fhir/StructureDefinition/ServiceRequest` +- `http://hl7.org/fhir/StructureDefinition/SimpleQuantity` +- `http://hl7.org/fhir/StructureDefinition/Slot` +- `http://hl7.org/fhir/StructureDefinition/Specimen` +- `http://hl7.org/fhir/StructureDefinition/SpecimenDefinition` +- `http://hl7.org/fhir/StructureDefinition/StructureDefinition` +- `http://hl7.org/fhir/StructureDefinition/StructureMap` +- `http://hl7.org/fhir/StructureDefinition/Subscription` +- `http://hl7.org/fhir/StructureDefinition/Substance` +- `http://hl7.org/fhir/StructureDefinition/SubstanceAmount` +- `http://hl7.org/fhir/StructureDefinition/SubstanceNucleicAcid` +- `http://hl7.org/fhir/StructureDefinition/SubstancePolymer` +- `http://hl7.org/fhir/StructureDefinition/SubstanceProtein` +- `http://hl7.org/fhir/StructureDefinition/SubstanceReferenceInformation` +- `http://hl7.org/fhir/StructureDefinition/SubstanceSourceMaterial` +- `http://hl7.org/fhir/StructureDefinition/SubstanceSpecification` +- `http://hl7.org/fhir/StructureDefinition/SupplyDelivery` +- `http://hl7.org/fhir/StructureDefinition/SupplyRequest` +- `http://hl7.org/fhir/StructureDefinition/Task` +- `http://hl7.org/fhir/StructureDefinition/TerminologyCapabilities` +- `http://hl7.org/fhir/StructureDefinition/TestReport` +- `http://hl7.org/fhir/StructureDefinition/TestScript` +- `http://hl7.org/fhir/StructureDefinition/Timing` +- `http://hl7.org/fhir/StructureDefinition/TriggerDefinition` +- `http://hl7.org/fhir/StructureDefinition/UsageContext` +- `http://hl7.org/fhir/StructureDefinition/ValueSet` +- `http://hl7.org/fhir/StructureDefinition/VerificationResult` +- `http://hl7.org/fhir/StructureDefinition/VisionPrescription` +- `http://hl7.org/fhir/StructureDefinition/actualgroup` +- `http://hl7.org/fhir/StructureDefinition/allergyintolerance-assertedDate` +- `http://hl7.org/fhir/StructureDefinition/allergyintolerance-certainty` +- `http://hl7.org/fhir/StructureDefinition/allergyintolerance-duration` +- `http://hl7.org/fhir/StructureDefinition/allergyintolerance-reasonRefuted` +- `http://hl7.org/fhir/StructureDefinition/allergyintolerance-resolutionAge` +- `http://hl7.org/fhir/StructureDefinition/allergyintolerance-substanceExposureRisk` +- `http://hl7.org/fhir/StructureDefinition/auditevent-Accession` +- `http://hl7.org/fhir/StructureDefinition/auditevent-Anonymized` +- `http://hl7.org/fhir/StructureDefinition/auditevent-Encrypted` +- `http://hl7.org/fhir/StructureDefinition/auditevent-Instance` +- `http://hl7.org/fhir/StructureDefinition/auditevent-MPPS` +- `http://hl7.org/fhir/StructureDefinition/auditevent-NumberOfInstances` +- `http://hl7.org/fhir/StructureDefinition/auditevent-ParticipantObjectContainsStudy` +- `http://hl7.org/fhir/StructureDefinition/auditevent-SOPClass` +- `http://hl7.org/fhir/StructureDefinition/bmi` +- `http://hl7.org/fhir/StructureDefinition/bodySite` +- `http://hl7.org/fhir/StructureDefinition/bodyheight` +- `http://hl7.org/fhir/StructureDefinition/bodytemp` +- `http://hl7.org/fhir/StructureDefinition/bodyweight` +- `http://hl7.org/fhir/StructureDefinition/bp` +- `http://hl7.org/fhir/StructureDefinition/capabilitystatement-expectation` +- `http://hl7.org/fhir/StructureDefinition/capabilitystatement-prohibited` +- `http://hl7.org/fhir/StructureDefinition/capabilitystatement-search-parameter-combination` +- `http://hl7.org/fhir/StructureDefinition/capabilitystatement-supported-system` +- `http://hl7.org/fhir/StructureDefinition/capabilitystatement-websocket` +- `http://hl7.org/fhir/StructureDefinition/careplan-activity-title` +- `http://hl7.org/fhir/StructureDefinition/catalog` +- `http://hl7.org/fhir/StructureDefinition/cdshooksguidanceresponse` +- `http://hl7.org/fhir/StructureDefinition/cdshooksrequestgroup` +- `http://hl7.org/fhir/StructureDefinition/cdshooksserviceplandefinition` +- `http://hl7.org/fhir/StructureDefinition/cholesterol` +- `http://hl7.org/fhir/StructureDefinition/clinicaldocument` +- `http://hl7.org/fhir/StructureDefinition/codesystem-alternate` +- `http://hl7.org/fhir/StructureDefinition/codesystem-author` +- `http://hl7.org/fhir/StructureDefinition/codesystem-concept-comments` +- `http://hl7.org/fhir/StructureDefinition/codesystem-conceptOrder` +- `http://hl7.org/fhir/StructureDefinition/codesystem-effectiveDate` +- `http://hl7.org/fhir/StructureDefinition/codesystem-expirationDate` +- `http://hl7.org/fhir/StructureDefinition/codesystem-history` +- `http://hl7.org/fhir/StructureDefinition/codesystem-keyWord` +- `http://hl7.org/fhir/StructureDefinition/codesystem-label` +- `http://hl7.org/fhir/StructureDefinition/codesystem-map` +- `http://hl7.org/fhir/StructureDefinition/codesystem-otherName` +- `http://hl7.org/fhir/StructureDefinition/codesystem-replacedby` +- `http://hl7.org/fhir/StructureDefinition/codesystem-sourceReference` +- `http://hl7.org/fhir/StructureDefinition/codesystem-trusted-expansion` +- `http://hl7.org/fhir/StructureDefinition/codesystem-usage` +- `http://hl7.org/fhir/StructureDefinition/codesystem-warning` +- `http://hl7.org/fhir/StructureDefinition/codesystem-workflowStatus` +- `http://hl7.org/fhir/StructureDefinition/coding-sctdescid` +- `http://hl7.org/fhir/StructureDefinition/communication-media` +- `http://hl7.org/fhir/StructureDefinition/communicationrequest-initiatingLocation` +- `http://hl7.org/fhir/StructureDefinition/composition-clinicaldocument-otherConfidentiality` +- `http://hl7.org/fhir/StructureDefinition/composition-clinicaldocument-versionNumber` +- `http://hl7.org/fhir/StructureDefinition/composition-section-subject` +- `http://hl7.org/fhir/StructureDefinition/computableplandefinition` +- `http://hl7.org/fhir/StructureDefinition/concept-bidirectional` +- `http://hl7.org/fhir/StructureDefinition/condition-assertedDate` +- `http://hl7.org/fhir/StructureDefinition/condition-dueTo` +- `http://hl7.org/fhir/StructureDefinition/condition-occurredFollowing` +- `http://hl7.org/fhir/StructureDefinition/condition-outcome` +- `http://hl7.org/fhir/StructureDefinition/condition-related` +- `http://hl7.org/fhir/StructureDefinition/condition-ruledOut` +- `http://hl7.org/fhir/StructureDefinition/consent-NotificationEndpoint` +- `http://hl7.org/fhir/StructureDefinition/consent-Transcriber` +- `http://hl7.org/fhir/StructureDefinition/consent-Witness` +- `http://hl7.org/fhir/StructureDefinition/consent-location` +- `http://hl7.org/fhir/StructureDefinition/contactpoint-area` +- `http://hl7.org/fhir/StructureDefinition/contactpoint-country` +- `http://hl7.org/fhir/StructureDefinition/contactpoint-extension` +- `http://hl7.org/fhir/StructureDefinition/contactpoint-local` +- `http://hl7.org/fhir/StructureDefinition/cqf-calculatedValue` +- `http://hl7.org/fhir/StructureDefinition/cqf-cdsHooksEndpoint` +- `http://hl7.org/fhir/StructureDefinition/cqf-citation` +- `http://hl7.org/fhir/StructureDefinition/cqf-encounterClass` +- `http://hl7.org/fhir/StructureDefinition/cqf-encounterType` +- `http://hl7.org/fhir/StructureDefinition/cqf-expression` +- `http://hl7.org/fhir/StructureDefinition/cqf-initialValue` +- `http://hl7.org/fhir/StructureDefinition/cqf-initiatingOrganization` +- `http://hl7.org/fhir/StructureDefinition/cqf-initiatingPerson` +- `http://hl7.org/fhir/StructureDefinition/cqf-library` +- `http://hl7.org/fhir/StructureDefinition/cqf-measureInfo` +- `http://hl7.org/fhir/StructureDefinition/cqf-qualityOfEvidence` +- `http://hl7.org/fhir/StructureDefinition/cqf-questionnaire` +- `http://hl7.org/fhir/StructureDefinition/cqf-receivingOrganization` +- `http://hl7.org/fhir/StructureDefinition/cqf-receivingPerson` +- `http://hl7.org/fhir/StructureDefinition/cqf-recipientLanguage` +- `http://hl7.org/fhir/StructureDefinition/cqf-recipientType` +- `http://hl7.org/fhir/StructureDefinition/cqf-relativeDateTime` +- `http://hl7.org/fhir/StructureDefinition/cqf-strengthOfRecommendation` +- `http://hl7.org/fhir/StructureDefinition/cqf-systemUserLanguage` +- `http://hl7.org/fhir/StructureDefinition/cqf-systemUserTaskContext` +- `http://hl7.org/fhir/StructureDefinition/cqf-systemUserType` +- `http://hl7.org/fhir/StructureDefinition/cqllibrary` +- `http://hl7.org/fhir/StructureDefinition/cqm-ValidityPeriod` +- `http://hl7.org/fhir/StructureDefinition/data-absent-reason` +- `http://hl7.org/fhir/StructureDefinition/designNote` +- `http://hl7.org/fhir/StructureDefinition/device-implantStatus` +- `http://hl7.org/fhir/StructureDefinition/devicemetricobservation` +- `http://hl7.org/fhir/StructureDefinition/devicerequest-patientInstruction` +- `http://hl7.org/fhir/StructureDefinition/diagnosticReport-addendumOf` +- `http://hl7.org/fhir/StructureDefinition/diagnosticReport-extends` +- `http://hl7.org/fhir/StructureDefinition/diagnosticReport-locationPerformed` +- `http://hl7.org/fhir/StructureDefinition/diagnosticReport-replaces` +- `http://hl7.org/fhir/StructureDefinition/diagnosticReport-risk` +- `http://hl7.org/fhir/StructureDefinition/diagnosticReport-summaryOf` +- `http://hl7.org/fhir/StructureDefinition/diagnosticreport-genetics` +- `http://hl7.org/fhir/StructureDefinition/display` +- `http://hl7.org/fhir/StructureDefinition/ehrsrle-auditevent` +- `http://hl7.org/fhir/StructureDefinition/ehrsrle-provenance` +- `http://hl7.org/fhir/StructureDefinition/elementdefinition-allowedUnits` +- `http://hl7.org/fhir/StructureDefinition/elementdefinition-bestpractice` +- `http://hl7.org/fhir/StructureDefinition/elementdefinition-bestpractice-explanation` +- `http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName` +- `http://hl7.org/fhir/StructureDefinition/elementdefinition-de` +- `http://hl7.org/fhir/StructureDefinition/elementdefinition-equivalence` +- `http://hl7.org/fhir/StructureDefinition/elementdefinition-identifier` +- `http://hl7.org/fhir/StructureDefinition/elementdefinition-inheritedExtensibleValueSet` +- `http://hl7.org/fhir/StructureDefinition/elementdefinition-isCommonBinding` +- `http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet` +- `http://hl7.org/fhir/StructureDefinition/elementdefinition-minValueSet` +- `http://hl7.org/fhir/StructureDefinition/elementdefinition-namespace` +- `http://hl7.org/fhir/StructureDefinition/elementdefinition-profile-element` +- `http://hl7.org/fhir/StructureDefinition/elementdefinition-question` +- `http://hl7.org/fhir/StructureDefinition/elementdefinition-selector` +- `http://hl7.org/fhir/StructureDefinition/elementdefinition-translatable` +- `http://hl7.org/fhir/StructureDefinition/encounter-associatedEncounter` +- `http://hl7.org/fhir/StructureDefinition/encounter-modeOfArrival` +- `http://hl7.org/fhir/StructureDefinition/encounter-reasonCancelled` +- `http://hl7.org/fhir/StructureDefinition/entryFormat` +- `http://hl7.org/fhir/StructureDefinition/event-basedOn` +- `http://hl7.org/fhir/StructureDefinition/event-eventHistory` +- `http://hl7.org/fhir/StructureDefinition/event-location` +- `http://hl7.org/fhir/StructureDefinition/event-partOf` +- `http://hl7.org/fhir/StructureDefinition/event-performerFunction` +- `http://hl7.org/fhir/StructureDefinition/event-statusReason` +- `http://hl7.org/fhir/StructureDefinition/example-composition` +- `http://hl7.org/fhir/StructureDefinition/example-section-library` +- `http://hl7.org/fhir/StructureDefinition/family-member-history-genetics-observation` +- `http://hl7.org/fhir/StructureDefinition/family-member-history-genetics-parent` +- `http://hl7.org/fhir/StructureDefinition/family-member-history-genetics-sibling` +- `http://hl7.org/fhir/StructureDefinition/familymemberhistory-abatement` +- `http://hl7.org/fhir/StructureDefinition/familymemberhistory-genetic` +- `http://hl7.org/fhir/StructureDefinition/familymemberhistory-patient-record` +- `http://hl7.org/fhir/StructureDefinition/familymemberhistory-severity` +- `http://hl7.org/fhir/StructureDefinition/familymemberhistory-type` +- `http://hl7.org/fhir/StructureDefinition/flag-detail` +- `http://hl7.org/fhir/StructureDefinition/flag-priority` +- `http://hl7.org/fhir/StructureDefinition/geolocation` +- `http://hl7.org/fhir/StructureDefinition/goal-acceptance` +- `http://hl7.org/fhir/StructureDefinition/goal-reasonRejected` +- `http://hl7.org/fhir/StructureDefinition/goal-relationship` +- `http://hl7.org/fhir/StructureDefinition/groupdefinition` +- `http://hl7.org/fhir/StructureDefinition/hdlcholesterol` +- `http://hl7.org/fhir/StructureDefinition/headcircum` +- `http://hl7.org/fhir/StructureDefinition/heartrate` +- `http://hl7.org/fhir/StructureDefinition/hla-genotyping-results-allele-database` +- `http://hl7.org/fhir/StructureDefinition/hla-genotyping-results-glstring` +- `http://hl7.org/fhir/StructureDefinition/hla-genotyping-results-haploid` +- `http://hl7.org/fhir/StructureDefinition/hla-genotyping-results-method` +- `http://hl7.org/fhir/StructureDefinition/hlaresult` +- `http://hl7.org/fhir/StructureDefinition/http-response-header` +- `http://hl7.org/fhir/StructureDefinition/humanname-assembly-order` +- `http://hl7.org/fhir/StructureDefinition/humanname-fathers-family` +- `http://hl7.org/fhir/StructureDefinition/humanname-mothers-family` +- `http://hl7.org/fhir/StructureDefinition/humanname-own-name` +- `http://hl7.org/fhir/StructureDefinition/humanname-own-prefix` +- `http://hl7.org/fhir/StructureDefinition/humanname-partner-name` +- `http://hl7.org/fhir/StructureDefinition/humanname-partner-prefix` +- `http://hl7.org/fhir/StructureDefinition/identifier-validDate` +- `http://hl7.org/fhir/StructureDefinition/iso21090-AD-use` +- `http://hl7.org/fhir/StructureDefinition/iso21090-ADXP-additionalLocator` +- `http://hl7.org/fhir/StructureDefinition/iso21090-ADXP-buildingNumberSuffix` +- `http://hl7.org/fhir/StructureDefinition/iso21090-ADXP-careOf` +- `http://hl7.org/fhir/StructureDefinition/iso21090-ADXP-censusTract` +- `http://hl7.org/fhir/StructureDefinition/iso21090-ADXP-delimiter` +- `http://hl7.org/fhir/StructureDefinition/iso21090-ADXP-deliveryAddressLine` +- `http://hl7.org/fhir/StructureDefinition/iso21090-ADXP-deliveryInstallationArea` +- `http://hl7.org/fhir/StructureDefinition/iso21090-ADXP-deliveryInstallationQualifier` +- `http://hl7.org/fhir/StructureDefinition/iso21090-ADXP-deliveryInstallationType` +- `http://hl7.org/fhir/StructureDefinition/iso21090-ADXP-deliveryMode` +- `http://hl7.org/fhir/StructureDefinition/iso21090-ADXP-deliveryModeIdentifier` +- `http://hl7.org/fhir/StructureDefinition/iso21090-ADXP-direction` +- `http://hl7.org/fhir/StructureDefinition/iso21090-ADXP-houseNumber` +- `http://hl7.org/fhir/StructureDefinition/iso21090-ADXP-houseNumberNumeric` +- `http://hl7.org/fhir/StructureDefinition/iso21090-ADXP-postBox` +- `http://hl7.org/fhir/StructureDefinition/iso21090-ADXP-precinct` +- `http://hl7.org/fhir/StructureDefinition/iso21090-ADXP-streetAddressLine` +- `http://hl7.org/fhir/StructureDefinition/iso21090-ADXP-streetName` +- `http://hl7.org/fhir/StructureDefinition/iso21090-ADXP-streetNameBase` +- `http://hl7.org/fhir/StructureDefinition/iso21090-ADXP-streetNameType` +- `http://hl7.org/fhir/StructureDefinition/iso21090-ADXP-unitID` +- `http://hl7.org/fhir/StructureDefinition/iso21090-ADXP-unitType` +- `http://hl7.org/fhir/StructureDefinition/iso21090-EN-qualifier` +- `http://hl7.org/fhir/StructureDefinition/iso21090-EN-representation` +- `http://hl7.org/fhir/StructureDefinition/iso21090-EN-use` +- `http://hl7.org/fhir/StructureDefinition/iso21090-PQ-translation` +- `http://hl7.org/fhir/StructureDefinition/iso21090-SC-coding` +- `http://hl7.org/fhir/StructureDefinition/iso21090-TEL-address` +- `http://hl7.org/fhir/StructureDefinition/iso21090-nullFlavor` +- `http://hl7.org/fhir/StructureDefinition/iso21090-preferred` +- `http://hl7.org/fhir/StructureDefinition/iso21090-uncertainty` +- `http://hl7.org/fhir/StructureDefinition/iso21090-uncertaintyType` +- `http://hl7.org/fhir/StructureDefinition/language` +- `http://hl7.org/fhir/StructureDefinition/ldlcholesterol` +- `http://hl7.org/fhir/StructureDefinition/lipidprofile` +- `http://hl7.org/fhir/StructureDefinition/list-changeBase` +- `http://hl7.org/fhir/StructureDefinition/location-boundary-geojson` +- `http://hl7.org/fhir/StructureDefinition/location-distance` +- `http://hl7.org/fhir/StructureDefinition/markdown` +- `http://hl7.org/fhir/StructureDefinition/match-grade` +- `http://hl7.org/fhir/StructureDefinition/maxDecimalPlaces` +- `http://hl7.org/fhir/StructureDefinition/maxSize` +- `http://hl7.org/fhir/StructureDefinition/maxValue` +- `http://hl7.org/fhir/StructureDefinition/messageheader-response-request` +- `http://hl7.org/fhir/StructureDefinition/mimeType` +- `http://hl7.org/fhir/StructureDefinition/minLength` +- `http://hl7.org/fhir/StructureDefinition/minValue` +- `http://hl7.org/fhir/StructureDefinition/narrativeLink` +- `http://hl7.org/fhir/StructureDefinition/nutritionorder-adaptiveFeedingDevice` +- `http://hl7.org/fhir/StructureDefinition/observation-bodyPosition` +- `http://hl7.org/fhir/StructureDefinition/observation-delta` +- `http://hl7.org/fhir/StructureDefinition/observation-deviceCode` +- `http://hl7.org/fhir/StructureDefinition/observation-focusCode` +- `http://hl7.org/fhir/StructureDefinition/observation-gatewayDevice` +- `http://hl7.org/fhir/StructureDefinition/observation-genetics` +- `http://hl7.org/fhir/StructureDefinition/observation-geneticsAllele` +- `http://hl7.org/fhir/StructureDefinition/observation-geneticsAminoAcidChange` +- `http://hl7.org/fhir/StructureDefinition/observation-geneticsAncestry` +- `http://hl7.org/fhir/StructureDefinition/observation-geneticsCopyNumberEvent` +- `http://hl7.org/fhir/StructureDefinition/observation-geneticsDNARegionName` +- `http://hl7.org/fhir/StructureDefinition/observation-geneticsGene` +- `http://hl7.org/fhir/StructureDefinition/observation-geneticsGenomicSourceClass` +- `http://hl7.org/fhir/StructureDefinition/observation-geneticsInterpretation` +- `http://hl7.org/fhir/StructureDefinition/observation-geneticsPhaseSet` +- `http://hl7.org/fhir/StructureDefinition/observation-geneticsVariant` +- `http://hl7.org/fhir/StructureDefinition/observation-precondition` +- `http://hl7.org/fhir/StructureDefinition/observation-reagent` +- `http://hl7.org/fhir/StructureDefinition/observation-replaces` +- `http://hl7.org/fhir/StructureDefinition/observation-secondaryFinding` +- `http://hl7.org/fhir/StructureDefinition/observation-sequelTo` +- `http://hl7.org/fhir/StructureDefinition/observation-specimenCode` +- `http://hl7.org/fhir/StructureDefinition/observation-timeOffset` +- `http://hl7.org/fhir/StructureDefinition/oid` +- `http://hl7.org/fhir/StructureDefinition/openEHR-administration` +- `http://hl7.org/fhir/StructureDefinition/openEHR-careplan` +- `http://hl7.org/fhir/StructureDefinition/openEHR-exposureDate` +- `http://hl7.org/fhir/StructureDefinition/openEHR-exposureDescription` +- `http://hl7.org/fhir/StructureDefinition/openEHR-exposureDuration` +- `http://hl7.org/fhir/StructureDefinition/openEHR-location` +- `http://hl7.org/fhir/StructureDefinition/openEHR-management` +- `http://hl7.org/fhir/StructureDefinition/openEHR-test` +- `http://hl7.org/fhir/StructureDefinition/operationdefinition-allowed-type` +- `http://hl7.org/fhir/StructureDefinition/operationdefinition-profile` +- `http://hl7.org/fhir/StructureDefinition/operationoutcome-authority` +- `http://hl7.org/fhir/StructureDefinition/operationoutcome-detectedIssue` +- `http://hl7.org/fhir/StructureDefinition/operationoutcome-issue-source` +- `http://hl7.org/fhir/StructureDefinition/ordinalValue` +- `http://hl7.org/fhir/StructureDefinition/organization-period` +- `http://hl7.org/fhir/StructureDefinition/organization-preferredContact` +- `http://hl7.org/fhir/StructureDefinition/organizationaffiliation-primaryInd` +- `http://hl7.org/fhir/StructureDefinition/originalText` +- `http://hl7.org/fhir/StructureDefinition/oxygensat` +- `http://hl7.org/fhir/StructureDefinition/parameters-fullUrl` +- `http://hl7.org/fhir/StructureDefinition/patient-adoptionInfo` +- `http://hl7.org/fhir/StructureDefinition/patient-animal` +- `http://hl7.org/fhir/StructureDefinition/patient-birthPlace` +- `http://hl7.org/fhir/StructureDefinition/patient-birthTime` +- `http://hl7.org/fhir/StructureDefinition/patient-cadavericDonor` +- `http://hl7.org/fhir/StructureDefinition/patient-citizenship` +- `http://hl7.org/fhir/StructureDefinition/patient-congregation` +- `http://hl7.org/fhir/StructureDefinition/patient-disability` +- `http://hl7.org/fhir/StructureDefinition/patient-genderIdentity` +- `http://hl7.org/fhir/StructureDefinition/patient-importance` +- `http://hl7.org/fhir/StructureDefinition/patient-interpreterRequired` +- `http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName` +- `http://hl7.org/fhir/StructureDefinition/patient-nationality` +- `http://hl7.org/fhir/StructureDefinition/patient-preferenceType` +- `http://hl7.org/fhir/StructureDefinition/patient-proficiency` +- `http://hl7.org/fhir/StructureDefinition/patient-relatedPerson` +- `http://hl7.org/fhir/StructureDefinition/patient-religion` +- `http://hl7.org/fhir/StructureDefinition/picoelement` +- `http://hl7.org/fhir/StructureDefinition/positiveInt` +- `http://hl7.org/fhir/StructureDefinition/practitioner-animalSpecies` +- `http://hl7.org/fhir/StructureDefinition/practitionerrole-primaryInd` +- `http://hl7.org/fhir/StructureDefinition/procedure-approachBodyStructure` +- `http://hl7.org/fhir/StructureDefinition/procedure-causedBy` +- `http://hl7.org/fhir/StructureDefinition/procedure-directedBy` +- `http://hl7.org/fhir/StructureDefinition/procedure-incisionDateTime` +- `http://hl7.org/fhir/StructureDefinition/procedure-method` +- `http://hl7.org/fhir/StructureDefinition/procedure-progressStatus` +- `http://hl7.org/fhir/StructureDefinition/procedure-schedule` +- `http://hl7.org/fhir/StructureDefinition/procedure-targetBodyStructure` +- `http://hl7.org/fhir/StructureDefinition/provenance-relevant-history` +- `http://hl7.org/fhir/StructureDefinition/quantity-precision` +- `http://hl7.org/fhir/StructureDefinition/questionnaire-baseType` +- `http://hl7.org/fhir/StructureDefinition/questionnaire-choiceOrientation` +- `http://hl7.org/fhir/StructureDefinition/questionnaire-constraint` +- `http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory` +- `http://hl7.org/fhir/StructureDefinition/questionnaire-fhirType` +- `http://hl7.org/fhir/StructureDefinition/questionnaire-hidden` +- `http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl` +- `http://hl7.org/fhir/StructureDefinition/questionnaire-maxOccurs` +- `http://hl7.org/fhir/StructureDefinition/questionnaire-minOccurs` +- `http://hl7.org/fhir/StructureDefinition/questionnaire-optionExclusive` +- `http://hl7.org/fhir/StructureDefinition/questionnaire-optionPrefix` +- `http://hl7.org/fhir/StructureDefinition/questionnaire-referenceFilter` +- `http://hl7.org/fhir/StructureDefinition/questionnaire-referenceProfile` +- `http://hl7.org/fhir/StructureDefinition/questionnaire-referenceResource` +- `http://hl7.org/fhir/StructureDefinition/questionnaire-signatureRequired` +- `http://hl7.org/fhir/StructureDefinition/questionnaire-sliderStepValue` +- `http://hl7.org/fhir/StructureDefinition/questionnaire-supportLink` +- `http://hl7.org/fhir/StructureDefinition/questionnaire-unit` +- `http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption` +- `http://hl7.org/fhir/StructureDefinition/questionnaire-unitValueSet` +- `http://hl7.org/fhir/StructureDefinition/questionnaire-usageMode` +- `http://hl7.org/fhir/StructureDefinition/questionnaireresponse-author` +- `http://hl7.org/fhir/StructureDefinition/questionnaireresponse-completionMode` +- `http://hl7.org/fhir/StructureDefinition/questionnaireresponse-reason` +- `http://hl7.org/fhir/StructureDefinition/questionnaireresponse-reviewer` +- `http://hl7.org/fhir/StructureDefinition/questionnaireresponse-signature` +- `http://hl7.org/fhir/StructureDefinition/regex` +- `http://hl7.org/fhir/StructureDefinition/relative-date` +- `http://hl7.org/fhir/StructureDefinition/rendered-value` +- `http://hl7.org/fhir/StructureDefinition/rendering-markdown` +- `http://hl7.org/fhir/StructureDefinition/rendering-style` +- `http://hl7.org/fhir/StructureDefinition/rendering-styleSensitive` +- `http://hl7.org/fhir/StructureDefinition/rendering-xhtml` +- `http://hl7.org/fhir/StructureDefinition/replaces` +- `http://hl7.org/fhir/StructureDefinition/request-doNotPerform` +- `http://hl7.org/fhir/StructureDefinition/request-insurance` +- `http://hl7.org/fhir/StructureDefinition/request-performerOrder` +- `http://hl7.org/fhir/StructureDefinition/request-relevantHistory` +- `http://hl7.org/fhir/StructureDefinition/request-replaces` +- `http://hl7.org/fhir/StructureDefinition/request-statusReason` +- `http://hl7.org/fhir/StructureDefinition/resource-approvalDate` +- `http://hl7.org/fhir/StructureDefinition/resource-effectivePeriod` +- `http://hl7.org/fhir/StructureDefinition/resource-lastReviewDate` +- `http://hl7.org/fhir/StructureDefinition/resource-pertainsToGoal` +- `http://hl7.org/fhir/StructureDefinition/resprate` +- `http://hl7.org/fhir/StructureDefinition/servicerequest-genetics` +- `http://hl7.org/fhir/StructureDefinition/servicerequest-geneticsItem` +- `http://hl7.org/fhir/StructureDefinition/servicerequest-precondition` +- `http://hl7.org/fhir/StructureDefinition/servicerequest-questionnaireRequest` +- `http://hl7.org/fhir/StructureDefinition/shareableactivitydefinition` +- `http://hl7.org/fhir/StructureDefinition/shareablecodesystem` +- `http://hl7.org/fhir/StructureDefinition/shareablelibrary` +- `http://hl7.org/fhir/StructureDefinition/shareablemeasure` +- `http://hl7.org/fhir/StructureDefinition/shareableplandefinition` +- `http://hl7.org/fhir/StructureDefinition/shareablevalueset` +- `http://hl7.org/fhir/StructureDefinition/specimen-collectionPriority` +- `http://hl7.org/fhir/StructureDefinition/specimen-isDryWeight` +- `http://hl7.org/fhir/StructureDefinition/specimen-processingTime` +- `http://hl7.org/fhir/StructureDefinition/specimen-sequenceNumber` +- `http://hl7.org/fhir/StructureDefinition/specimen-specialHandling` +- `http://hl7.org/fhir/StructureDefinition/structuredefinition-ancestor` +- `http://hl7.org/fhir/StructureDefinition/structuredefinition-applicable-version` +- `http://hl7.org/fhir/StructureDefinition/structuredefinition-category` +- `http://hl7.org/fhir/StructureDefinition/structuredefinition-codegen-super` +- `http://hl7.org/fhir/StructureDefinition/structuredefinition-dependencies` +- `http://hl7.org/fhir/StructureDefinition/structuredefinition-display-hint` +- `http://hl7.org/fhir/StructureDefinition/structuredefinition-explicit-type-name` +- `http://hl7.org/fhir/StructureDefinition/structuredefinition-fhir-type` +- `http://hl7.org/fhir/StructureDefinition/structuredefinition-fmm` +- `http://hl7.org/fhir/StructureDefinition/structuredefinition-fmm-no-warnings` +- `http://hl7.org/fhir/StructureDefinition/structuredefinition-hierarchy` +- `http://hl7.org/fhir/StructureDefinition/structuredefinition-normative-version` +- `http://hl7.org/fhir/StructureDefinition/structuredefinition-security-category` +- `http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status` +- `http://hl7.org/fhir/StructureDefinition/structuredefinition-summary` +- `http://hl7.org/fhir/StructureDefinition/structuredefinition-table-name` +- `http://hl7.org/fhir/StructureDefinition/structuredefinition-template-status` +- `http://hl7.org/fhir/StructureDefinition/structuredefinition-wg` +- `http://hl7.org/fhir/StructureDefinition/structuredefinition-xml-no-order` +- `http://hl7.org/fhir/StructureDefinition/synthesis` +- `http://hl7.org/fhir/StructureDefinition/task-candidateList` +- `http://hl7.org/fhir/StructureDefinition/task-replaces` +- `http://hl7.org/fhir/StructureDefinition/time` +- `http://hl7.org/fhir/StructureDefinition/timing-dayOfMonth` +- `http://hl7.org/fhir/StructureDefinition/timing-daysOfCycle` +- `http://hl7.org/fhir/StructureDefinition/timing-exact` +- `http://hl7.org/fhir/StructureDefinition/translation` +- `http://hl7.org/fhir/StructureDefinition/triglyceride` +- `http://hl7.org/fhir/StructureDefinition/tz-code` +- `http://hl7.org/fhir/StructureDefinition/tz-offset` +- `http://hl7.org/fhir/StructureDefinition/url` +- `http://hl7.org/fhir/StructureDefinition/usagecontext-group` +- `http://hl7.org/fhir/StructureDefinition/uuid` +- `http://hl7.org/fhir/StructureDefinition/valueset-activityStatusDate` +- `http://hl7.org/fhir/StructureDefinition/valueset-author` +- `http://hl7.org/fhir/StructureDefinition/valueset-authoritativeSource` +- `http://hl7.org/fhir/StructureDefinition/valueset-caseSensitive` +- `http://hl7.org/fhir/StructureDefinition/valueset-concept-comments` +- `http://hl7.org/fhir/StructureDefinition/valueset-concept-definition` +- `http://hl7.org/fhir/StructureDefinition/valueset-conceptOrder` +- `http://hl7.org/fhir/StructureDefinition/valueset-deprecated` +- `http://hl7.org/fhir/StructureDefinition/valueset-effectiveDate` +- `http://hl7.org/fhir/StructureDefinition/valueset-expand-group` +- `http://hl7.org/fhir/StructureDefinition/valueset-expand-rules` +- `http://hl7.org/fhir/StructureDefinition/valueset-expansionSource` +- `http://hl7.org/fhir/StructureDefinition/valueset-expirationDate` +- `http://hl7.org/fhir/StructureDefinition/valueset-expression` +- `http://hl7.org/fhir/StructureDefinition/valueset-extensible` +- `http://hl7.org/fhir/StructureDefinition/valueset-keyWord` +- `http://hl7.org/fhir/StructureDefinition/valueset-label` +- `http://hl7.org/fhir/StructureDefinition/valueset-map` +- `http://hl7.org/fhir/StructureDefinition/valueset-otherName` +- `http://hl7.org/fhir/StructureDefinition/valueset-parameterSource` +- `http://hl7.org/fhir/StructureDefinition/valueset-reference` +- `http://hl7.org/fhir/StructureDefinition/valueset-rules-text` +- `http://hl7.org/fhir/StructureDefinition/valueset-sourceReference` +- `http://hl7.org/fhir/StructureDefinition/valueset-special-status` +- `http://hl7.org/fhir/StructureDefinition/valueset-steward` +- `http://hl7.org/fhir/StructureDefinition/valueset-supplement` +- `http://hl7.org/fhir/StructureDefinition/valueset-system` +- `http://hl7.org/fhir/StructureDefinition/valueset-systemName` +- `http://hl7.org/fhir/StructureDefinition/valueset-systemRef` +- `http://hl7.org/fhir/StructureDefinition/valueset-toocostly` +- `http://hl7.org/fhir/StructureDefinition/valueset-trusted-expansion` +- `http://hl7.org/fhir/StructureDefinition/valueset-unclosed` +- `http://hl7.org/fhir/StructureDefinition/valueset-usage` +- `http://hl7.org/fhir/StructureDefinition/valueset-warning` +- `http://hl7.org/fhir/StructureDefinition/valueset-workflowStatus` +- `http://hl7.org/fhir/StructureDefinition/variable` +- `http://hl7.org/fhir/StructureDefinition/vitalsigns` +- `http://hl7.org/fhir/StructureDefinition/vitalspanel` +- `http://hl7.org/fhir/StructureDefinition/workflow-episodeOfCare` +- `http://hl7.org/fhir/StructureDefinition/workflow-instantiatesCanonical` +- `http://hl7.org/fhir/StructureDefinition/workflow-instantiatesUri` +- `http://hl7.org/fhir/StructureDefinition/workflow-reasonCode` +- `http://hl7.org/fhir/StructureDefinition/workflow-reasonReference` +- `http://hl7.org/fhir/StructureDefinition/workflow-relatedArtifact` +- `http://hl7.org/fhir/StructureDefinition/workflow-researchStudy` +- `http://hl7.org/fhir/StructureDefinition/workflow-supportingInfo` +- `http://hl7.org/fhir/ValueSet/FHIR-version` +- `http://hl7.org/fhir/ValueSet/abstract-types` +- `http://hl7.org/fhir/ValueSet/account-status` +- `http://hl7.org/fhir/ValueSet/account-type` +- `http://hl7.org/fhir/ValueSet/action-cardinality-behavior` +- `http://hl7.org/fhir/ValueSet/action-condition-kind` +- `http://hl7.org/fhir/ValueSet/action-grouping-behavior` +- `http://hl7.org/fhir/ValueSet/action-participant-role` +- `http://hl7.org/fhir/ValueSet/action-participant-type` +- `http://hl7.org/fhir/ValueSet/action-precheck-behavior` +- `http://hl7.org/fhir/ValueSet/action-relationship-type` +- `http://hl7.org/fhir/ValueSet/action-required-behavior` +- `http://hl7.org/fhir/ValueSet/action-selection-behavior` +- `http://hl7.org/fhir/ValueSet/action-type` +- `http://hl7.org/fhir/ValueSet/activity-definition-category` +- `http://hl7.org/fhir/ValueSet/additional-instruction-codes` +- `http://hl7.org/fhir/ValueSet/additionalmaterials` +- `http://hl7.org/fhir/ValueSet/address-type` +- `http://hl7.org/fhir/ValueSet/address-use` +- `http://hl7.org/fhir/ValueSet/adjudication` +- `http://hl7.org/fhir/ValueSet/adjudication-error` +- `http://hl7.org/fhir/ValueSet/adjudication-reason` +- `http://hl7.org/fhir/ValueSet/administration-method-codes` +- `http://hl7.org/fhir/ValueSet/administrative-gender` +- `http://hl7.org/fhir/ValueSet/adverse-event-actuality` +- `http://hl7.org/fhir/ValueSet/adverse-event-category` +- `http://hl7.org/fhir/ValueSet/adverse-event-causality-assess` +- `http://hl7.org/fhir/ValueSet/adverse-event-causality-method` +- `http://hl7.org/fhir/ValueSet/adverse-event-outcome` +- `http://hl7.org/fhir/ValueSet/adverse-event-seriousness` +- `http://hl7.org/fhir/ValueSet/adverse-event-severity` +- `http://hl7.org/fhir/ValueSet/adverse-event-type` +- `http://hl7.org/fhir/ValueSet/age-units` +- `http://hl7.org/fhir/ValueSet/all-distance-units` +- `http://hl7.org/fhir/ValueSet/all-languages` +- `http://hl7.org/fhir/ValueSet/all-time-units` +- `http://hl7.org/fhir/ValueSet/all-types` +- `http://hl7.org/fhir/ValueSet/allelename` +- `http://hl7.org/fhir/ValueSet/allerg-intol-substance-exp-risk` +- `http://hl7.org/fhir/ValueSet/allergy-intolerance-category` +- `http://hl7.org/fhir/ValueSet/allergy-intolerance-criticality` +- `http://hl7.org/fhir/ValueSet/allergy-intolerance-type` +- `http://hl7.org/fhir/ValueSet/allergyintolerance-clinical` +- `http://hl7.org/fhir/ValueSet/allergyintolerance-code` +- `http://hl7.org/fhir/ValueSet/allergyintolerance-verification` +- `http://hl7.org/fhir/ValueSet/animal-breeds` +- `http://hl7.org/fhir/ValueSet/animal-genderstatus` +- `http://hl7.org/fhir/ValueSet/animal-species` +- `http://hl7.org/fhir/ValueSet/appointment-cancellation-reason` +- `http://hl7.org/fhir/ValueSet/appointmentstatus` +- `http://hl7.org/fhir/ValueSet/approach-site-codes` +- `http://hl7.org/fhir/ValueSet/assert-direction-codes` +- `http://hl7.org/fhir/ValueSet/assert-operator-codes` +- `http://hl7.org/fhir/ValueSet/assert-response-code-types` +- `http://hl7.org/fhir/ValueSet/asset-availability` +- `http://hl7.org/fhir/ValueSet/audit-entity-type` +- `http://hl7.org/fhir/ValueSet/audit-event-action` +- `http://hl7.org/fhir/ValueSet/audit-event-outcome` +- `http://hl7.org/fhir/ValueSet/audit-event-sub-type` +- `http://hl7.org/fhir/ValueSet/audit-event-type` +- `http://hl7.org/fhir/ValueSet/audit-source-type` +- `http://hl7.org/fhir/ValueSet/basic-resource-type` +- `http://hl7.org/fhir/ValueSet/benefit-network` +- `http://hl7.org/fhir/ValueSet/benefit-term` +- `http://hl7.org/fhir/ValueSet/benefit-type` +- `http://hl7.org/fhir/ValueSet/benefit-unit` +- `http://hl7.org/fhir/ValueSet/binding-strength` +- `http://hl7.org/fhir/ValueSet/body-site` +- `http://hl7.org/fhir/ValueSet/bodysite-laterality` +- `http://hl7.org/fhir/ValueSet/bodystructure-code` +- `http://hl7.org/fhir/ValueSet/bodystructure-relative-location` +- `http://hl7.org/fhir/ValueSet/bundle-type` +- `http://hl7.org/fhir/ValueSet/c80-doc-typecodes` +- `http://hl7.org/fhir/ValueSet/c80-facilitycodes` +- `http://hl7.org/fhir/ValueSet/c80-practice-codes` +- `http://hl7.org/fhir/ValueSet/capability-statement-kind` +- `http://hl7.org/fhir/ValueSet/care-plan-activity-kind` +- `http://hl7.org/fhir/ValueSet/care-plan-activity-outcome` +- `http://hl7.org/fhir/ValueSet/care-plan-activity-status` +- `http://hl7.org/fhir/ValueSet/care-plan-category` +- `http://hl7.org/fhir/ValueSet/care-plan-intent` +- `http://hl7.org/fhir/ValueSet/care-team-category` +- `http://hl7.org/fhir/ValueSet/care-team-status` +- `http://hl7.org/fhir/ValueSet/catalogType` +- `http://hl7.org/fhir/ValueSet/cdshooks-indicator` +- `http://hl7.org/fhir/ValueSet/certainty-subcomponent-rating` +- `http://hl7.org/fhir/ValueSet/certainty-subcomponent-type` +- `http://hl7.org/fhir/ValueSet/chargeitem-billingcodes` +- `http://hl7.org/fhir/ValueSet/chargeitem-status` +- `http://hl7.org/fhir/ValueSet/choice-list-orientation` +- `http://hl7.org/fhir/ValueSet/chromosome-human` +- `http://hl7.org/fhir/ValueSet/claim-careteamrole` +- `http://hl7.org/fhir/ValueSet/claim-exception` +- `http://hl7.org/fhir/ValueSet/claim-informationcategory` +- `http://hl7.org/fhir/ValueSet/claim-modifiers` +- `http://hl7.org/fhir/ValueSet/claim-subtype` +- `http://hl7.org/fhir/ValueSet/claim-type` +- `http://hl7.org/fhir/ValueSet/claim-use` +- `http://hl7.org/fhir/ValueSet/clinical-findings` +- `http://hl7.org/fhir/ValueSet/clinicalimpression-prognosis` +- `http://hl7.org/fhir/ValueSet/clinicalimpression-status` +- `http://hl7.org/fhir/ValueSet/clinvar` +- `http://hl7.org/fhir/ValueSet/code-search-support` +- `http://hl7.org/fhir/ValueSet/codesystem-altcode-kind` +- `http://hl7.org/fhir/ValueSet/codesystem-content-mode` +- `http://hl7.org/fhir/ValueSet/codesystem-hierarchy-meaning` +- `http://hl7.org/fhir/ValueSet/common-tags` +- `http://hl7.org/fhir/ValueSet/communication-category` +- `http://hl7.org/fhir/ValueSet/communication-not-done-reason` +- `http://hl7.org/fhir/ValueSet/communication-topic` +- `http://hl7.org/fhir/ValueSet/compartment-type` +- `http://hl7.org/fhir/ValueSet/composite-measure-scoring` +- `http://hl7.org/fhir/ValueSet/composition-altcode-kind` +- `http://hl7.org/fhir/ValueSet/composition-attestation-mode` +- `http://hl7.org/fhir/ValueSet/composition-status` +- `http://hl7.org/fhir/ValueSet/concept-map-equivalence` +- `http://hl7.org/fhir/ValueSet/concept-property-type` +- `http://hl7.org/fhir/ValueSet/concept-subsumption-outcome` +- `http://hl7.org/fhir/ValueSet/conceptmap-unmapped-mode` +- `http://hl7.org/fhir/ValueSet/condition-category` +- `http://hl7.org/fhir/ValueSet/condition-cause` +- `http://hl7.org/fhir/ValueSet/condition-clinical` +- `http://hl7.org/fhir/ValueSet/condition-code` +- `http://hl7.org/fhir/ValueSet/condition-outcome` +- `http://hl7.org/fhir/ValueSet/condition-predecessor` +- `http://hl7.org/fhir/ValueSet/condition-severity` +- `http://hl7.org/fhir/ValueSet/condition-stage` +- `http://hl7.org/fhir/ValueSet/condition-stage-type` +- `http://hl7.org/fhir/ValueSet/condition-state` +- `http://hl7.org/fhir/ValueSet/condition-ver-status` +- `http://hl7.org/fhir/ValueSet/conditional-delete-status` +- `http://hl7.org/fhir/ValueSet/conditional-read-status` +- `http://hl7.org/fhir/ValueSet/conformance-expectation` +- `http://hl7.org/fhir/ValueSet/consent-action` +- `http://hl7.org/fhir/ValueSet/consent-category` +- `http://hl7.org/fhir/ValueSet/consent-content-class` +- `http://hl7.org/fhir/ValueSet/consent-content-code` +- `http://hl7.org/fhir/ValueSet/consent-data-meaning` +- `http://hl7.org/fhir/ValueSet/consent-performer` +- `http://hl7.org/fhir/ValueSet/consent-policy` +- `http://hl7.org/fhir/ValueSet/consent-provision-type` +- `http://hl7.org/fhir/ValueSet/consent-scope` +- `http://hl7.org/fhir/ValueSet/consent-state-codes` +- `http://hl7.org/fhir/ValueSet/consistency-type` +- `http://hl7.org/fhir/ValueSet/constraint-severity` +- `http://hl7.org/fhir/ValueSet/contact-point-system` +- `http://hl7.org/fhir/ValueSet/contact-point-use` +- `http://hl7.org/fhir/ValueSet/contactentity-type` +- `http://hl7.org/fhir/ValueSet/container-cap` +- `http://hl7.org/fhir/ValueSet/container-material` +- `http://hl7.org/fhir/ValueSet/contract-action` +- `http://hl7.org/fhir/ValueSet/contract-actionstatus` +- `http://hl7.org/fhir/ValueSet/contract-actorrole` +- `http://hl7.org/fhir/ValueSet/contract-assetcontext` +- `http://hl7.org/fhir/ValueSet/contract-assetscope` +- `http://hl7.org/fhir/ValueSet/contract-assetsubtype` +- `http://hl7.org/fhir/ValueSet/contract-assettype` +- `http://hl7.org/fhir/ValueSet/contract-content-derivative` +- `http://hl7.org/fhir/ValueSet/contract-data-meaning` +- `http://hl7.org/fhir/ValueSet/contract-decision-mode` +- `http://hl7.org/fhir/ValueSet/contract-definition-subtype` +- `http://hl7.org/fhir/ValueSet/contract-definition-type` +- `http://hl7.org/fhir/ValueSet/contract-expiration-type` +- `http://hl7.org/fhir/ValueSet/contract-legalstate` +- `http://hl7.org/fhir/ValueSet/contract-party-role` +- `http://hl7.org/fhir/ValueSet/contract-publicationstatus` +- `http://hl7.org/fhir/ValueSet/contract-scope` +- `http://hl7.org/fhir/ValueSet/contract-security-category` +- `http://hl7.org/fhir/ValueSet/contract-security-classification` +- `http://hl7.org/fhir/ValueSet/contract-security-control` +- `http://hl7.org/fhir/ValueSet/contract-signer-type` +- `http://hl7.org/fhir/ValueSet/contract-status` +- `http://hl7.org/fhir/ValueSet/contract-subtype` +- `http://hl7.org/fhir/ValueSet/contract-term-subtype` +- `http://hl7.org/fhir/ValueSet/contract-term-type` +- `http://hl7.org/fhir/ValueSet/contract-type` +- `http://hl7.org/fhir/ValueSet/contributor-type` +- `http://hl7.org/fhir/ValueSet/copy-number-event` +- `http://hl7.org/fhir/ValueSet/cosmic` +- `http://hl7.org/fhir/ValueSet/coverage-class` +- `http://hl7.org/fhir/ValueSet/coverage-copay-type` +- `http://hl7.org/fhir/ValueSet/coverage-financial-exception` +- `http://hl7.org/fhir/ValueSet/coverage-selfpay` +- `http://hl7.org/fhir/ValueSet/coverage-type` +- `http://hl7.org/fhir/ValueSet/coverageeligibilityresponse-ex-auth-support` +- `http://hl7.org/fhir/ValueSet/cpt-all` +- `http://hl7.org/fhir/ValueSet/currencies` +- `http://hl7.org/fhir/ValueSet/data-absent-reason` +- `http://hl7.org/fhir/ValueSet/data-types` +- `http://hl7.org/fhir/ValueSet/dataelement-sdcobjectclass` +- `http://hl7.org/fhir/ValueSet/dataelement-sdcobjectclassproperty` +- `http://hl7.org/fhir/ValueSet/days-of-week` +- `http://hl7.org/fhir/ValueSet/dbsnp` +- `http://hl7.org/fhir/ValueSet/defined-types` +- `http://hl7.org/fhir/ValueSet/definition-resource-types` +- `http://hl7.org/fhir/ValueSet/definition-status` +- `http://hl7.org/fhir/ValueSet/definition-topic` +- `http://hl7.org/fhir/ValueSet/definition-use` +- `http://hl7.org/fhir/ValueSet/designation-use` +- `http://hl7.org/fhir/ValueSet/detectedissue-category` +- `http://hl7.org/fhir/ValueSet/detectedissue-mitigation-action` +- `http://hl7.org/fhir/ValueSet/detectedissue-severity` +- `http://hl7.org/fhir/ValueSet/device-action` +- `http://hl7.org/fhir/ValueSet/device-component-property` +- `http://hl7.org/fhir/ValueSet/device-definition-status` +- `http://hl7.org/fhir/ValueSet/device-kind` +- `http://hl7.org/fhir/ValueSet/device-nametype` +- `http://hl7.org/fhir/ValueSet/device-safety` +- `http://hl7.org/fhir/ValueSet/device-statement-status` +- `http://hl7.org/fhir/ValueSet/device-status` +- `http://hl7.org/fhir/ValueSet/device-status-reason` +- `http://hl7.org/fhir/ValueSet/device-type` +- `http://hl7.org/fhir/ValueSet/devicemetric-type` +- `http://hl7.org/fhir/ValueSet/diagnosis-role` +- `http://hl7.org/fhir/ValueSet/diagnostic-based-on-snomed` +- `http://hl7.org/fhir/ValueSet/diagnostic-report-status` +- `http://hl7.org/fhir/ValueSet/diagnostic-service-sections` +- `http://hl7.org/fhir/ValueSet/dicm-405-mediatype` +- `http://hl7.org/fhir/ValueSet/diet-type` +- `http://hl7.org/fhir/ValueSet/discriminator-type` +- `http://hl7.org/fhir/ValueSet/distance-units` +- `http://hl7.org/fhir/ValueSet/doc-section-codes` +- `http://hl7.org/fhir/ValueSet/doc-typecodes` +- `http://hl7.org/fhir/ValueSet/document-classcodes` +- `http://hl7.org/fhir/ValueSet/document-mode` +- `http://hl7.org/fhir/ValueSet/document-reference-status` +- `http://hl7.org/fhir/ValueSet/document-relationship-type` +- `http://hl7.org/fhir/ValueSet/dose-rate-type` +- `http://hl7.org/fhir/ValueSet/duration-units` +- `http://hl7.org/fhir/ValueSet/effect-estimate-type` +- `http://hl7.org/fhir/ValueSet/eligibilityrequest-purpose` +- `http://hl7.org/fhir/ValueSet/eligibilityresponse-purpose` +- `http://hl7.org/fhir/ValueSet/encounter-admit-source` +- `http://hl7.org/fhir/ValueSet/encounter-diet` +- `http://hl7.org/fhir/ValueSet/encounter-discharge-disposition` +- `http://hl7.org/fhir/ValueSet/encounter-location-status` +- `http://hl7.org/fhir/ValueSet/encounter-participant-type` +- `http://hl7.org/fhir/ValueSet/encounter-reason` +- `http://hl7.org/fhir/ValueSet/encounter-special-arrangements` +- `http://hl7.org/fhir/ValueSet/encounter-special-courtesy` +- `http://hl7.org/fhir/ValueSet/encounter-status` +- `http://hl7.org/fhir/ValueSet/encounter-type` +- `http://hl7.org/fhir/ValueSet/endpoint-connection-type` +- `http://hl7.org/fhir/ValueSet/endpoint-payload-type` +- `http://hl7.org/fhir/ValueSet/endpoint-status` +- `http://hl7.org/fhir/ValueSet/ensembl` +- `http://hl7.org/fhir/ValueSet/enteral-route` +- `http://hl7.org/fhir/ValueSet/entformula-additive` +- `http://hl7.org/fhir/ValueSet/entformula-type` +- `http://hl7.org/fhir/ValueSet/episode-of-care-status` +- `http://hl7.org/fhir/ValueSet/episodeofcare-type` +- `http://hl7.org/fhir/ValueSet/event-capability-mode` +- `http://hl7.org/fhir/ValueSet/event-or-request-resource-types` +- `http://hl7.org/fhir/ValueSet/event-resource-types` +- `http://hl7.org/fhir/ValueSet/event-status` +- `http://hl7.org/fhir/ValueSet/event-timing` +- `http://hl7.org/fhir/ValueSet/evidence-quality` +- `http://hl7.org/fhir/ValueSet/evidence-variant-state` +- `http://hl7.org/fhir/ValueSet/ex-benefitcategory` +- `http://hl7.org/fhir/ValueSet/ex-diagnosis-on-admission` +- `http://hl7.org/fhir/ValueSet/ex-diagnosisrelatedgroup` +- `http://hl7.org/fhir/ValueSet/ex-diagnosistype` +- `http://hl7.org/fhir/ValueSet/ex-onsettype` +- `http://hl7.org/fhir/ValueSet/ex-payee-resource-type` +- `http://hl7.org/fhir/ValueSet/ex-paymenttype` +- `http://hl7.org/fhir/ValueSet/ex-procedure-type` +- `http://hl7.org/fhir/ValueSet/ex-program-code` +- `http://hl7.org/fhir/ValueSet/ex-revenue-center` +- `http://hl7.org/fhir/ValueSet/example-expansion` +- `http://hl7.org/fhir/ValueSet/example-extensional` +- `http://hl7.org/fhir/ValueSet/example-filter` +- `http://hl7.org/fhir/ValueSet/example-hierarchical` +- `http://hl7.org/fhir/ValueSet/example-intensional` +- `http://hl7.org/fhir/ValueSet/examplescenario-actor-type` +- `http://hl7.org/fhir/ValueSet/expansion-parameter-source` +- `http://hl7.org/fhir/ValueSet/expansion-processing-rule` +- `http://hl7.org/fhir/ValueSet/explanationofbenefit-status` +- `http://hl7.org/fhir/ValueSet/exposure-state` +- `http://hl7.org/fhir/ValueSet/expression-language` +- `http://hl7.org/fhir/ValueSet/extension-context-type` +- `http://hl7.org/fhir/ValueSet/feeding-device` +- `http://hl7.org/fhir/ValueSet/filter-operator` +- `http://hl7.org/fhir/ValueSet/financial-taskcode` +- `http://hl7.org/fhir/ValueSet/financial-taskinputtype` +- `http://hl7.org/fhir/ValueSet/flag-category` +- `http://hl7.org/fhir/ValueSet/flag-code` +- `http://hl7.org/fhir/ValueSet/flag-priority` +- `http://hl7.org/fhir/ValueSet/flag-status` +- `http://hl7.org/fhir/ValueSet/fm-conditions` +- `http://hl7.org/fhir/ValueSet/fm-itemtype` +- `http://hl7.org/fhir/ValueSet/fm-status` +- `http://hl7.org/fhir/ValueSet/focal-subject` +- `http://hl7.org/fhir/ValueSet/food-type` +- `http://hl7.org/fhir/ValueSet/formatcodes` +- `http://hl7.org/fhir/ValueSet/forms` +- `http://hl7.org/fhir/ValueSet/fundsreserve` +- `http://hl7.org/fhir/ValueSet/gender-identity` +- `http://hl7.org/fhir/ValueSet/genenames` +- `http://hl7.org/fhir/ValueSet/goal-acceptance-status` +- `http://hl7.org/fhir/ValueSet/goal-achievement` +- `http://hl7.org/fhir/ValueSet/goal-category` +- `http://hl7.org/fhir/ValueSet/goal-priority` +- `http://hl7.org/fhir/ValueSet/goal-relationship-type` +- `http://hl7.org/fhir/ValueSet/goal-start-event` +- `http://hl7.org/fhir/ValueSet/goal-status` +- `http://hl7.org/fhir/ValueSet/goal-status-reason` +- `http://hl7.org/fhir/ValueSet/graph-compartment-rule` +- `http://hl7.org/fhir/ValueSet/graph-compartment-use` +- `http://hl7.org/fhir/ValueSet/group-measure` +- `http://hl7.org/fhir/ValueSet/group-type` +- `http://hl7.org/fhir/ValueSet/guidance-response-status` +- `http://hl7.org/fhir/ValueSet/guide-page-generation` +- `http://hl7.org/fhir/ValueSet/guide-parameter-code` +- `http://hl7.org/fhir/ValueSet/handling-condition` +- `http://hl7.org/fhir/ValueSet/history-absent-reason` +- `http://hl7.org/fhir/ValueSet/history-status` +- `http://hl7.org/fhir/ValueSet/hl7-work-group` +- `http://hl7.org/fhir/ValueSet/http-operations` +- `http://hl7.org/fhir/ValueSet/http-verb` +- `http://hl7.org/fhir/ValueSet/icd-10` +- `http://hl7.org/fhir/ValueSet/icd-10-procedures` +- `http://hl7.org/fhir/ValueSet/identifier-type` +- `http://hl7.org/fhir/ValueSet/identifier-use` +- `http://hl7.org/fhir/ValueSet/identity-assuranceLevel` +- `http://hl7.org/fhir/ValueSet/imagingstudy-status` +- `http://hl7.org/fhir/ValueSet/immunization-evaluation-dose-status` +- `http://hl7.org/fhir/ValueSet/immunization-evaluation-dose-status-reason` +- `http://hl7.org/fhir/ValueSet/immunization-evaluation-status` +- `http://hl7.org/fhir/ValueSet/immunization-evaluation-target-disease` +- `http://hl7.org/fhir/ValueSet/immunization-function` +- `http://hl7.org/fhir/ValueSet/immunization-funding-source` +- `http://hl7.org/fhir/ValueSet/immunization-origin` +- `http://hl7.org/fhir/ValueSet/immunization-program-eligibility` +- `http://hl7.org/fhir/ValueSet/immunization-reason` +- `http://hl7.org/fhir/ValueSet/immunization-recommendation-date-criterion` +- `http://hl7.org/fhir/ValueSet/immunization-recommendation-reason` +- `http://hl7.org/fhir/ValueSet/immunization-recommendation-status` +- `http://hl7.org/fhir/ValueSet/immunization-recommendation-target-disease` +- `http://hl7.org/fhir/ValueSet/immunization-route` +- `http://hl7.org/fhir/ValueSet/immunization-site` +- `http://hl7.org/fhir/ValueSet/immunization-status` +- `http://hl7.org/fhir/ValueSet/immunization-status-reason` +- `http://hl7.org/fhir/ValueSet/immunization-subpotent-reason` +- `http://hl7.org/fhir/ValueSet/immunization-target-disease` +- `http://hl7.org/fhir/ValueSet/implantStatus` +- `http://hl7.org/fhir/ValueSet/inactive` +- `http://hl7.org/fhir/ValueSet/instance-availability` +- `http://hl7.org/fhir/ValueSet/insuranceplan-applicability` +- `http://hl7.org/fhir/ValueSet/insuranceplan-type` +- `http://hl7.org/fhir/ValueSet/intervention` +- `http://hl7.org/fhir/ValueSet/investigation-sets` +- `http://hl7.org/fhir/ValueSet/invoice-priceComponentType` +- `http://hl7.org/fhir/ValueSet/invoice-status` +- `http://hl7.org/fhir/ValueSet/iso3166-1-2` +- `http://hl7.org/fhir/ValueSet/iso3166-1-3` +- `http://hl7.org/fhir/ValueSet/iso3166-1-N` +- `http://hl7.org/fhir/ValueSet/issue-severity` +- `http://hl7.org/fhir/ValueSet/issue-type` +- `http://hl7.org/fhir/ValueSet/item-type` +- `http://hl7.org/fhir/ValueSet/jurisdiction` +- `http://hl7.org/fhir/ValueSet/knowledge-resource-types` +- `http://hl7.org/fhir/ValueSet/language-preference-type` +- `http://hl7.org/fhir/ValueSet/languages` +- `http://hl7.org/fhir/ValueSet/ldlcholesterol-codes` +- `http://hl7.org/fhir/ValueSet/library-type` +- `http://hl7.org/fhir/ValueSet/link-type` +- `http://hl7.org/fhir/ValueSet/linkage-type` +- `http://hl7.org/fhir/ValueSet/list-empty-reason` +- `http://hl7.org/fhir/ValueSet/list-example-codes` +- `http://hl7.org/fhir/ValueSet/list-item-flag` +- `http://hl7.org/fhir/ValueSet/list-mode` +- `http://hl7.org/fhir/ValueSet/list-order` +- `http://hl7.org/fhir/ValueSet/list-status` +- `http://hl7.org/fhir/ValueSet/location-mode` +- `http://hl7.org/fhir/ValueSet/location-physical-type` +- `http://hl7.org/fhir/ValueSet/location-status` +- `http://hl7.org/fhir/ValueSet/manifestation-or-symptom` +- `http://hl7.org/fhir/ValueSet/map-context-type` +- `http://hl7.org/fhir/ValueSet/map-group-type-mode` +- `http://hl7.org/fhir/ValueSet/map-input-mode` +- `http://hl7.org/fhir/ValueSet/map-model-mode` +- `http://hl7.org/fhir/ValueSet/map-source-list-mode` +- `http://hl7.org/fhir/ValueSet/map-target-list-mode` +- `http://hl7.org/fhir/ValueSet/map-transform` +- `http://hl7.org/fhir/ValueSet/marital-status` +- `http://hl7.org/fhir/ValueSet/match-grade` +- `http://hl7.org/fhir/ValueSet/measure-data-usage` +- `http://hl7.org/fhir/ValueSet/measure-improvement-notation` +- `http://hl7.org/fhir/ValueSet/measure-population` +- `http://hl7.org/fhir/ValueSet/measure-report-status` +- `http://hl7.org/fhir/ValueSet/measure-report-type` +- `http://hl7.org/fhir/ValueSet/measure-scoring` +- `http://hl7.org/fhir/ValueSet/measure-type` +- `http://hl7.org/fhir/ValueSet/med-admin-perform-function` +- `http://hl7.org/fhir/ValueSet/media-modality` +- `http://hl7.org/fhir/ValueSet/media-type` +- `http://hl7.org/fhir/ValueSet/media-view` +- `http://hl7.org/fhir/ValueSet/medication-admin-category` +- `http://hl7.org/fhir/ValueSet/medication-admin-status` +- `http://hl7.org/fhir/ValueSet/medication-as-needed-reason` +- `http://hl7.org/fhir/ValueSet/medication-codes` +- `http://hl7.org/fhir/ValueSet/medication-form-codes` +- `http://hl7.org/fhir/ValueSet/medication-statement-category` +- `http://hl7.org/fhir/ValueSet/medication-statement-status` +- `http://hl7.org/fhir/ValueSet/medication-status` +- `http://hl7.org/fhir/ValueSet/medicationdispense-category` +- `http://hl7.org/fhir/ValueSet/medicationdispense-performer-function` +- `http://hl7.org/fhir/ValueSet/medicationdispense-status` +- `http://hl7.org/fhir/ValueSet/medicationdispense-status-reason` +- `http://hl7.org/fhir/ValueSet/medicationknowledge-characteristic` +- `http://hl7.org/fhir/ValueSet/medicationknowledge-package-type` +- `http://hl7.org/fhir/ValueSet/medicationknowledge-status` +- `http://hl7.org/fhir/ValueSet/medicationrequest-category` +- `http://hl7.org/fhir/ValueSet/medicationrequest-course-of-therapy` +- `http://hl7.org/fhir/ValueSet/medicationrequest-intent` +- `http://hl7.org/fhir/ValueSet/medicationrequest-status` +- `http://hl7.org/fhir/ValueSet/medicationrequest-status-reason` +- `http://hl7.org/fhir/ValueSet/message-events` +- `http://hl7.org/fhir/ValueSet/message-reason-encounter` +- `http://hl7.org/fhir/ValueSet/message-significance-category` +- `http://hl7.org/fhir/ValueSet/message-transport` +- `http://hl7.org/fhir/ValueSet/messageheader-response-request` +- `http://hl7.org/fhir/ValueSet/metric-calibration-state` +- `http://hl7.org/fhir/ValueSet/metric-calibration-type` +- `http://hl7.org/fhir/ValueSet/metric-category` +- `http://hl7.org/fhir/ValueSet/metric-color` +- `http://hl7.org/fhir/ValueSet/metric-operational-status` +- `http://hl7.org/fhir/ValueSet/mimetypes` +- `http://hl7.org/fhir/ValueSet/missing-tooth-reason` +- `http://hl7.org/fhir/ValueSet/modified-foodtype` +- `http://hl7.org/fhir/ValueSet/name-assembly-order` +- `http://hl7.org/fhir/ValueSet/name-part-qualifier` +- `http://hl7.org/fhir/ValueSet/name-use` +- `http://hl7.org/fhir/ValueSet/name-v3-representation` +- `http://hl7.org/fhir/ValueSet/namingsystem-identifier-type` +- `http://hl7.org/fhir/ValueSet/namingsystem-type` +- `http://hl7.org/fhir/ValueSet/narrative-status` +- `http://hl7.org/fhir/ValueSet/network-type` +- `http://hl7.org/fhir/ValueSet/nhin-purposeofuse` +- `http://hl7.org/fhir/ValueSet/note-type` +- `http://hl7.org/fhir/ValueSet/nutrient-code` +- `http://hl7.org/fhir/ValueSet/object-lifecycle-events` +- `http://hl7.org/fhir/ValueSet/object-role` +- `http://hl7.org/fhir/ValueSet/observation-category` +- `http://hl7.org/fhir/ValueSet/observation-codes` +- `http://hl7.org/fhir/ValueSet/observation-interpretation` +- `http://hl7.org/fhir/ValueSet/observation-methods` +- `http://hl7.org/fhir/ValueSet/observation-range-category` +- `http://hl7.org/fhir/ValueSet/observation-statistics` +- `http://hl7.org/fhir/ValueSet/observation-status` +- `http://hl7.org/fhir/ValueSet/observation-vitalsignresult` +- `http://hl7.org/fhir/ValueSet/operation-kind` +- `http://hl7.org/fhir/ValueSet/operation-outcome` +- `http://hl7.org/fhir/ValueSet/operation-parameter-use` +- `http://hl7.org/fhir/ValueSet/oral-prosthodontic-material` +- `http://hl7.org/fhir/ValueSet/organization-role` +- `http://hl7.org/fhir/ValueSet/organization-type` +- `http://hl7.org/fhir/ValueSet/orientation-type` +- `http://hl7.org/fhir/ValueSet/parameter-group` +- `http://hl7.org/fhir/ValueSet/parent-relationship-codes` +- `http://hl7.org/fhir/ValueSet/participant-role` +- `http://hl7.org/fhir/ValueSet/participantrequired` +- `http://hl7.org/fhir/ValueSet/participation-role-type` +- `http://hl7.org/fhir/ValueSet/participationstatus` +- `http://hl7.org/fhir/ValueSet/patient-contactrelationship` +- `http://hl7.org/fhir/ValueSet/payeetype` +- `http://hl7.org/fhir/ValueSet/payment-adjustment-reason` +- `http://hl7.org/fhir/ValueSet/payment-status` +- `http://hl7.org/fhir/ValueSet/payment-type` +- `http://hl7.org/fhir/ValueSet/performer-function` +- `http://hl7.org/fhir/ValueSet/performer-role` +- `http://hl7.org/fhir/ValueSet/permitted-data-type` +- `http://hl7.org/fhir/ValueSet/plan-definition-type` +- `http://hl7.org/fhir/ValueSet/postal-address-use` +- `http://hl7.org/fhir/ValueSet/practitioner-role` +- `http://hl7.org/fhir/ValueSet/practitioner-specialty` +- `http://hl7.org/fhir/ValueSet/precision-estimate-type` +- `http://hl7.org/fhir/ValueSet/prepare-patient-prior-specimen-collection` +- `http://hl7.org/fhir/ValueSet/probability-distribution-type` +- `http://hl7.org/fhir/ValueSet/procedure-category` +- `http://hl7.org/fhir/ValueSet/procedure-code` +- `http://hl7.org/fhir/ValueSet/procedure-followup` +- `http://hl7.org/fhir/ValueSet/procedure-not-performed-reason` +- `http://hl7.org/fhir/ValueSet/procedure-outcome` +- `http://hl7.org/fhir/ValueSet/procedure-progress-status-codes` +- `http://hl7.org/fhir/ValueSet/procedure-reason` +- `http://hl7.org/fhir/ValueSet/process-priority` +- `http://hl7.org/fhir/ValueSet/product-category` +- `http://hl7.org/fhir/ValueSet/product-status` +- `http://hl7.org/fhir/ValueSet/product-storage-scale` +- `http://hl7.org/fhir/ValueSet/program` +- `http://hl7.org/fhir/ValueSet/property-representation` +- `http://hl7.org/fhir/ValueSet/provenance-activity-type` +- `http://hl7.org/fhir/ValueSet/provenance-agent-role` +- `http://hl7.org/fhir/ValueSet/provenance-agent-type` +- `http://hl7.org/fhir/ValueSet/provenance-entity-role` +- `http://hl7.org/fhir/ValueSet/provenance-history-agent-type` +- `http://hl7.org/fhir/ValueSet/provenance-history-record-activity` +- `http://hl7.org/fhir/ValueSet/provider-qualification` +- `http://hl7.org/fhir/ValueSet/provider-taxonomy` +- `http://hl7.org/fhir/ValueSet/publication-status` +- `http://hl7.org/fhir/ValueSet/quality-type` +- `http://hl7.org/fhir/ValueSet/quantity-comparator` +- `http://hl7.org/fhir/ValueSet/question-max-occurs` +- `http://hl7.org/fhir/ValueSet/questionnaire-answers` +- `http://hl7.org/fhir/ValueSet/questionnaire-answers-status` +- `http://hl7.org/fhir/ValueSet/questionnaire-category` +- `http://hl7.org/fhir/ValueSet/questionnaire-display-category` +- `http://hl7.org/fhir/ValueSet/questionnaire-enable-behavior` +- `http://hl7.org/fhir/ValueSet/questionnaire-enable-operator` +- `http://hl7.org/fhir/ValueSet/questionnaire-item-control` +- `http://hl7.org/fhir/ValueSet/questionnaire-questions` +- `http://hl7.org/fhir/ValueSet/questionnaire-usage-mode` +- `http://hl7.org/fhir/ValueSet/questionnaireresponse-mode` +- `http://hl7.org/fhir/ValueSet/reaction-event-certainty` +- `http://hl7.org/fhir/ValueSet/reaction-event-severity` +- `http://hl7.org/fhir/ValueSet/reason-medication-given-codes` +- `http://hl7.org/fhir/ValueSet/reason-medication-not-given-codes` +- `http://hl7.org/fhir/ValueSet/reason-medication-status-codes` +- `http://hl7.org/fhir/ValueSet/recommendation-strength` +- `http://hl7.org/fhir/ValueSet/ref-sequences` +- `http://hl7.org/fhir/ValueSet/reference-handling-policy` +- `http://hl7.org/fhir/ValueSet/reference-version-rules` +- `http://hl7.org/fhir/ValueSet/referencerange-appliesto` +- `http://hl7.org/fhir/ValueSet/referencerange-meaning` +- `http://hl7.org/fhir/ValueSet/rejection-criteria` +- `http://hl7.org/fhir/ValueSet/related-artifact-type` +- `http://hl7.org/fhir/ValueSet/related-claim-relationship` +- `http://hl7.org/fhir/ValueSet/relatedperson-relationshiptype` +- `http://hl7.org/fhir/ValueSet/relation-type` +- `http://hl7.org/fhir/ValueSet/relationship` +- `http://hl7.org/fhir/ValueSet/remittance-outcome` +- `http://hl7.org/fhir/ValueSet/report-action-result-codes` +- `http://hl7.org/fhir/ValueSet/report-codes` +- `http://hl7.org/fhir/ValueSet/report-participant-type` +- `http://hl7.org/fhir/ValueSet/report-result-codes` +- `http://hl7.org/fhir/ValueSet/report-status-codes` +- `http://hl7.org/fhir/ValueSet/repository-type` +- `http://hl7.org/fhir/ValueSet/request-intent` +- `http://hl7.org/fhir/ValueSet/request-priority` +- `http://hl7.org/fhir/ValueSet/request-resource-types` +- `http://hl7.org/fhir/ValueSet/request-status` +- `http://hl7.org/fhir/ValueSet/research-element-type` +- `http://hl7.org/fhir/ValueSet/research-study-objective-type` +- `http://hl7.org/fhir/ValueSet/research-study-phase` +- `http://hl7.org/fhir/ValueSet/research-study-prim-purp-type` +- `http://hl7.org/fhir/ValueSet/research-study-reason-stopped` +- `http://hl7.org/fhir/ValueSet/research-study-status` +- `http://hl7.org/fhir/ValueSet/research-subject-status` +- `http://hl7.org/fhir/ValueSet/resource-aggregation-mode` +- `http://hl7.org/fhir/ValueSet/resource-security-category` +- `http://hl7.org/fhir/ValueSet/resource-slicing-rules` +- `http://hl7.org/fhir/ValueSet/resource-status` +- `http://hl7.org/fhir/ValueSet/resource-type-link` +- `http://hl7.org/fhir/ValueSet/resource-types` +- `http://hl7.org/fhir/ValueSet/resource-validation-mode` +- `http://hl7.org/fhir/ValueSet/response-code` +- `http://hl7.org/fhir/ValueSet/restful-capability-mode` +- `http://hl7.org/fhir/ValueSet/restful-security-service` +- `http://hl7.org/fhir/ValueSet/risk-estimate-type` +- `http://hl7.org/fhir/ValueSet/risk-probability` +- `http://hl7.org/fhir/ValueSet/route-codes` +- `http://hl7.org/fhir/ValueSet/search-comparator` +- `http://hl7.org/fhir/ValueSet/search-entry-mode` +- `http://hl7.org/fhir/ValueSet/search-modifier-code` +- `http://hl7.org/fhir/ValueSet/search-param-type` +- `http://hl7.org/fhir/ValueSet/search-xpath-usage` +- `http://hl7.org/fhir/ValueSet/secondary-finding` +- `http://hl7.org/fhir/ValueSet/security-labels` +- `http://hl7.org/fhir/ValueSet/security-role-type` +- `http://hl7.org/fhir/ValueSet/sequence-quality-method` +- `http://hl7.org/fhir/ValueSet/sequence-quality-standardSequence` +- `http://hl7.org/fhir/ValueSet/sequence-referenceSeq` +- `http://hl7.org/fhir/ValueSet/sequence-species` +- `http://hl7.org/fhir/ValueSet/sequence-type` +- `http://hl7.org/fhir/ValueSet/sequenceontology` +- `http://hl7.org/fhir/ValueSet/series-performer-function` +- `http://hl7.org/fhir/ValueSet/service-category` +- `http://hl7.org/fhir/ValueSet/service-modifiers` +- `http://hl7.org/fhir/ValueSet/service-pharmacy` +- `http://hl7.org/fhir/ValueSet/service-place` +- `http://hl7.org/fhir/ValueSet/service-product` +- `http://hl7.org/fhir/ValueSet/service-provision-conditions` +- `http://hl7.org/fhir/ValueSet/service-referral-method` +- `http://hl7.org/fhir/ValueSet/service-type` +- `http://hl7.org/fhir/ValueSet/service-uscls` +- `http://hl7.org/fhir/ValueSet/servicerequest-category` +- `http://hl7.org/fhir/ValueSet/servicerequest-orderdetail` +- `http://hl7.org/fhir/ValueSet/sibling-relationship-codes` +- `http://hl7.org/fhir/ValueSet/signature-type` +- `http://hl7.org/fhir/ValueSet/slotstatus` +- `http://hl7.org/fhir/ValueSet/smart-capabilities` +- `http://hl7.org/fhir/ValueSet/sort-direction` +- `http://hl7.org/fhir/ValueSet/spdx-license` +- `http://hl7.org/fhir/ValueSet/special-values` +- `http://hl7.org/fhir/ValueSet/specimen-collection` +- `http://hl7.org/fhir/ValueSet/specimen-collection-method` +- `http://hl7.org/fhir/ValueSet/specimen-collection-priority` +- `http://hl7.org/fhir/ValueSet/specimen-contained-preference` +- `http://hl7.org/fhir/ValueSet/specimen-container-type` +- `http://hl7.org/fhir/ValueSet/specimen-processing-procedure` +- `http://hl7.org/fhir/ValueSet/specimen-status` +- `http://hl7.org/fhir/ValueSet/standards-status` +- `http://hl7.org/fhir/ValueSet/strand-type` +- `http://hl7.org/fhir/ValueSet/structure-definition-kind` +- `http://hl7.org/fhir/ValueSet/study-type` +- `http://hl7.org/fhir/ValueSet/subject-type` +- `http://hl7.org/fhir/ValueSet/subscriber-relationship` +- `http://hl7.org/fhir/ValueSet/subscription-channel-type` +- `http://hl7.org/fhir/ValueSet/subscription-status` +- `http://hl7.org/fhir/ValueSet/subscription-tag` +- `http://hl7.org/fhir/ValueSet/substance-category` +- `http://hl7.org/fhir/ValueSet/substance-code` +- `http://hl7.org/fhir/ValueSet/substance-status` +- `http://hl7.org/fhir/ValueSet/supplement-type` +- `http://hl7.org/fhir/ValueSet/supply-item` +- `http://hl7.org/fhir/ValueSet/supplydelivery-status` +- `http://hl7.org/fhir/ValueSet/supplydelivery-type` +- `http://hl7.org/fhir/ValueSet/supplyrequest-kind` +- `http://hl7.org/fhir/ValueSet/supplyrequest-reason` +- `http://hl7.org/fhir/ValueSet/supplyrequest-status` +- `http://hl7.org/fhir/ValueSet/surface` +- `http://hl7.org/fhir/ValueSet/synthesis-type` +- `http://hl7.org/fhir/ValueSet/system-restful-interaction` +- `http://hl7.org/fhir/ValueSet/task-code` +- `http://hl7.org/fhir/ValueSet/task-intent` +- `http://hl7.org/fhir/ValueSet/task-status` +- `http://hl7.org/fhir/ValueSet/teeth` +- `http://hl7.org/fhir/ValueSet/template-status-code` +- `http://hl7.org/fhir/ValueSet/testscript-operation-codes` +- `http://hl7.org/fhir/ValueSet/testscript-profile-destination-types` +- `http://hl7.org/fhir/ValueSet/testscript-profile-origin-types` +- `http://hl7.org/fhir/ValueSet/texture-code` +- `http://hl7.org/fhir/ValueSet/timezones` +- `http://hl7.org/fhir/ValueSet/timing-abbreviation` +- `http://hl7.org/fhir/ValueSet/tooth` +- `http://hl7.org/fhir/ValueSet/transaction-mode` +- `http://hl7.org/fhir/ValueSet/trigger-type` +- `http://hl7.org/fhir/ValueSet/type-derivation-rule` +- `http://hl7.org/fhir/ValueSet/type-restful-interaction` +- `http://hl7.org/fhir/ValueSet/ucum-bodylength` +- `http://hl7.org/fhir/ValueSet/ucum-bodytemp` +- `http://hl7.org/fhir/ValueSet/ucum-bodyweight` +- `http://hl7.org/fhir/ValueSet/ucum-common` +- `http://hl7.org/fhir/ValueSet/ucum-units` +- `http://hl7.org/fhir/ValueSet/ucum-vitals-common` +- `http://hl7.org/fhir/ValueSet/udi` +- `http://hl7.org/fhir/ValueSet/udi-entry-type` +- `http://hl7.org/fhir/ValueSet/units-of-time` +- `http://hl7.org/fhir/ValueSet/unknown-content-code` +- `http://hl7.org/fhir/ValueSet/usage-context-type` +- `http://hl7.org/fhir/ValueSet/use-context` +- `http://hl7.org/fhir/ValueSet/vaccine-code` +- `http://hl7.org/fhir/ValueSet/variable-type` +- `http://hl7.org/fhir/ValueSet/variant-state` +- `http://hl7.org/fhir/ValueSet/variants` +- `http://hl7.org/fhir/ValueSet/verificationresult-can-push-updates` +- `http://hl7.org/fhir/ValueSet/verificationresult-communication-method` +- `http://hl7.org/fhir/ValueSet/verificationresult-failure-action` +- `http://hl7.org/fhir/ValueSet/verificationresult-need` +- `http://hl7.org/fhir/ValueSet/verificationresult-primary-source-type` +- `http://hl7.org/fhir/ValueSet/verificationresult-push-type-available` +- `http://hl7.org/fhir/ValueSet/verificationresult-status` +- `http://hl7.org/fhir/ValueSet/verificationresult-validation-process` +- `http://hl7.org/fhir/ValueSet/verificationresult-validation-status` +- `http://hl7.org/fhir/ValueSet/verificationresult-validation-type` +- `http://hl7.org/fhir/ValueSet/versioning-policy` +- `http://hl7.org/fhir/ValueSet/vision-base-codes` +- `http://hl7.org/fhir/ValueSet/vision-eye-codes` +- `http://hl7.org/fhir/ValueSet/vision-product` +- `http://hl7.org/fhir/ValueSet/written-language` +- `http://hl7.org/fhir/ValueSet/yesnodontknow` +- `http://terminology.hl7.org/ValueSet/v2-0001` +- `http://terminology.hl7.org/ValueSet/v2-0002` +- `http://terminology.hl7.org/ValueSet/v2-0003` +- `http://terminology.hl7.org/ValueSet/v2-0004` +- `http://terminology.hl7.org/ValueSet/v2-0005` +- `http://terminology.hl7.org/ValueSet/v2-0007` +- `http://terminology.hl7.org/ValueSet/v2-0008` +- `http://terminology.hl7.org/ValueSet/v2-0009` +- `http://terminology.hl7.org/ValueSet/v2-0012` +- `http://terminology.hl7.org/ValueSet/v2-0017` +- `http://terminology.hl7.org/ValueSet/v2-0023` +- `http://terminology.hl7.org/ValueSet/v2-0027` +- `http://terminology.hl7.org/ValueSet/v2-0033` +- `http://terminology.hl7.org/ValueSet/v2-0034` +- `http://terminology.hl7.org/ValueSet/v2-0038` +- `http://terminology.hl7.org/ValueSet/v2-0043` +- `http://terminology.hl7.org/ValueSet/v2-0048` +- `http://terminology.hl7.org/ValueSet/v2-0052` +- `http://terminology.hl7.org/ValueSet/v2-0061` +- `http://terminology.hl7.org/ValueSet/v2-0062` +- `http://terminology.hl7.org/ValueSet/v2-0063` +- `http://terminology.hl7.org/ValueSet/v2-0065` +- `http://terminology.hl7.org/ValueSet/v2-0066` +- `http://terminology.hl7.org/ValueSet/v2-0069` +- `http://terminology.hl7.org/ValueSet/v2-0070` +- `http://terminology.hl7.org/ValueSet/v2-0074` +- `http://terminology.hl7.org/ValueSet/v2-0076` +- `http://terminology.hl7.org/ValueSet/v2-0078` +- `http://terminology.hl7.org/ValueSet/v2-0080` +- `http://terminology.hl7.org/ValueSet/v2-0083` +- `http://terminology.hl7.org/ValueSet/v2-0085` +- `http://terminology.hl7.org/ValueSet/v2-0091` +- `http://terminology.hl7.org/ValueSet/v2-0092` +- `http://terminology.hl7.org/ValueSet/v2-0098` +- `http://terminology.hl7.org/ValueSet/v2-0100` +- `http://terminology.hl7.org/ValueSet/v2-0102` +- `http://terminology.hl7.org/ValueSet/v2-0103` +- `http://terminology.hl7.org/ValueSet/v2-0104` +- `http://terminology.hl7.org/ValueSet/v2-0105` +- `http://terminology.hl7.org/ValueSet/v2-0106` +- `http://terminology.hl7.org/ValueSet/v2-0107` +- `http://terminology.hl7.org/ValueSet/v2-0108` +- `http://terminology.hl7.org/ValueSet/v2-0109` +- `http://terminology.hl7.org/ValueSet/v2-0116` +- `http://terminology.hl7.org/ValueSet/v2-0119` +- `http://terminology.hl7.org/ValueSet/v2-0121` +- `http://terminology.hl7.org/ValueSet/v2-0122` +- `http://terminology.hl7.org/ValueSet/v2-0123` +- `http://terminology.hl7.org/ValueSet/v2-0124` +- `http://terminology.hl7.org/ValueSet/v2-0125` +- `http://terminology.hl7.org/ValueSet/v2-0126` +- `http://terminology.hl7.org/ValueSet/v2-0127` +- `http://terminology.hl7.org/ValueSet/v2-0128` +- `http://terminology.hl7.org/ValueSet/v2-0130` +- `http://terminology.hl7.org/ValueSet/v2-0131` +- `http://terminology.hl7.org/ValueSet/v2-0133` +- `http://terminology.hl7.org/ValueSet/v2-0135` +- `http://terminology.hl7.org/ValueSet/v2-0136` +- `http://terminology.hl7.org/ValueSet/v2-0137` +- `http://terminology.hl7.org/ValueSet/v2-0140` +- `http://terminology.hl7.org/ValueSet/v2-0141` +- `http://terminology.hl7.org/ValueSet/v2-0142` +- `http://terminology.hl7.org/ValueSet/v2-0144` +- `http://terminology.hl7.org/ValueSet/v2-0145` +- `http://terminology.hl7.org/ValueSet/v2-0146` +- `http://terminology.hl7.org/ValueSet/v2-0147` +- `http://terminology.hl7.org/ValueSet/v2-0148` +- `http://terminology.hl7.org/ValueSet/v2-0149` +- `http://terminology.hl7.org/ValueSet/v2-0150` +- `http://terminology.hl7.org/ValueSet/v2-0153` +- `http://terminology.hl7.org/ValueSet/v2-0155` +- `http://terminology.hl7.org/ValueSet/v2-0156` +- `http://terminology.hl7.org/ValueSet/v2-0157` +- `http://terminology.hl7.org/ValueSet/v2-0158` +- `http://terminology.hl7.org/ValueSet/v2-0159` +- `http://terminology.hl7.org/ValueSet/v2-0160` +- `http://terminology.hl7.org/ValueSet/v2-0161` +- `http://terminology.hl7.org/ValueSet/v2-0162` +- `http://terminology.hl7.org/ValueSet/v2-0163` +- `http://terminology.hl7.org/ValueSet/v2-0164` +- `http://terminology.hl7.org/ValueSet/v2-0165` +- `http://terminology.hl7.org/ValueSet/v2-0166` +- `http://terminology.hl7.org/ValueSet/v2-0167` +- `http://terminology.hl7.org/ValueSet/v2-0168` +- `http://terminology.hl7.org/ValueSet/v2-0169` +- `http://terminology.hl7.org/ValueSet/v2-0170` +- `http://terminology.hl7.org/ValueSet/v2-0173` +- `http://terminology.hl7.org/ValueSet/v2-0174` +- `http://terminology.hl7.org/ValueSet/v2-0175` +- `http://terminology.hl7.org/ValueSet/v2-0177` +- `http://terminology.hl7.org/ValueSet/v2-0178` +- `http://terminology.hl7.org/ValueSet/v2-0179` +- `http://terminology.hl7.org/ValueSet/v2-0180` +- `http://terminology.hl7.org/ValueSet/v2-0181` +- `http://terminology.hl7.org/ValueSet/v2-0183` +- `http://terminology.hl7.org/ValueSet/v2-0185` +- `http://terminology.hl7.org/ValueSet/v2-0187` +- `http://terminology.hl7.org/ValueSet/v2-0189` +- `http://terminology.hl7.org/ValueSet/v2-0190` +- `http://terminology.hl7.org/ValueSet/v2-0191` +- `http://terminology.hl7.org/ValueSet/v2-0193` +- `http://terminology.hl7.org/ValueSet/v2-0200` +- `http://terminology.hl7.org/ValueSet/v2-0201` +- `http://terminology.hl7.org/ValueSet/v2-0202` +- `http://terminology.hl7.org/ValueSet/v2-0203` +- `http://terminology.hl7.org/ValueSet/v2-0204` +- `http://terminology.hl7.org/ValueSet/v2-0205` +- `http://terminology.hl7.org/ValueSet/v2-0206` +- `http://terminology.hl7.org/ValueSet/v2-0207` +- `http://terminology.hl7.org/ValueSet/v2-0208` +- `http://terminology.hl7.org/ValueSet/v2-0209` +- `http://terminology.hl7.org/ValueSet/v2-0210` +- `http://terminology.hl7.org/ValueSet/v2-0211` +- `http://terminology.hl7.org/ValueSet/v2-0213` +- `http://terminology.hl7.org/ValueSet/v2-0214` +- `http://terminology.hl7.org/ValueSet/v2-0215` +- `http://terminology.hl7.org/ValueSet/v2-0216` +- `http://terminology.hl7.org/ValueSet/v2-0217` +- `http://terminology.hl7.org/ValueSet/v2-0220` +- `http://terminology.hl7.org/ValueSet/v2-0223` +- `http://terminology.hl7.org/ValueSet/v2-0224` +- `http://terminology.hl7.org/ValueSet/v2-0225` +- `http://terminology.hl7.org/ValueSet/v2-0227` +- `http://terminology.hl7.org/ValueSet/v2-0228` +- `http://terminology.hl7.org/ValueSet/v2-0229` +- `http://terminology.hl7.org/ValueSet/v2-0230` +- `http://terminology.hl7.org/ValueSet/v2-0231` +- `http://terminology.hl7.org/ValueSet/v2-0232` +- `http://terminology.hl7.org/ValueSet/v2-0234` +- `http://terminology.hl7.org/ValueSet/v2-0235` +- `http://terminology.hl7.org/ValueSet/v2-0236` +- `http://terminology.hl7.org/ValueSet/v2-0237` +- `http://terminology.hl7.org/ValueSet/v2-0238` +- `http://terminology.hl7.org/ValueSet/v2-0239` +- `http://terminology.hl7.org/ValueSet/v2-0240` +- `http://terminology.hl7.org/ValueSet/v2-0241` +- `http://terminology.hl7.org/ValueSet/v2-0242` +- `http://terminology.hl7.org/ValueSet/v2-0243` +- `http://terminology.hl7.org/ValueSet/v2-0247` +- `http://terminology.hl7.org/ValueSet/v2-0248` +- `http://terminology.hl7.org/ValueSet/v2-0250` +- `http://terminology.hl7.org/ValueSet/v2-0251` +- `http://terminology.hl7.org/ValueSet/v2-0252` +- `http://terminology.hl7.org/ValueSet/v2-0253` +- `http://terminology.hl7.org/ValueSet/v2-0254` +- `http://terminology.hl7.org/ValueSet/v2-0255` +- `http://terminology.hl7.org/ValueSet/v2-0256` +- `http://terminology.hl7.org/ValueSet/v2-0257` +- `http://terminology.hl7.org/ValueSet/v2-0258` +- `http://terminology.hl7.org/ValueSet/v2-0259` +- `http://terminology.hl7.org/ValueSet/v2-0260` +- `http://terminology.hl7.org/ValueSet/v2-0261` +- `http://terminology.hl7.org/ValueSet/v2-0262` +- `http://terminology.hl7.org/ValueSet/v2-0263` +- `http://terminology.hl7.org/ValueSet/v2-0265` +- `http://terminology.hl7.org/ValueSet/v2-0267` +- `http://terminology.hl7.org/ValueSet/v2-0268` +- `http://terminology.hl7.org/ValueSet/v2-0269` +- `http://terminology.hl7.org/ValueSet/v2-0270` +- `http://terminology.hl7.org/ValueSet/v2-0271` +- `http://terminology.hl7.org/ValueSet/v2-0272` +- `http://terminology.hl7.org/ValueSet/v2-0273` +- `http://terminology.hl7.org/ValueSet/v2-0275` +- `http://terminology.hl7.org/ValueSet/v2-0276` +- `http://terminology.hl7.org/ValueSet/v2-0277` +- `http://terminology.hl7.org/ValueSet/v2-0278` +- `http://terminology.hl7.org/ValueSet/v2-0279` +- `http://terminology.hl7.org/ValueSet/v2-0280` +- `http://terminology.hl7.org/ValueSet/v2-0281` +- `http://terminology.hl7.org/ValueSet/v2-0282` +- `http://terminology.hl7.org/ValueSet/v2-0283` +- `http://terminology.hl7.org/ValueSet/v2-0284` +- `http://terminology.hl7.org/ValueSet/v2-0286` +- `http://terminology.hl7.org/ValueSet/v2-0287` +- `http://terminology.hl7.org/ValueSet/v2-0290` +- `http://terminology.hl7.org/ValueSet/v2-0291` +- `http://terminology.hl7.org/ValueSet/v2-0292` +- `http://terminology.hl7.org/ValueSet/v2-0294` +- `http://terminology.hl7.org/ValueSet/v2-0298` +- `http://terminology.hl7.org/ValueSet/v2-0299` +- `http://terminology.hl7.org/ValueSet/v2-0301` +- `http://terminology.hl7.org/ValueSet/v2-0305` +- `http://terminology.hl7.org/ValueSet/v2-0309` +- `http://terminology.hl7.org/ValueSet/v2-0311` +- `http://terminology.hl7.org/ValueSet/v2-0315` +- `http://terminology.hl7.org/ValueSet/v2-0316` +- `http://terminology.hl7.org/ValueSet/v2-0317` +- `http://terminology.hl7.org/ValueSet/v2-0321` +- `http://terminology.hl7.org/ValueSet/v2-0322` +- `http://terminology.hl7.org/ValueSet/v2-0323` +- `http://terminology.hl7.org/ValueSet/v2-0324` +- `http://terminology.hl7.org/ValueSet/v2-0325` +- `http://terminology.hl7.org/ValueSet/v2-0326` +- `http://terminology.hl7.org/ValueSet/v2-0329` +- `http://terminology.hl7.org/ValueSet/v2-0330` +- `http://terminology.hl7.org/ValueSet/v2-0331` +- `http://terminology.hl7.org/ValueSet/v2-0332` +- `http://terminology.hl7.org/ValueSet/v2-0334` +- `http://terminology.hl7.org/ValueSet/v2-0335` +- `http://terminology.hl7.org/ValueSet/v2-0336` +- `http://terminology.hl7.org/ValueSet/v2-0337` +- `http://terminology.hl7.org/ValueSet/v2-0338` +- `http://terminology.hl7.org/ValueSet/v2-0339` +- `http://terminology.hl7.org/ValueSet/v2-0344` +- `http://terminology.hl7.org/ValueSet/v2-0350` +- `http://terminology.hl7.org/ValueSet/v2-0351` +- `http://terminology.hl7.org/ValueSet/v2-0353` +- `http://terminology.hl7.org/ValueSet/v2-0354` +- `http://terminology.hl7.org/ValueSet/v2-0355` +- `http://terminology.hl7.org/ValueSet/v2-0356` +- `http://terminology.hl7.org/ValueSet/v2-0357` +- `http://terminology.hl7.org/ValueSet/v2-0359` +- `http://terminology.hl7.org/ValueSet/v2-0363` +- `http://terminology.hl7.org/ValueSet/v2-0364` +- `http://terminology.hl7.org/ValueSet/v2-0365` +- `http://terminology.hl7.org/ValueSet/v2-0366` +- `http://terminology.hl7.org/ValueSet/v2-0367` +- `http://terminology.hl7.org/ValueSet/v2-0368` +- `http://terminology.hl7.org/ValueSet/v2-0369` +- `http://terminology.hl7.org/ValueSet/v2-0370` +- `http://terminology.hl7.org/ValueSet/v2-0371` +- `http://terminology.hl7.org/ValueSet/v2-0372` +- `http://terminology.hl7.org/ValueSet/v2-0373` +- `http://terminology.hl7.org/ValueSet/v2-0374` +- `http://terminology.hl7.org/ValueSet/v2-0375` +- `http://terminology.hl7.org/ValueSet/v2-0376` +- `http://terminology.hl7.org/ValueSet/v2-0377` +- `http://terminology.hl7.org/ValueSet/v2-0383` +- `http://terminology.hl7.org/ValueSet/v2-0384` +- `http://terminology.hl7.org/ValueSet/v2-0387` +- `http://terminology.hl7.org/ValueSet/v2-0388` +- `http://terminology.hl7.org/ValueSet/v2-0389` +- `http://terminology.hl7.org/ValueSet/v2-0392` +- `http://terminology.hl7.org/ValueSet/v2-0393` +- `http://terminology.hl7.org/ValueSet/v2-0394` +- `http://terminology.hl7.org/ValueSet/v2-0395` +- `http://terminology.hl7.org/ValueSet/v2-0396` +- `http://terminology.hl7.org/ValueSet/v2-0397` +- `http://terminology.hl7.org/ValueSet/v2-0398` +- `http://terminology.hl7.org/ValueSet/v2-0401` +- `http://terminology.hl7.org/ValueSet/v2-0402` +- `http://terminology.hl7.org/ValueSet/v2-0403` +- `http://terminology.hl7.org/ValueSet/v2-0404` +- `http://terminology.hl7.org/ValueSet/v2-0406` +- `http://terminology.hl7.org/ValueSet/v2-0409` +- `http://terminology.hl7.org/ValueSet/v2-0411` +- `http://terminology.hl7.org/ValueSet/v2-0415` +- `http://terminology.hl7.org/ValueSet/v2-0416` +- `http://terminology.hl7.org/ValueSet/v2-0417` +- `http://terminology.hl7.org/ValueSet/v2-0418` +- `http://terminology.hl7.org/ValueSet/v2-0421` +- `http://terminology.hl7.org/ValueSet/v2-0422` +- `http://terminology.hl7.org/ValueSet/v2-0423` +- `http://terminology.hl7.org/ValueSet/v2-0424` +- `http://terminology.hl7.org/ValueSet/v2-0425` +- `http://terminology.hl7.org/ValueSet/v2-0426` +- `http://terminology.hl7.org/ValueSet/v2-0427` +- `http://terminology.hl7.org/ValueSet/v2-0428` +- `http://terminology.hl7.org/ValueSet/v2-0429` +- `http://terminology.hl7.org/ValueSet/v2-0430` +- `http://terminology.hl7.org/ValueSet/v2-0431` +- `http://terminology.hl7.org/ValueSet/v2-0432` +- `http://terminology.hl7.org/ValueSet/v2-0433` +- `http://terminology.hl7.org/ValueSet/v2-0434` +- `http://terminology.hl7.org/ValueSet/v2-0435` +- `http://terminology.hl7.org/ValueSet/v2-0436` +- `http://terminology.hl7.org/ValueSet/v2-0437` +- `http://terminology.hl7.org/ValueSet/v2-0438` +- `http://terminology.hl7.org/ValueSet/v2-0440` +- `http://terminology.hl7.org/ValueSet/v2-0441` +- `http://terminology.hl7.org/ValueSet/v2-0442` +- `http://terminology.hl7.org/ValueSet/v2-0443` +- `http://terminology.hl7.org/ValueSet/v2-0444` +- `http://terminology.hl7.org/ValueSet/v2-0445` +- `http://terminology.hl7.org/ValueSet/v2-0450` +- `http://terminology.hl7.org/ValueSet/v2-0455` +- `http://terminology.hl7.org/ValueSet/v2-0456` +- `http://terminology.hl7.org/ValueSet/v2-0457` +- `http://terminology.hl7.org/ValueSet/v2-0459` +- `http://terminology.hl7.org/ValueSet/v2-0460` +- `http://terminology.hl7.org/ValueSet/v2-0465` +- `http://terminology.hl7.org/ValueSet/v2-0466` +- `http://terminology.hl7.org/ValueSet/v2-0468` +- `http://terminology.hl7.org/ValueSet/v2-0469` +- `http://terminology.hl7.org/ValueSet/v2-0470` +- `http://terminology.hl7.org/ValueSet/v2-0472` +- `http://terminology.hl7.org/ValueSet/v2-0473` +- `http://terminology.hl7.org/ValueSet/v2-0474` +- `http://terminology.hl7.org/ValueSet/v2-0475` +- `http://terminology.hl7.org/ValueSet/v2-0477` +- `http://terminology.hl7.org/ValueSet/v2-0478` +- `http://terminology.hl7.org/ValueSet/v2-0480` +- `http://terminology.hl7.org/ValueSet/v2-0482` +- `http://terminology.hl7.org/ValueSet/v2-0483` +- `http://terminology.hl7.org/ValueSet/v2-0484` +- `http://terminology.hl7.org/ValueSet/v2-0485` +- `http://terminology.hl7.org/ValueSet/v2-0487` +- `http://terminology.hl7.org/ValueSet/v2-0488` +- `http://terminology.hl7.org/ValueSet/v2-0489` +- `http://terminology.hl7.org/ValueSet/v2-0490` +- `http://terminology.hl7.org/ValueSet/v2-0491` +- `http://terminology.hl7.org/ValueSet/v2-0492` +- `http://terminology.hl7.org/ValueSet/v2-0493` +- `http://terminology.hl7.org/ValueSet/v2-0494` +- `http://terminology.hl7.org/ValueSet/v2-0495` +- `http://terminology.hl7.org/ValueSet/v2-0496` +- `http://terminology.hl7.org/ValueSet/v2-0497` +- `http://terminology.hl7.org/ValueSet/v2-0498` +- `http://terminology.hl7.org/ValueSet/v2-0499` +- `http://terminology.hl7.org/ValueSet/v2-0500` +- `http://terminology.hl7.org/ValueSet/v2-0501` +- `http://terminology.hl7.org/ValueSet/v2-0502` +- `http://terminology.hl7.org/ValueSet/v2-0503` +- `http://terminology.hl7.org/ValueSet/v2-0504` +- `http://terminology.hl7.org/ValueSet/v2-0505` +- `http://terminology.hl7.org/ValueSet/v2-0506` +- `http://terminology.hl7.org/ValueSet/v2-0507` +- `http://terminology.hl7.org/ValueSet/v2-0508` +- `http://terminology.hl7.org/ValueSet/v2-0510` +- `http://terminology.hl7.org/ValueSet/v2-0511` +- `http://terminology.hl7.org/ValueSet/v2-0513` +- `http://terminology.hl7.org/ValueSet/v2-0514` +- `http://terminology.hl7.org/ValueSet/v2-0516` +- `http://terminology.hl7.org/ValueSet/v2-0517` +- `http://terminology.hl7.org/ValueSet/v2-0518` +- `http://terminology.hl7.org/ValueSet/v2-0520` +- `http://terminology.hl7.org/ValueSet/v2-0523` +- `http://terminology.hl7.org/ValueSet/v2-0524` +- `http://terminology.hl7.org/ValueSet/v2-0527` +- `http://terminology.hl7.org/ValueSet/v2-0528` +- `http://terminology.hl7.org/ValueSet/v2-0529` +- `http://terminology.hl7.org/ValueSet/v2-0530` +- `http://terminology.hl7.org/ValueSet/v2-0532` +- `http://terminology.hl7.org/ValueSet/v2-0534` +- `http://terminology.hl7.org/ValueSet/v2-0535` +- `http://terminology.hl7.org/ValueSet/v2-0536` +- `http://terminology.hl7.org/ValueSet/v2-0538` +- `http://terminology.hl7.org/ValueSet/v2-0540` +- `http://terminology.hl7.org/ValueSet/v2-0544` +- `http://terminology.hl7.org/ValueSet/v2-0547` +- `http://terminology.hl7.org/ValueSet/v2-0548` +- `http://terminology.hl7.org/ValueSet/v2-0550` +- `http://terminology.hl7.org/ValueSet/v2-0553` +- `http://terminology.hl7.org/ValueSet/v2-0554` +- `http://terminology.hl7.org/ValueSet/v2-0555` +- `http://terminology.hl7.org/ValueSet/v2-0556` +- `http://terminology.hl7.org/ValueSet/v2-0557` +- `http://terminology.hl7.org/ValueSet/v2-0558` +- `http://terminology.hl7.org/ValueSet/v2-0559` +- `http://terminology.hl7.org/ValueSet/v2-0561` +- `http://terminology.hl7.org/ValueSet/v2-0562` +- `http://terminology.hl7.org/ValueSet/v2-0564` +- `http://terminology.hl7.org/ValueSet/v2-0565` +- `http://terminology.hl7.org/ValueSet/v2-0566` +- `http://terminology.hl7.org/ValueSet/v2-0569` +- `http://terminology.hl7.org/ValueSet/v2-0570` +- `http://terminology.hl7.org/ValueSet/v2-0571` +- `http://terminology.hl7.org/ValueSet/v2-0572` +- `http://terminology.hl7.org/ValueSet/v2-0615` +- `http://terminology.hl7.org/ValueSet/v2-0616` +- `http://terminology.hl7.org/ValueSet/v2-0617` +- `http://terminology.hl7.org/ValueSet/v2-0618` +- `http://terminology.hl7.org/ValueSet/v2-0625` +- `http://terminology.hl7.org/ValueSet/v2-0634` +- `http://terminology.hl7.org/ValueSet/v2-0642` +- `http://terminology.hl7.org/ValueSet/v2-0651` +- `http://terminology.hl7.org/ValueSet/v2-0653` +- `http://terminology.hl7.org/ValueSet/v2-0657` +- `http://terminology.hl7.org/ValueSet/v2-0659` +- `http://terminology.hl7.org/ValueSet/v2-0667` +- `http://terminology.hl7.org/ValueSet/v2-0669` +- `http://terminology.hl7.org/ValueSet/v2-0682` +- `http://terminology.hl7.org/ValueSet/v2-0702` +- `http://terminology.hl7.org/ValueSet/v2-0717` +- `http://terminology.hl7.org/ValueSet/v2-0719` +- `http://terminology.hl7.org/ValueSet/v2-0725` +- `http://terminology.hl7.org/ValueSet/v2-0728` +- `http://terminology.hl7.org/ValueSet/v2-0731` +- `http://terminology.hl7.org/ValueSet/v2-0734` +- `http://terminology.hl7.org/ValueSet/v2-0739` +- `http://terminology.hl7.org/ValueSet/v2-0742` +- `http://terminology.hl7.org/ValueSet/v2-0749` +- `http://terminology.hl7.org/ValueSet/v2-0755` +- `http://terminology.hl7.org/ValueSet/v2-0757` +- `http://terminology.hl7.org/ValueSet/v2-0759` +- `http://terminology.hl7.org/ValueSet/v2-0761` +- `http://terminology.hl7.org/ValueSet/v2-0763` +- `http://terminology.hl7.org/ValueSet/v2-0776` +- `http://terminology.hl7.org/ValueSet/v2-0778` +- `http://terminology.hl7.org/ValueSet/v2-0790` +- `http://terminology.hl7.org/ValueSet/v2-0793` +- `http://terminology.hl7.org/ValueSet/v2-0806` +- `http://terminology.hl7.org/ValueSet/v2-0818` +- `http://terminology.hl7.org/ValueSet/v2-0834` +- `http://terminology.hl7.org/ValueSet/v2-0868` +- `http://terminology.hl7.org/ValueSet/v2-0871` +- `http://terminology.hl7.org/ValueSet/v2-0881` +- `http://terminology.hl7.org/ValueSet/v2-0882` +- `http://terminology.hl7.org/ValueSet/v2-0894` +- `http://terminology.hl7.org/ValueSet/v2-0895` +- `http://terminology.hl7.org/ValueSet/v2-0904` +- `http://terminology.hl7.org/ValueSet/v2-0905` +- `http://terminology.hl7.org/ValueSet/v2-0906` +- `http://terminology.hl7.org/ValueSet/v2-0907` +- `http://terminology.hl7.org/ValueSet/v2-0909` +- `http://terminology.hl7.org/ValueSet/v2-0912` +- `http://terminology.hl7.org/ValueSet/v2-0914` +- `http://terminology.hl7.org/ValueSet/v2-0916` +- `http://terminology.hl7.org/ValueSet/v2-0917` +- `http://terminology.hl7.org/ValueSet/v2-0918` +- `http://terminology.hl7.org/ValueSet/v2-0919` +- `http://terminology.hl7.org/ValueSet/v2-0920` +- `http://terminology.hl7.org/ValueSet/v2-0921` +- `http://terminology.hl7.org/ValueSet/v2-0922` +- `http://terminology.hl7.org/ValueSet/v2-0923` +- `http://terminology.hl7.org/ValueSet/v2-0924` +- `http://terminology.hl7.org/ValueSet/v2-0925` +- `http://terminology.hl7.org/ValueSet/v2-0926` +- `http://terminology.hl7.org/ValueSet/v2-0927` +- `http://terminology.hl7.org/ValueSet/v2-0933` +- `http://terminology.hl7.org/ValueSet/v2-0935` +- `http://terminology.hl7.org/ValueSet/v2-2.1-0006` +- `http://terminology.hl7.org/ValueSet/v2-2.3.1-0360` +- `http://terminology.hl7.org/ValueSet/v2-2.4-0006` +- `http://terminology.hl7.org/ValueSet/v2-2.4-0391` +- `http://terminology.hl7.org/ValueSet/v2-2.6-0391` +- `http://terminology.hl7.org/ValueSet/v2-2.7-0360` +- `http://terminology.hl7.org/ValueSet/v2-4000` +- `http://terminology.hl7.org/ValueSet/v3-AcknowledgementCondition` +- `http://terminology.hl7.org/ValueSet/v3-AcknowledgementDetailCode` +- `http://terminology.hl7.org/ValueSet/v3-AcknowledgementDetailType` +- `http://terminology.hl7.org/ValueSet/v3-AcknowledgementType` +- `http://terminology.hl7.org/ValueSet/v3-ActClass` +- `http://terminology.hl7.org/ValueSet/v3-ActClassClinicalDocument` +- `http://terminology.hl7.org/ValueSet/v3-ActClassDocument` +- `http://terminology.hl7.org/ValueSet/v3-ActClassInvestigation` +- `http://terminology.hl7.org/ValueSet/v3-ActClassObservation` +- `http://terminology.hl7.org/ValueSet/v3-ActClassProcedure` +- `http://terminology.hl7.org/ValueSet/v3-ActClassROI` +- `http://terminology.hl7.org/ValueSet/v3-ActClassSupply` +- `http://terminology.hl7.org/ValueSet/v3-ActCode` +- `http://terminology.hl7.org/ValueSet/v3-ActConsentDirective` +- `http://terminology.hl7.org/ValueSet/v3-ActConsentType` +- `http://terminology.hl7.org/ValueSet/v3-ActCoverageTypeCode` +- `http://terminology.hl7.org/ValueSet/v3-ActEncounterCode` +- `http://terminology.hl7.org/ValueSet/v3-ActExposureLevelCode` +- `http://terminology.hl7.org/ValueSet/v3-ActIncidentCode` +- `http://terminology.hl7.org/ValueSet/v3-ActInvoiceElementModifier` +- `http://terminology.hl7.org/ValueSet/v3-ActInvoiceGroupCode` +- `http://terminology.hl7.org/ValueSet/v3-ActMood` +- `http://terminology.hl7.org/ValueSet/v3-ActMoodIntent` +- `http://terminology.hl7.org/ValueSet/v3-ActMoodPredicate` +- `http://terminology.hl7.org/ValueSet/v3-ActPharmacySupplyType` +- `http://terminology.hl7.org/ValueSet/v3-ActPriority` +- `http://terminology.hl7.org/ValueSet/v3-ActReason` +- `http://terminology.hl7.org/ValueSet/v3-ActRelationshipCheckpoint` +- `http://terminology.hl7.org/ValueSet/v3-ActRelationshipConditional` +- `http://terminology.hl7.org/ValueSet/v3-ActRelationshipFulfills` +- `http://terminology.hl7.org/ValueSet/v3-ActRelationshipHasComponent` +- `http://terminology.hl7.org/ValueSet/v3-ActRelationshipJoin` +- `http://terminology.hl7.org/ValueSet/v3-ActRelationshipPertains` +- `http://terminology.hl7.org/ValueSet/v3-ActRelationshipSplit` +- `http://terminology.hl7.org/ValueSet/v3-ActRelationshipSubset` +- `http://terminology.hl7.org/ValueSet/v3-ActRelationshipType` +- `http://terminology.hl7.org/ValueSet/v3-ActSite` +- `http://terminology.hl7.org/ValueSet/v3-ActStatus` +- `http://terminology.hl7.org/ValueSet/v3-ActSubstanceAdminSubstitutionCode` +- `http://terminology.hl7.org/ValueSet/v3-ActTaskCode` +- `http://terminology.hl7.org/ValueSet/v3-ActUSPrivacyLaw` +- `http://terminology.hl7.org/ValueSet/v3-ActUncertainty` +- `http://terminology.hl7.org/ValueSet/v3-AddressPartType` +- `http://terminology.hl7.org/ValueSet/v3-AddressUse` +- `http://terminology.hl7.org/ValueSet/v3-AdministrativeGender` +- `http://terminology.hl7.org/ValueSet/v3-AmericanIndianAlaskaNativeLanguages` +- `http://terminology.hl7.org/ValueSet/v3-Calendar` +- `http://terminology.hl7.org/ValueSet/v3-CalendarCycle` +- `http://terminology.hl7.org/ValueSet/v3-CalendarType` +- `http://terminology.hl7.org/ValueSet/v3-Charset` +- `http://terminology.hl7.org/ValueSet/v3-CodingRationale` +- `http://terminology.hl7.org/ValueSet/v3-CommunicationFunctionType` +- `http://terminology.hl7.org/ValueSet/v3-Compartment` +- `http://terminology.hl7.org/ValueSet/v3-CompressionAlgorithm` +- `http://terminology.hl7.org/ValueSet/v3-Confidentiality` +- `http://terminology.hl7.org/ValueSet/v3-ConfidentialityClassification` +- `http://terminology.hl7.org/ValueSet/v3-ContainerCap` +- `http://terminology.hl7.org/ValueSet/v3-ContainerSeparator` +- `http://terminology.hl7.org/ValueSet/v3-ContentProcessingMode` +- `http://terminology.hl7.org/ValueSet/v3-ContextControl` +- `http://terminology.hl7.org/ValueSet/v3-DataOperation` +- `http://terminology.hl7.org/ValueSet/v3-Dentition` +- `http://terminology.hl7.org/ValueSet/v3-DeviceAlertLevel` +- `http://terminology.hl7.org/ValueSet/v3-DocumentCompletion` +- `http://terminology.hl7.org/ValueSet/v3-DocumentSectionType` +- `http://terminology.hl7.org/ValueSet/v3-DocumentStorage` +- `http://terminology.hl7.org/ValueSet/v3-EducationLevel` +- `http://terminology.hl7.org/ValueSet/v3-EmployeeJobClass` +- `http://terminology.hl7.org/ValueSet/v3-EncounterAdmissionSource` +- `http://terminology.hl7.org/ValueSet/v3-EncounterSpecialCourtesy` +- `http://terminology.hl7.org/ValueSet/v3-EntityClass` +- `http://terminology.hl7.org/ValueSet/v3-EntityClassDevice` +- `http://terminology.hl7.org/ValueSet/v3-EntityClassLivingSubject` +- `http://terminology.hl7.org/ValueSet/v3-EntityClassManufacturedMaterial` +- `http://terminology.hl7.org/ValueSet/v3-EntityClassOrganization` +- `http://terminology.hl7.org/ValueSet/v3-EntityClassPlace` +- `http://terminology.hl7.org/ValueSet/v3-EntityClassRoot` +- `http://terminology.hl7.org/ValueSet/v3-EntityCode` +- `http://terminology.hl7.org/ValueSet/v3-EntityDeterminer` +- `http://terminology.hl7.org/ValueSet/v3-EntityDeterminerDetermined` +- `http://terminology.hl7.org/ValueSet/v3-EntityHandling` +- `http://terminology.hl7.org/ValueSet/v3-EntityNamePartQualifier` +- `http://terminology.hl7.org/ValueSet/v3-EntityNamePartQualifierR2` +- `http://terminology.hl7.org/ValueSet/v3-EntityNamePartType` +- `http://terminology.hl7.org/ValueSet/v3-EntityNamePartTypeR2` +- `http://terminology.hl7.org/ValueSet/v3-EntityNameUse` +- `http://terminology.hl7.org/ValueSet/v3-EntityNameUseR2` +- `http://terminology.hl7.org/ValueSet/v3-EntityRisk` +- `http://terminology.hl7.org/ValueSet/v3-EntityStatus` +- `http://terminology.hl7.org/ValueSet/v3-EquipmentAlertLevel` +- `http://terminology.hl7.org/ValueSet/v3-Ethnicity` +- `http://terminology.hl7.org/ValueSet/v3-ExposureMode` +- `http://terminology.hl7.org/ValueSet/v3-FamilyMember` +- `http://terminology.hl7.org/ValueSet/v3-GTSAbbreviation` +- `http://terminology.hl7.org/ValueSet/v3-GenderStatus` +- `http://terminology.hl7.org/ValueSet/v3-GeneralPurposeOfUse` +- `http://terminology.hl7.org/ValueSet/v3-HL7ContextConductionStyle` +- `http://terminology.hl7.org/ValueSet/v3-HL7StandardVersionCode` +- `http://terminology.hl7.org/ValueSet/v3-HL7UpdateMode` +- `http://terminology.hl7.org/ValueSet/v3-HtmlLinkType` +- `http://terminology.hl7.org/ValueSet/v3-HumanLanguage` +- `http://terminology.hl7.org/ValueSet/v3-IdentifierReliability` +- `http://terminology.hl7.org/ValueSet/v3-IdentifierScope` +- `http://terminology.hl7.org/ValueSet/v3-InformationSensitivityPolicy` +- `http://terminology.hl7.org/ValueSet/v3-IntegrityCheckAlgorithm` +- `http://terminology.hl7.org/ValueSet/v3-LanguageAbilityMode` +- `http://terminology.hl7.org/ValueSet/v3-LanguageAbilityProficiency` +- `http://terminology.hl7.org/ValueSet/v3-LivingArrangement` +- `http://terminology.hl7.org/ValueSet/v3-LocalMarkupIgnore` +- `http://terminology.hl7.org/ValueSet/v3-LocalRemoteControlState` +- `http://terminology.hl7.org/ValueSet/v3-ManagedParticipationStatus` +- `http://terminology.hl7.org/ValueSet/v3-MapRelationship` +- `http://terminology.hl7.org/ValueSet/v3-MaritalStatus` +- `http://terminology.hl7.org/ValueSet/v3-MessageWaitingPriority` +- `http://terminology.hl7.org/ValueSet/v3-MilitaryRoleType` +- `http://terminology.hl7.org/ValueSet/v3-ModifyIndicator` +- `http://terminology.hl7.org/ValueSet/v3-NullFlavor` +- `http://terminology.hl7.org/ValueSet/v3-ObligationPolicy` +- `http://terminology.hl7.org/ValueSet/v3-ObservationCategory` +- `http://terminology.hl7.org/ValueSet/v3-ObservationInterpretation` +- `http://terminology.hl7.org/ValueSet/v3-ObservationMethod` +- `http://terminology.hl7.org/ValueSet/v3-ObservationType` +- `http://terminology.hl7.org/ValueSet/v3-ObservationValue` +- `http://terminology.hl7.org/ValueSet/v3-ParticipationFunction` +- `http://terminology.hl7.org/ValueSet/v3-ParticipationIndirectTarget` +- `http://terminology.hl7.org/ValueSet/v3-ParticipationInformationGenerator` +- `http://terminology.hl7.org/ValueSet/v3-ParticipationInformationTranscriber` +- `http://terminology.hl7.org/ValueSet/v3-ParticipationMode` +- `http://terminology.hl7.org/ValueSet/v3-ParticipationPhysicalPerformer` +- `http://terminology.hl7.org/ValueSet/v3-ParticipationSignature` +- `http://terminology.hl7.org/ValueSet/v3-ParticipationTargetDirect` +- `http://terminology.hl7.org/ValueSet/v3-ParticipationTargetLocation` +- `http://terminology.hl7.org/ValueSet/v3-ParticipationTargetSubject` +- `http://terminology.hl7.org/ValueSet/v3-ParticipationType` +- `http://terminology.hl7.org/ValueSet/v3-ParticipationVerifier` +- `http://terminology.hl7.org/ValueSet/v3-PatientImportance` +- `http://terminology.hl7.org/ValueSet/v3-PaymentTerms` +- `http://terminology.hl7.org/ValueSet/v3-PersonDisabilityType` +- `http://terminology.hl7.org/ValueSet/v3-PersonalRelationshipRoleType` +- `http://terminology.hl7.org/ValueSet/v3-ProbabilityDistributionType` +- `http://terminology.hl7.org/ValueSet/v3-ProcessingID` +- `http://terminology.hl7.org/ValueSet/v3-ProcessingMode` +- `http://terminology.hl7.org/ValueSet/v3-ProvenanceEventCurrentState` +- `http://terminology.hl7.org/ValueSet/v3-ProvenanceEventCurrentState-AS` +- `http://terminology.hl7.org/ValueSet/v3-ProvenanceEventCurrentState-DC` +- `http://terminology.hl7.org/ValueSet/v3-PurposeOfUse` +- `http://terminology.hl7.org/ValueSet/v3-QueryParameterValue` +- `http://terminology.hl7.org/ValueSet/v3-QueryPriority` +- `http://terminology.hl7.org/ValueSet/v3-QueryRequestLimit` +- `http://terminology.hl7.org/ValueSet/v3-QueryResponse` +- `http://terminology.hl7.org/ValueSet/v3-QueryStatusCode` +- `http://terminology.hl7.org/ValueSet/v3-Race` +- `http://terminology.hl7.org/ValueSet/v3-RefrainPolicy` +- `http://terminology.hl7.org/ValueSet/v3-RelationalOperator` +- `http://terminology.hl7.org/ValueSet/v3-RelationshipConjunction` +- `http://terminology.hl7.org/ValueSet/v3-ReligiousAffiliation` +- `http://terminology.hl7.org/ValueSet/v3-ResponseLevel` +- `http://terminology.hl7.org/ValueSet/v3-ResponseModality` +- `http://terminology.hl7.org/ValueSet/v3-ResponseMode` +- `http://terminology.hl7.org/ValueSet/v3-RoleClass` +- `http://terminology.hl7.org/ValueSet/v3-RoleClassAgent` +- `http://terminology.hl7.org/ValueSet/v3-RoleClassAssociative` +- `http://terminology.hl7.org/ValueSet/v3-RoleClassManufacturedProduct` +- `http://terminology.hl7.org/ValueSet/v3-RoleClassMutualRelationship` +- `http://terminology.hl7.org/ValueSet/v3-RoleClassPartitive` +- `http://terminology.hl7.org/ValueSet/v3-RoleClassPassive` +- `http://terminology.hl7.org/ValueSet/v3-RoleClassRelationshipFormal` +- `http://terminology.hl7.org/ValueSet/v3-RoleClassRoot` +- `http://terminology.hl7.org/ValueSet/v3-RoleClassServiceDeliveryLocation` +- `http://terminology.hl7.org/ValueSet/v3-RoleClassSpecimen` +- `http://terminology.hl7.org/ValueSet/v3-RoleCode` +- `http://terminology.hl7.org/ValueSet/v3-RoleLinkStatus` +- `http://terminology.hl7.org/ValueSet/v3-RoleLinkType` +- `http://terminology.hl7.org/ValueSet/v3-RoleStatus` +- `http://terminology.hl7.org/ValueSet/v3-RouteOfAdministration` +- `http://terminology.hl7.org/ValueSet/v3-SecurityControlObservationValue` +- `http://terminology.hl7.org/ValueSet/v3-SecurityIntegrityObservationValue` +- `http://terminology.hl7.org/ValueSet/v3-SecurityPolicy` +- `http://terminology.hl7.org/ValueSet/v3-Sequencing` +- `http://terminology.hl7.org/ValueSet/v3-ServiceDeliveryLocationRoleType` +- `http://terminology.hl7.org/ValueSet/v3-SetOperator` +- `http://terminology.hl7.org/ValueSet/v3-SeverityObservation` +- `http://terminology.hl7.org/ValueSet/v3-SpecimenType` +- `http://terminology.hl7.org/ValueSet/v3-SubstanceAdminSubstitutionReason` +- `http://terminology.hl7.org/ValueSet/v3-SubstitutionCondition` +- `http://terminology.hl7.org/ValueSet/v3-TableCellHorizontalAlign` +- `http://terminology.hl7.org/ValueSet/v3-TableCellScope` +- `http://terminology.hl7.org/ValueSet/v3-TableCellVerticalAlign` +- `http://terminology.hl7.org/ValueSet/v3-TableFrame` +- `http://terminology.hl7.org/ValueSet/v3-TableRules` +- `http://terminology.hl7.org/ValueSet/v3-TargetAwareness` +- `http://terminology.hl7.org/ValueSet/v3-TelecommunicationCapabilities` +- `http://terminology.hl7.org/ValueSet/v3-TimingEvent` +- `http://terminology.hl7.org/ValueSet/v3-TransmissionRelationshipTypeCode` +- `http://terminology.hl7.org/ValueSet/v3-TribalEntityUS` +- `http://terminology.hl7.org/ValueSet/v3-VaccineManufacturer` +- `http://terminology.hl7.org/ValueSet/v3-VerificationMethod` +- `http://terminology.hl7.org/ValueSet/v3-WorkClassificationODH` +- `http://terminology.hl7.org/ValueSet/v3-WorkScheduleODH` +- `http://terminology.hl7.org/ValueSet/v3-employmentStatusODH` +- `http://terminology.hl7.org/ValueSet/v3-hl7ApprovalStatus` +- `http://terminology.hl7.org/ValueSet/v3-hl7CMETAttribution` +- `http://terminology.hl7.org/ValueSet/v3-hl7ITSType` +- `http://terminology.hl7.org/ValueSet/v3-hl7ITSVersionCode` +- `http://terminology.hl7.org/ValueSet/v3-hl7PublishingDomain` +- `http://terminology.hl7.org/ValueSet/v3-hl7PublishingSection` +- `http://terminology.hl7.org/ValueSet/v3-hl7PublishingSubSection` +- `http://terminology.hl7.org/ValueSet/v3-hl7Realm` +- `http://terminology.hl7.org/ValueSet/v3-hl7V3Conformance` +- `http://terminology.hl7.org/ValueSet/v3-hl7VoteResolution` +- `http://terminology.hl7.org/ValueSet/v3-orderableDrugForm` +- `http://terminology.hl7.org/ValueSet/v3-policyHolderRole` +- `http://terminology.hl7.org/ValueSet/v3-styleType` +- `http://terminology.hl7.org/ValueSet/v3-substanceAdminSubstitution` +- `http://terminology.hl7.org/ValueSet/v3-triggerEventID` +- `http://terminology.hl7.org/ValueSet/v3-xBasicConfidentialityKind` + +## Package: `shared` + +### Skipped Canonicals + +- `urn:fhir:binding:AccidentType` +- `urn:fhir:binding:AccountStatus` +- `urn:fhir:binding:AccountType` +- `urn:fhir:binding:ActionCardinalityBehavior` +- `urn:fhir:binding:ActionConditionKind` +- `urn:fhir:binding:ActionGroupingBehavior` +- `urn:fhir:binding:ActionParticipantRole` +- `urn:fhir:binding:ActionParticipantType` +- `urn:fhir:binding:ActionPrecheckBehavior` +- `urn:fhir:binding:ActionRelationshipType` +- `urn:fhir:binding:ActionRequiredBehavior` +- `urn:fhir:binding:ActionSelectionBehavior` +- `urn:fhir:binding:ActionType` +- `urn:fhir:binding:ActivityDefinitionKind` +- `urn:fhir:binding:ActivityDefinitionType` +- `urn:fhir:binding:ActivityParticipantRole` +- `urn:fhir:binding:ActivityParticipantType` +- `urn:fhir:binding:AdditionalInstruction` +- `urn:fhir:binding:AddressType` +- `urn:fhir:binding:AddressUse` +- `urn:fhir:binding:Adjudication` +- `urn:fhir:binding:AdjudicationError` +- `urn:fhir:binding:AdjudicationReason` +- `urn:fhir:binding:AdjunctDiagnosis` +- `urn:fhir:binding:AdmitSource` +- `urn:fhir:binding:AdverseEventActuality` +- `urn:fhir:binding:AdverseEventCategory` +- `urn:fhir:binding:AdverseEventCausalityAssessment` +- `urn:fhir:binding:AdverseEventCausalityMethod` +- `urn:fhir:binding:AdverseEventOutcome` +- `urn:fhir:binding:AdverseEventSeriousness` +- `urn:fhir:binding:AdverseEventSeverity` +- `urn:fhir:binding:AdverseEventType` +- `urn:fhir:binding:AggregationMode` +- `urn:fhir:binding:AllergyIntoleranceCategory` +- `urn:fhir:binding:AllergyIntoleranceClinicalStatus` +- `urn:fhir:binding:AllergyIntoleranceCode` +- `urn:fhir:binding:AllergyIntoleranceCriticality` +- `urn:fhir:binding:AllergyIntoleranceSeverity` +- `urn:fhir:binding:AllergyIntoleranceType` +- `urn:fhir:binding:AllergyIntoleranceVerificationStatus` +- `urn:fhir:binding:AppointmentStatus` +- `urn:fhir:binding:ApptReason` +- `urn:fhir:binding:Arrangements` +- `urn:fhir:binding:AssertionDirectionType` +- `urn:fhir:binding:AssertionOperatorType` +- `urn:fhir:binding:AssertionResponseTypes` +- `urn:fhir:binding:AssetAvailabilityType` +- `urn:fhir:binding:AuditAgentRole` +- `urn:fhir:binding:AuditAgentType` +- `urn:fhir:binding:AuditEventAction` +- `urn:fhir:binding:AuditEventAgentNetworkType` +- `urn:fhir:binding:AuditEventEntityLifecycle` +- `urn:fhir:binding:AuditEventEntityRole` +- `urn:fhir:binding:AuditEventEntityType` +- `urn:fhir:binding:AuditEventOutcome` +- `urn:fhir:binding:AuditEventSourceType` +- `urn:fhir:binding:AuditEventSubType` +- `urn:fhir:binding:AuditEventType` +- `urn:fhir:binding:AuditPurposeOfUse` +- `urn:fhir:binding:AuthSupporting` +- `urn:fhir:binding:BasicResourceType` +- `urn:fhir:binding:BenefitCategory` +- `urn:fhir:binding:BenefitCostApplicability` +- `urn:fhir:binding:BenefitNetwork` +- `urn:fhir:binding:BenefitTerm` +- `urn:fhir:binding:BenefitType` +- `urn:fhir:binding:BenefitUnit` +- `urn:fhir:binding:BindingStrength` +- `urn:fhir:binding:BiologicallyDerivedProductCategory` +- `urn:fhir:binding:BiologicallyDerivedProductProcedure` +- `urn:fhir:binding:BiologicallyDerivedProductStatus` +- `urn:fhir:binding:BiologicallyDerivedProductStorageScale` +- `urn:fhir:binding:BodyLengthUnits` +- `urn:fhir:binding:BodySite` +- `urn:fhir:binding:BodyStructureCode` +- `urn:fhir:binding:BodyStructureQualifier` +- `urn:fhir:binding:BodyTempUnits` +- `urn:fhir:binding:BodyWeightUnits` +- `urn:fhir:binding:CapabilityStatementKind` +- `urn:fhir:binding:CarePlanActivityKind` +- `urn:fhir:binding:CarePlanActivityOutcome` +- `urn:fhir:binding:CarePlanActivityReason` +- `urn:fhir:binding:CarePlanActivityStatus` +- `urn:fhir:binding:CarePlanActivityType` +- `urn:fhir:binding:CarePlanCategory` +- `urn:fhir:binding:CarePlanIntent` +- `urn:fhir:binding:CarePlanStatus` +- `urn:fhir:binding:CareTeamCategory` +- `urn:fhir:binding:CareTeamParticipantRole` +- `urn:fhir:binding:CareTeamReason` +- `urn:fhir:binding:CareTeamRole` +- `urn:fhir:binding:CareTeamStatus` +- `urn:fhir:binding:CatalogEntryRelationType` +- `urn:fhir:binding:CatalogType` +- `urn:fhir:binding:CertaintySubcomponentRating` +- `urn:fhir:binding:CertaintySubcomponentType` +- `urn:fhir:binding:ChargeItemCode` +- `urn:fhir:binding:ChargeItemDefinitionCode` +- `urn:fhir:binding:ChargeItemDefinitionPriceComponentType` +- `urn:fhir:binding:ChargeItemPerformerFunction` +- `urn:fhir:binding:ChargeItemReason` +- `urn:fhir:binding:ChargeItemStatus` +- `urn:fhir:binding:ClaimResponseStatus` +- `urn:fhir:binding:ClaimStatus` +- `urn:fhir:binding:ClaimSubType` +- `urn:fhir:binding:ClaimType` +- `urn:fhir:binding:ClinicalImpressionPrognosis` +- `urn:fhir:binding:ClinicalImpressionStatus` +- `urn:fhir:binding:CodeSearchSupport` +- `urn:fhir:binding:CodeSystemContentMode` +- `urn:fhir:binding:CodeSystemHierarchyMeaning` +- `urn:fhir:binding:CollectedSpecimenType` +- `urn:fhir:binding:CommunicationCategory` +- `urn:fhir:binding:CommunicationMedium` +- `urn:fhir:binding:CommunicationNotDoneReason` +- `urn:fhir:binding:CommunicationPriority` +- `urn:fhir:binding:CommunicationReason` +- `urn:fhir:binding:CommunicationRequestStatus` +- `urn:fhir:binding:CommunicationStatus` +- `urn:fhir:binding:CommunicationTopic` +- `urn:fhir:binding:CompartmentCode` +- `urn:fhir:binding:CompartmentType` +- `urn:fhir:binding:CompositeMeasureScoring` +- `urn:fhir:binding:CompositionAttestationMode` +- `urn:fhir:binding:CompositionSectionType` +- `urn:fhir:binding:CompositionStatus` +- `urn:fhir:binding:ConceptDesignationUse` +- `urn:fhir:binding:ConceptMapEquivalence` +- `urn:fhir:binding:ConceptMapGroupUnmappedMode` +- `urn:fhir:binding:ConditionCategory` +- `urn:fhir:binding:ConditionClinicalStatus` +- `urn:fhir:binding:ConditionCode` +- `urn:fhir:binding:ConditionKind` +- `urn:fhir:binding:ConditionOutcome` +- `urn:fhir:binding:ConditionSeverity` +- `urn:fhir:binding:ConditionStage` +- `urn:fhir:binding:ConditionStageType` +- `urn:fhir:binding:ConditionVerificationStatus` +- `urn:fhir:binding:ConditionalDeleteStatus` +- `urn:fhir:binding:ConditionalReadStatus` +- `urn:fhir:binding:ConsentAction` +- `urn:fhir:binding:ConsentActorRole` +- `urn:fhir:binding:ConsentCategory` +- `urn:fhir:binding:ConsentContentClass` +- `urn:fhir:binding:ConsentContentCode` +- `urn:fhir:binding:ConsentDataMeaning` +- `urn:fhir:binding:ConsentPolicyRule` +- `urn:fhir:binding:ConsentProvisionType` +- `urn:fhir:binding:ConsentScope` +- `urn:fhir:binding:ConsentState` +- `urn:fhir:binding:ConstraintSeverity` +- `urn:fhir:binding:ContactPartyType` +- `urn:fhir:binding:ContactPointSystem` +- `urn:fhir:binding:ContactPointUse` +- `urn:fhir:binding:ContactRelationship` +- `urn:fhir:binding:ContainerCap` +- `urn:fhir:binding:ContainerMaterial` +- `urn:fhir:binding:ContainerType` +- `urn:fhir:binding:ContractAction` +- `urn:fhir:binding:ContractActionPerformerRole` +- `urn:fhir:binding:ContractActionPerformerType` +- `urn:fhir:binding:ContractActionReason` +- `urn:fhir:binding:ContractActionStatus` +- `urn:fhir:binding:ContractActorRole` +- `urn:fhir:binding:ContractAssetContext` +- `urn:fhir:binding:ContractAssetScope` +- `urn:fhir:binding:ContractAssetSubtype` +- `urn:fhir:binding:ContractAssetType` +- `urn:fhir:binding:ContractContentDerivative` +- `urn:fhir:binding:ContractDecisionMode` +- `urn:fhir:binding:ContractDecisionType` +- `urn:fhir:binding:ContractDefinitionSubtype` +- `urn:fhir:binding:ContractDefinitionType` +- `urn:fhir:binding:ContractExpiration` +- `urn:fhir:binding:ContractLegalState` +- `urn:fhir:binding:ContractPartyRole` +- `urn:fhir:binding:ContractPublicationStatus` +- `urn:fhir:binding:ContractScope` +- `urn:fhir:binding:ContractSecurityCategory` +- `urn:fhir:binding:ContractSecurityClassification` +- `urn:fhir:binding:ContractSecurityControl` +- `urn:fhir:binding:ContractSignerType` +- `urn:fhir:binding:ContractStatus` +- `urn:fhir:binding:ContractSubtype` +- `urn:fhir:binding:ContractTermSubType` +- `urn:fhir:binding:ContractTermType` +- `urn:fhir:binding:ContractType` +- `urn:fhir:binding:ContributorType` +- `urn:fhir:binding:CopayTypes` +- `urn:fhir:binding:Courtesies` +- `urn:fhir:binding:CoverageClass` +- `urn:fhir:binding:CoverageFinancialException` +- `urn:fhir:binding:CoverageStatus` +- `urn:fhir:binding:CoverageType` +- `urn:fhir:binding:CurrencyCode` +- `urn:fhir:binding:DICOMMediaType` +- `urn:fhir:binding:DayOfWeek` +- `urn:fhir:binding:DaysOfWeek` +- `urn:fhir:binding:DefinitionTopic` +- `urn:fhir:binding:DetectedIssueCategory` +- `urn:fhir:binding:DetectedIssueEvidenceCode` +- `urn:fhir:binding:DetectedIssueMitigationAction` +- `urn:fhir:binding:DetectedIssueSeverity` +- `urn:fhir:binding:DetectedIssueStatus` +- `urn:fhir:binding:DeviceActionKind` +- `urn:fhir:binding:DeviceKind` +- `urn:fhir:binding:DeviceMetricCalibrationState` +- `urn:fhir:binding:DeviceMetricCalibrationType` +- `urn:fhir:binding:DeviceMetricCategory` +- `urn:fhir:binding:DeviceMetricColor` +- `urn:fhir:binding:DeviceMetricOperationalStatus` +- `urn:fhir:binding:DeviceNameType` +- `urn:fhir:binding:DeviceRequestParticipantRole` +- `urn:fhir:binding:DeviceRequestReason` +- `urn:fhir:binding:DeviceRequestStatus` +- `urn:fhir:binding:DeviceType` +- `urn:fhir:binding:DeviceUseStatementStatus` +- `urn:fhir:binding:DiagnosisOnAdmission` +- `urn:fhir:binding:DiagnosisRelatedGroup` +- `urn:fhir:binding:DiagnosisRole` +- `urn:fhir:binding:DiagnosisType` +- `urn:fhir:binding:DiagnosticReportCodes` +- `urn:fhir:binding:DiagnosticReportStatus` +- `urn:fhir:binding:DiagnosticServiceSection` +- `urn:fhir:binding:DischargeDisp` +- `urn:fhir:binding:DiscriminatorType` +- `urn:fhir:binding:DocumentC80Class` +- `urn:fhir:binding:DocumentC80FacilityType` +- `urn:fhir:binding:DocumentC80PracticeSetting` +- `urn:fhir:binding:DocumentC80Type` +- `urn:fhir:binding:DocumentCategory` +- `urn:fhir:binding:DocumentConfidentiality` +- `urn:fhir:binding:DocumentEventType` +- `urn:fhir:binding:DocumentFormat` +- `urn:fhir:binding:DocumentMode` +- `urn:fhir:binding:DocumentReferenceStatus` +- `urn:fhir:binding:DocumentRelationshipType` +- `urn:fhir:binding:DocumentType` +- `urn:fhir:binding:DoseAndRateType` +- `urn:fhir:binding:EffectEstimateType` +- `urn:fhir:binding:ElementDefinitionCode` +- `urn:fhir:binding:EligibilityRequestPurpose` +- `urn:fhir:binding:EligibilityRequestStatus` +- `urn:fhir:binding:EligibilityResponsePurpose` +- `urn:fhir:binding:EligibilityResponseStatus` +- `urn:fhir:binding:EnableWhenBehavior` +- `urn:fhir:binding:EncounterClass` +- `urn:fhir:binding:EncounterLocationStatus` +- `urn:fhir:binding:EncounterReason` +- `urn:fhir:binding:EncounterServiceType` +- `urn:fhir:binding:EncounterStatus` +- `urn:fhir:binding:EncounterType` +- `urn:fhir:binding:EndpointStatus` +- `urn:fhir:binding:EnrollmentRequestStatus` +- `urn:fhir:binding:EnrollmentResponseStatus` +- `urn:fhir:binding:EnteralFormulaAdditiveType` +- `urn:fhir:binding:EnteralFormulaType` +- `urn:fhir:binding:EnteralRouteOfAdministration` +- `urn:fhir:binding:EpisodeOfCareStatus` +- `urn:fhir:binding:EpisodeOfCareType` +- `urn:fhir:binding:EvaluationDoseStatus` +- `urn:fhir:binding:EvaluationDoseStatusReason` +- `urn:fhir:binding:EvaluationTargetDisease` +- `urn:fhir:binding:EventCapabilityMode` +- `urn:fhir:binding:EventPerformerFunction` +- `urn:fhir:binding:EventReason` +- `urn:fhir:binding:EventTiming` +- `urn:fhir:binding:EvidenceVariableType` +- `urn:fhir:binding:EvidenceVariantState` +- `urn:fhir:binding:ExampleScenarioActorType` +- `urn:fhir:binding:ExplanationOfBenefitStatus` +- `urn:fhir:binding:ExposureState` +- `urn:fhir:binding:ExpressionLanguage` +- `urn:fhir:binding:ExtensionContextType` +- `urn:fhir:binding:FHIRAllTypes` +- `urn:fhir:binding:FHIRDefinedType` +- `urn:fhir:binding:FHIRDefinedTypeExt` +- `urn:fhir:binding:FHIRDeviceStatus` +- `urn:fhir:binding:FHIRDeviceStatusReason` +- `urn:fhir:binding:FHIRResourceType` +- `urn:fhir:binding:FHIRSubstanceStatus` +- `urn:fhir:binding:FHIRVersion` +- `urn:fhir:binding:FamilialRelationship` +- `urn:fhir:binding:FamilyHistoryAbsentReason` +- `urn:fhir:binding:FamilyHistoryReason` +- `urn:fhir:binding:FamilyHistoryStatus` +- `urn:fhir:binding:FilterOperator` +- `urn:fhir:binding:FlagCategory` +- `urn:fhir:binding:FlagCode` +- `urn:fhir:binding:FlagStatus` +- `urn:fhir:binding:FluidConsistencyType` +- `urn:fhir:binding:FoodType` +- `urn:fhir:binding:Forms` +- `urn:fhir:binding:FundingSource` +- `urn:fhir:binding:FundsReserve` +- `urn:fhir:binding:GoalAchievementStatus` +- `urn:fhir:binding:GoalAddresses` +- `urn:fhir:binding:GoalCategory` +- `urn:fhir:binding:GoalDescription` +- `urn:fhir:binding:GoalLifecycleStatus` +- `urn:fhir:binding:GoalOutcome` +- `urn:fhir:binding:GoalPriority` +- `urn:fhir:binding:GoalStartEvent` +- `urn:fhir:binding:GoalTargetMeasure` +- `urn:fhir:binding:GraphCompartmentRule` +- `urn:fhir:binding:GraphCompartmentUse` +- `urn:fhir:binding:GroupMeasure` +- `urn:fhir:binding:GroupType` +- `urn:fhir:binding:GuidanceResponseStatus` +- `urn:fhir:binding:GuidePageGeneration` +- `urn:fhir:binding:GuideParameterCode` +- `urn:fhir:binding:HandlingConditionSet` +- `urn:fhir:binding:IdentityAssuranceLevel` +- `urn:fhir:binding:ImagingModality` +- `urn:fhir:binding:ImagingProcedureCode` +- `urn:fhir:binding:ImagingReason` +- `urn:fhir:binding:ImagingStudyStatus` +- `urn:fhir:binding:ImmunizationEvaluationStatus` +- `urn:fhir:binding:ImmunizationFunction` +- `urn:fhir:binding:ImmunizationReason` +- `urn:fhir:binding:ImmunizationRecommendationDateCriterion` +- `urn:fhir:binding:ImmunizationRecommendationReason` +- `urn:fhir:binding:ImmunizationRecommendationStatus` +- `urn:fhir:binding:ImmunizationReportOrigin` +- `urn:fhir:binding:ImmunizationRoute` +- `urn:fhir:binding:ImmunizationSite` +- `urn:fhir:binding:ImmunizationStatus` +- `urn:fhir:binding:ImmunizationStatusReason` +- `urn:fhir:binding:InformationCategory` +- `urn:fhir:binding:InformationCode` +- `urn:fhir:binding:InsurancePlanType` +- `urn:fhir:binding:IntendedSpecimenType` +- `urn:fhir:binding:InvestigationGroupType` +- `urn:fhir:binding:InvoicePriceComponentType` +- `urn:fhir:binding:InvoiceStatus` +- `urn:fhir:binding:Jurisdiction` +- `urn:fhir:binding:LDLCodes` +- `urn:fhir:binding:LOINC LL379-9 answerlist` +- `urn:fhir:binding:Laterality` +- `urn:fhir:binding:LibraryType` +- `urn:fhir:binding:LinkType` +- `urn:fhir:binding:LinkageType` +- `urn:fhir:binding:ListEmptyReason` +- `urn:fhir:binding:ListItemFlag` +- `urn:fhir:binding:ListMode` +- `urn:fhir:binding:ListOrder` +- `urn:fhir:binding:ListPurpose` +- `urn:fhir:binding:ListStatus` +- `urn:fhir:binding:LocationMode` +- `urn:fhir:binding:LocationStatus` +- `urn:fhir:binding:LocationType` +- `urn:fhir:binding:Manifestation` +- `urn:fhir:binding:ManifestationOrSymptom` +- `urn:fhir:binding:MeasureDataUsage` +- `urn:fhir:binding:MeasureImprovementNotation` +- `urn:fhir:binding:MeasurePopulation` +- `urn:fhir:binding:MeasurePopulationType` +- `urn:fhir:binding:MeasureReportStatus` +- `urn:fhir:binding:MeasureReportType` +- `urn:fhir:binding:MeasureScoring` +- `urn:fhir:binding:MeasureType` +- `urn:fhir:binding:MediaModality` +- `urn:fhir:binding:MediaReason` +- `urn:fhir:binding:MediaStatus` +- `urn:fhir:binding:MediaType` +- `urn:fhir:binding:MediaView` +- `urn:fhir:binding:MedicationAdministrationCategory` +- `urn:fhir:binding:MedicationAdministrationMethod` +- `urn:fhir:binding:MedicationAdministrationNegationReason` +- `urn:fhir:binding:MedicationAdministrationPerformerFunction` +- `urn:fhir:binding:MedicationAdministrationReason` +- `urn:fhir:binding:MedicationAdministrationSite` +- `urn:fhir:binding:MedicationAdministrationStatus` +- `urn:fhir:binding:MedicationCharacteristic` +- `urn:fhir:binding:MedicationDispenseCategory` +- `urn:fhir:binding:MedicationDispensePerformerFunction` +- `urn:fhir:binding:MedicationDispenseStatus` +- `urn:fhir:binding:MedicationDispenseType` +- `urn:fhir:binding:MedicationForm` +- `urn:fhir:binding:MedicationFormalRepresentation` +- `urn:fhir:binding:MedicationIntendedSubstitutionReason` +- `urn:fhir:binding:MedicationIntendedSubstitutionType` +- `urn:fhir:binding:MedicationKnowledgeStatus` +- `urn:fhir:binding:MedicationPackageType` +- `urn:fhir:binding:MedicationReason` +- `urn:fhir:binding:MedicationRequestCategory` +- `urn:fhir:binding:MedicationRequestCourseOfTherapy` +- `urn:fhir:binding:MedicationRequestIntent` +- `urn:fhir:binding:MedicationRequestPerformerType` +- `urn:fhir:binding:MedicationRequestPriority` +- `urn:fhir:binding:MedicationRequestReason` +- `urn:fhir:binding:MedicationRequestStatus` +- `urn:fhir:binding:MedicationRequestStatusReason` +- `urn:fhir:binding:MedicationRoute` +- `urn:fhir:binding:MedicationStatementCategory` +- `urn:fhir:binding:MedicationStatementStatus` +- `urn:fhir:binding:MedicationStatementStatusReason` +- `urn:fhir:binding:MedicationStatus` +- `urn:fhir:binding:MessageSignificanceCategory` +- `urn:fhir:binding:MessageTransport` +- `urn:fhir:binding:MetricType` +- `urn:fhir:binding:MetricUnit` +- `urn:fhir:binding:MissingReason` +- `urn:fhir:binding:Modifiers` +- `urn:fhir:binding:NamingSystemIdentifierType` +- `urn:fhir:binding:NamingSystemType` +- `urn:fhir:binding:NoteType` +- `urn:fhir:binding:NutrientModifier` +- `urn:fhir:binding:NutritiionOrderIntent` +- `urn:fhir:binding:NutritionOrderStatus` +- `urn:fhir:binding:ObservationCategory` +- `urn:fhir:binding:ObservationCode` +- `urn:fhir:binding:ObservationDataType` +- `urn:fhir:binding:ObservationInterpretation` +- `urn:fhir:binding:ObservationMethod` +- `urn:fhir:binding:ObservationRangeAppliesTo` +- `urn:fhir:binding:ObservationRangeCategory` +- `urn:fhir:binding:ObservationRangeMeaning` +- `urn:fhir:binding:ObservationRangeType` +- `urn:fhir:binding:ObservationStatus` +- `urn:fhir:binding:ObservationUnit` +- `urn:fhir:binding:ObservationValueAbsentReason` +- `urn:fhir:binding:OperationKind` +- `urn:fhir:binding:OperationParameterUse` +- `urn:fhir:binding:OperationalStatus` +- `urn:fhir:binding:OralDiet` +- `urn:fhir:binding:OralSites` +- `urn:fhir:binding:OrderDetail` +- `urn:fhir:binding:OrganizationAffiliation` +- `urn:fhir:binding:OrganizationSpecialty` +- `urn:fhir:binding:OrganizationType` +- `urn:fhir:binding:ParameterUse` +- `urn:fhir:binding:ParticipantRequired` +- `urn:fhir:binding:ParticipantStatus` +- `urn:fhir:binding:ParticipantType` +- `urn:fhir:binding:ParticipationStatus` +- `urn:fhir:binding:PatientDiet` +- `urn:fhir:binding:PatientRelationshipType` +- `urn:fhir:binding:PayeeType` +- `urn:fhir:binding:PayloadType` +- `urn:fhir:binding:PaymentAdjustmentReason` +- `urn:fhir:binding:PaymentNoticeStatus` +- `urn:fhir:binding:PaymentReconciliationStatus` +- `urn:fhir:binding:PaymentStatus` +- `urn:fhir:binding:PaymentType` +- `urn:fhir:binding:PhysicalType` +- `urn:fhir:binding:PlanDefinitionType` +- `urn:fhir:binding:PractitionerRole` +- `urn:fhir:binding:PractitionerSpecialty` +- `urn:fhir:binding:PrecisionEstimateType` +- `urn:fhir:binding:PreparePatient` +- `urn:fhir:binding:Priority` +- `urn:fhir:binding:ProcedureCategory` +- `urn:fhir:binding:ProcedureCode` +- `urn:fhir:binding:ProcedureComplication` +- `urn:fhir:binding:ProcedureFollowUp` +- `urn:fhir:binding:ProcedureNegationReason` +- `urn:fhir:binding:ProcedureOutcome` +- `urn:fhir:binding:ProcedurePerformerRole` +- `urn:fhir:binding:ProcedureReason` +- `urn:fhir:binding:ProcedureStatus` +- `urn:fhir:binding:ProcedureType` +- `urn:fhir:binding:ProcedureUsed` +- `urn:fhir:binding:ProcessPriority` +- `urn:fhir:binding:Program` +- `urn:fhir:binding:ProgramCode` +- `urn:fhir:binding:ProgramEligibility` +- `urn:fhir:binding:PropertyRepresentation` +- `urn:fhir:binding:PropertyType` +- `urn:fhir:binding:ProvenanceActivity` +- `urn:fhir:binding:ProvenanceAgentRole` +- `urn:fhir:binding:ProvenanceAgentType` +- `urn:fhir:binding:ProvenanceEntityRole` +- `urn:fhir:binding:ProvenanceHistoryAgentType` +- `urn:fhir:binding:ProvenanceHistoryRecordActivity` +- `urn:fhir:binding:ProvenanceReason` +- `urn:fhir:binding:ProviderQualification` +- `urn:fhir:binding:PublicationStatus` +- `urn:fhir:binding:PurposeOfUse` +- `urn:fhir:binding:Qualification` +- `urn:fhir:binding:QualityOfEvidenceRating` +- `urn:fhir:binding:QuantityComparator` +- `urn:fhir:binding:QuestionnaireConcept` +- `urn:fhir:binding:QuestionnaireItemOperator` +- `urn:fhir:binding:QuestionnaireItemType` +- `urn:fhir:binding:QuestionnaireResponseStatus` +- `urn:fhir:binding:ReAdmissionType` +- `urn:fhir:binding:ReferenceHandlingPolicy` +- `urn:fhir:binding:ReferenceVersionRules` +- `urn:fhir:binding:ReferralMethod` +- `urn:fhir:binding:ReferredDocumentStatus` +- `urn:fhir:binding:RejectionCriterion` +- `urn:fhir:binding:RelatedArtifactType` +- `urn:fhir:binding:RelatedClaimRelationship` +- `urn:fhir:binding:Relationship` +- `urn:fhir:binding:RemittanceOutcome` +- `urn:fhir:binding:RequestIntent` +- `urn:fhir:binding:RequestPriority` +- `urn:fhir:binding:RequestStatus` +- `urn:fhir:binding:ResearchElementType` +- `urn:fhir:binding:ResearchStudyObjectiveType` +- `urn:fhir:binding:ResearchStudyPhase` +- `urn:fhir:binding:ResearchStudyPrimaryPurposeType` +- `urn:fhir:binding:ResearchStudyReasonStopped` +- `urn:fhir:binding:ResearchStudyStatus` +- `urn:fhir:binding:ResearchSubjectStatus` +- `urn:fhir:binding:ResourceType` +- `urn:fhir:binding:ResourceVersionPolicy` +- `urn:fhir:binding:ResponseType` +- `urn:fhir:binding:RestfulCapabilityMode` +- `urn:fhir:binding:RestfulSecurityService` +- `urn:fhir:binding:RevenueCenter` +- `urn:fhir:binding:RiskAssessmentProbability` +- `urn:fhir:binding:RiskAssessmentStatus` +- `urn:fhir:binding:RiskEstimateType` +- `urn:fhir:binding:RouteOfAdministration` +- `urn:fhir:binding:SPDXLicense` +- `urn:fhir:binding:Safety` +- `urn:fhir:binding:SearchComparator` +- `urn:fhir:binding:SearchModifierCode` +- `urn:fhir:binding:SearchParamType` +- `urn:fhir:binding:SectionEmptyReason` +- `urn:fhir:binding:SectionEntryOrder` +- `urn:fhir:binding:SectionMode` +- `urn:fhir:binding:ServiceProduct` +- `urn:fhir:binding:ServiceProvisionConditions` +- `urn:fhir:binding:ServiceRequestCategory` +- `urn:fhir:binding:ServiceRequestCode` +- `urn:fhir:binding:ServiceRequestIntent` +- `urn:fhir:binding:ServiceRequestLocation` +- `urn:fhir:binding:ServiceRequestParticipantRole` +- `urn:fhir:binding:ServiceRequestPriority` +- `urn:fhir:binding:ServiceRequestReason` +- `urn:fhir:binding:ServiceRequestStatus` +- `urn:fhir:binding:Sex` +- `urn:fhir:binding:SlicingRules` +- `urn:fhir:binding:SlotStatus` +- `urn:fhir:binding:SortDirection` +- `urn:fhir:binding:SpecimenCollection` +- `urn:fhir:binding:SpecimenCollectionMethod` +- `urn:fhir:binding:SpecimenCondition` +- `urn:fhir:binding:SpecimenContainedPreference` +- `urn:fhir:binding:SpecimenContainerType` +- `urn:fhir:binding:SpecimenProcessingProcedure` +- `urn:fhir:binding:SpecimenStatus` +- `urn:fhir:binding:SpecimenType` +- `urn:fhir:binding:Status` +- `urn:fhir:binding:StructureDefinitionKeyword` +- `urn:fhir:binding:StructureDefinitionKind` +- `urn:fhir:binding:StructureMapContextType` +- `urn:fhir:binding:StructureMapGroupTypeMode` +- `urn:fhir:binding:StructureMapInputMode` +- `urn:fhir:binding:StructureMapModelMode` +- `urn:fhir:binding:StructureMapSourceListMode` +- `urn:fhir:binding:StructureMapTargetListMode` +- `urn:fhir:binding:StructureMapTransform` +- `urn:fhir:binding:StudyType` +- `urn:fhir:binding:SubpotentReason` +- `urn:fhir:binding:SubscriptionChannelType` +- `urn:fhir:binding:SubscriptionStatus` +- `urn:fhir:binding:SubstanceCategory` +- `urn:fhir:binding:SubstanceCode` +- `urn:fhir:binding:SupplementType` +- `urn:fhir:binding:SupplyDeliveryStatus` +- `urn:fhir:binding:SupplyDeliveryType` +- `urn:fhir:binding:SupplyRequestKind` +- `urn:fhir:binding:SupplyRequestReason` +- `urn:fhir:binding:SupplyRequestStatus` +- `urn:fhir:binding:Surface` +- `urn:fhir:binding:SynthesisType` +- `urn:fhir:binding:SystemRestfulInteraction` +- `urn:fhir:binding:TargetDisease` +- `urn:fhir:binding:TaskCode` +- `urn:fhir:binding:TaskIntent` +- `urn:fhir:binding:TaskPerformerType` +- `urn:fhir:binding:TaskPriority` +- `urn:fhir:binding:TaskStatus` +- `urn:fhir:binding:TestReportActionResult` +- `urn:fhir:binding:TestReportParticipantType` +- `urn:fhir:binding:TestReportResult` +- `urn:fhir:binding:TestReportStatus` +- `urn:fhir:binding:TestScriptOperationCode` +- `urn:fhir:binding:TestScriptProfileDestinationType` +- `urn:fhir:binding:TestScriptProfileOriginType` +- `urn:fhir:binding:TestScriptRequestMethodCode` +- `urn:fhir:binding:TextureModifiedFoodType` +- `urn:fhir:binding:TextureModifier` +- `urn:fhir:binding:TimingAbbreviation` +- `urn:fhir:binding:TriggerType` +- `urn:fhir:binding:TypeDerivationRule` +- `urn:fhir:binding:TypeRestfulInteraction` +- `urn:fhir:binding:UCUMUnits` +- `urn:fhir:binding:UDIEntryType` +- `urn:fhir:binding:UnitsOfTime` +- `urn:fhir:binding:UsageContextType` +- `urn:fhir:binding:Use` +- `urn:fhir:binding:VaccineCode` +- `urn:fhir:binding:VariableType` +- `urn:fhir:binding:VisionBase` +- `urn:fhir:binding:VisionEyes` +- `urn:fhir:binding:VisionProduct` +- `urn:fhir:binding:VisionStatus` +- `urn:fhir:binding:VitalSigns` +- `urn:fhir:binding:XPathUsageType` +- `urn:fhir:binding:appointment-type` +- `urn:fhir:binding:can-push-updates` +- `urn:fhir:binding:cancelation-reason` +- `urn:fhir:binding:chromosome-human` +- `urn:fhir:binding:communication-method` +- `urn:fhir:binding:endpoint-contype` +- `urn:fhir:binding:failure-action` +- `urn:fhir:binding:messageheader-response-request` +- `urn:fhir:binding:need` +- `urn:fhir:binding:orientationType` +- `urn:fhir:binding:primary-source-type` +- `urn:fhir:binding:push-type-available` +- `urn:fhir:binding:qualityMethod` +- `urn:fhir:binding:qualityStandardSequence` +- `urn:fhir:binding:qualityType` +- `urn:fhir:binding:repositoryType` +- `urn:fhir:binding:sequenceReference` +- `urn:fhir:binding:sequenceType` +- `urn:fhir:binding:service-category` +- `urn:fhir:binding:service-specialty` +- `urn:fhir:binding:service-type` +- `urn:fhir:binding:sopClass` +- `urn:fhir:binding:specialty` +- `urn:fhir:binding:status` +- `urn:fhir:binding:strandType` +- `urn:fhir:binding:v3Act` +- `urn:fhir:binding:validation-process` +- `urn:fhir:binding:validation-status` +- `urn:fhir:binding:validation-type` + +## Schema Collisions + +The following canonicals have multiple schema versions with different content. +To inspect collision versions, export TypeSchemas using `.introspection({ typeSchemas: 'path' })` +and check `/collisions//1.json, 2.json, ...` files. + +### `shared` + +- `urn:fhir:binding:CommunicationReason` (2 versions) + - Version 1 (auto): Communication (hl7.fhir.r4.core#4.0.1) + - Version 2: CommunicationRequest (hl7.fhir.r4.core#4.0.1) +- `urn:fhir:binding:ObservationCategory` (2 versions) + - Version 1 (auto): Observation (hl7.fhir.r4.core#4.0.1), vitalsigns (hl7.fhir.r4.core#4.0.1) + - Version 2: ObservationDefinition (hl7.fhir.r4.core#4.0.1) +- `urn:fhir:binding:ObservationRangeMeaning` (2 versions) + - Version 1 (auto): cholesterol (hl7.fhir.r4.core#4.0.1), hdlcholesterol (hl7.fhir.r4.core#4.0.1), ldlcholesterol (hl7.fhir.r4.core#4.0.1), Observation (hl7.fhir.r4.core#4.0.1), triglyceride (hl7.fhir.r4.core#4.0.1) + - Version 2: ObservationDefinition (hl7.fhir.r4.core#4.0.1) +- `urn:fhir:binding:PaymentType` (2 versions) + - Version 1 (auto): ClaimResponse (hl7.fhir.r4.core#4.0.1), ExplanationOfBenefit (hl7.fhir.r4.core#4.0.1) + - Version 2: PaymentReconciliation (hl7.fhir.r4.core#4.0.1) +- `urn:fhir:binding:ProcessPriority` (2 versions) + - Version 1 (auto): Claim (hl7.fhir.r4.core#4.0.1), CoverageEligibilityRequest (hl7.fhir.r4.core#4.0.1) + - Version 2: ExplanationOfBenefit (hl7.fhir.r4.core#4.0.1) +- `urn:fhir:binding:TargetDisease` (2 versions) + - Version 1 (auto): Immunization (hl7.fhir.r4.core#4.0.1) + - Version 2: ImmunizationRecommendation (hl7.fhir.r4.core#4.0.1) + +### Suggested `resolveCollisions` config + +Add to `.typeSchema({ resolveCollisions: { ... } })` to resolve remaining collisions: + +```typescript +.typeSchema({ + resolveCollisions: { + "urn:fhir:binding:CommunicationReason": { + package: "hl7.fhir.r4.core#4.0.1", + canonical: "http://hl7.org/fhir/StructureDefinition/Communication", + }, + "urn:fhir:binding:ObservationCategory": { + package: "hl7.fhir.r4.core#4.0.1", + canonical: "http://hl7.org/fhir/StructureDefinition/Observation", + }, + "urn:fhir:binding:ObservationRangeMeaning": { + package: "hl7.fhir.r4.core#4.0.1", + canonical: "http://hl7.org/fhir/StructureDefinition/cholesterol", + }, + "urn:fhir:binding:PaymentType": { + package: "hl7.fhir.r4.core#4.0.1", + canonical: "http://hl7.org/fhir/StructureDefinition/ClaimResponse", + }, + "urn:fhir:binding:ProcessPriority": { + package: "hl7.fhir.r4.core#4.0.1", + canonical: "http://hl7.org/fhir/StructureDefinition/Claim", + }, + "urn:fhir:binding:TargetDisease": { + package: "hl7.fhir.r4.core#4.0.1", + canonical: "http://hl7.org/fhir/StructureDefinition/Immunization", + }, + }, +}) +``` diff --git a/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Address.ts b/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Address.ts deleted file mode 100644 index 778577f6..00000000 --- a/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Address.ts +++ /dev/null @@ -1,32 +0,0 @@ -// WARNING: This file is autogenerated by @atomic-ehr/codegen. -// GitHub: https://github.com/atomic-ehr/codegen -// Any manual changes made to this file may be overwritten. - -import type { Element } from "../hl7-fhir-r4-core/Element"; -import type { Period } from "../hl7-fhir-r4-core/Period"; - -export type { Element } from "../hl7-fhir-r4-core/Element"; -export type { Period } from "../hl7-fhir-r4-core/Period"; - -// CanonicalURL: http://hl7.org/fhir/StructureDefinition/Address -export interface Address extends Element { - city?: string; - _city?: Element; - country?: string; - _country?: Element; - district?: string; - _district?: Element; - line?: string[]; - _line?: Element; - period?: Period; - postalCode?: string; - _postalCode?: Element; - state?: string; - _state?: Element; - text?: string; - _text?: Element; - type?: ("postal" | "physical" | "both"); - _type?: Element; - use?: ("home" | "work" | "temp" | "old" | "billing"); - _use?: Element; -} diff --git a/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/BackboneElement.ts b/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/BackboneElement.ts index 302ef173..4eedb60b 100644 --- a/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/BackboneElement.ts +++ b/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/BackboneElement.ts @@ -6,6 +6,6 @@ import type { Element } from "../hl7-fhir-r4-core/Element"; export type { Element } from "../hl7-fhir-r4-core/Element"; -// CanonicalURL: http://hl7.org/fhir/StructureDefinition/BackboneElement +// CanonicalURL: http://hl7.org/fhir/StructureDefinition/BackboneElement (pkg: hl7.fhir.r4.core#4.0.1) export interface BackboneElement extends Element { } diff --git a/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Bundle.ts b/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Bundle.ts index 8b563536..4683d070 100644 --- a/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Bundle.ts +++ b/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Bundle.ts @@ -7,16 +7,17 @@ import type { Identifier } from "../hl7-fhir-r4-core/Identifier"; import type { Resource } from "../hl7-fhir-r4-core/Resource"; import type { Signature } from "../hl7-fhir-r4-core/Signature"; +import type { Element } from "../hl7-fhir-r4-core/Element"; export type { BackboneElement } from "../hl7-fhir-r4-core/BackboneElement"; export type { Identifier } from "../hl7-fhir-r4-core/Identifier"; export type { Signature } from "../hl7-fhir-r4-core/Signature"; -export interface BundleEntry extends BackboneElement { +export interface BundleEntry extends BackboneElement { fullUrl?: string; link?: BundleLink[]; request?: BundleEntryRequest; - resource?: Resource; - response?: BundleEntryResponse; + resource?: T1; + response?: BundleEntryResponse; search?: BundleEntrySearch; } @@ -29,11 +30,11 @@ export interface BundleEntryRequest extends BackboneElement { url: string; } -export interface BundleEntryResponse extends BackboneElement { +export interface BundleEntryResponse extends BackboneElement { etag?: string; lastModified?: string; location?: string; - outcome?: Resource; + outcome?: T; status: string; } @@ -47,11 +48,11 @@ export interface BundleLink extends BackboneElement { url: string; } -// CanonicalURL: http://hl7.org/fhir/StructureDefinition/Bundle -export interface Bundle extends Resource { +// CanonicalURL: http://hl7.org/fhir/StructureDefinition/Bundle (pkg: hl7.fhir.r4.core#4.0.1) +export interface Bundle extends Resource { resourceType: "Bundle"; - entry?: BundleEntry[]; + entry?: BundleEntry[]; identifier?: Identifier; link?: BundleLink[]; signature?: Signature; diff --git a/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/CodeableConcept.ts b/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/CodeableConcept.ts index 233a7a6a..94f47f6f 100644 --- a/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/CodeableConcept.ts +++ b/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/CodeableConcept.ts @@ -8,9 +8,9 @@ import type { Element } from "../hl7-fhir-r4-core/Element"; export type { Coding } from "../hl7-fhir-r4-core/Coding"; export type { Element } from "../hl7-fhir-r4-core/Element"; -// CanonicalURL: http://hl7.org/fhir/StructureDefinition/CodeableConcept -export interface CodeableConcept extends Element { - coding?: Coding[]; +// CanonicalURL: http://hl7.org/fhir/StructureDefinition/CodeableConcept (pkg: hl7.fhir.r4.core#4.0.1) +export interface CodeableConcept extends Element { + coding?: Coding[]; text?: string; _text?: Element; } diff --git a/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Coding.ts b/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Coding.ts index f02997ee..84ad129e 100644 --- a/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Coding.ts +++ b/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Coding.ts @@ -6,9 +6,9 @@ import type { Element } from "../hl7-fhir-r4-core/Element"; export type { Element } from "../hl7-fhir-r4-core/Element"; -// CanonicalURL: http://hl7.org/fhir/StructureDefinition/Coding -export interface Coding extends Element { - code?: string; +// CanonicalURL: http://hl7.org/fhir/StructureDefinition/Coding (pkg: hl7.fhir.r4.core#4.0.1) +export interface Coding extends Element { + code?: T; _code?: Element; display?: string; _display?: Element; diff --git a/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/ContactPoint.ts b/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/ContactPoint.ts deleted file mode 100644 index 6696f3bf..00000000 --- a/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/ContactPoint.ts +++ /dev/null @@ -1,22 +0,0 @@ -// WARNING: This file is autogenerated by @atomic-ehr/codegen. -// GitHub: https://github.com/atomic-ehr/codegen -// Any manual changes made to this file may be overwritten. - -import type { Element } from "../hl7-fhir-r4-core/Element"; -import type { Period } from "../hl7-fhir-r4-core/Period"; - -export type { Element } from "../hl7-fhir-r4-core/Element"; -export type { Period } from "../hl7-fhir-r4-core/Period"; - -// CanonicalURL: http://hl7.org/fhir/StructureDefinition/ContactPoint -export interface ContactPoint extends Element { - period?: Period; - rank?: number; - _rank?: Element; - system?: ("phone" | "fax" | "email" | "pager" | "url" | "sms" | "other"); - _system?: Element; - use?: ("home" | "work" | "temp" | "old" | "mobile"); - _use?: Element; - value?: string; - _value?: Element; -} diff --git a/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/DomainResource.ts b/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/DomainResource.ts index 830f8104..2317a823 100644 --- a/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/DomainResource.ts +++ b/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/DomainResource.ts @@ -7,11 +7,11 @@ import type { Resource } from "../hl7-fhir-r4-core/Resource"; export type { Narrative } from "../hl7-fhir-r4-core/Narrative"; -// CanonicalURL: http://hl7.org/fhir/StructureDefinition/DomainResource -export interface DomainResource extends Resource { +// CanonicalURL: http://hl7.org/fhir/StructureDefinition/DomainResource (pkg: hl7.fhir.r4.core#4.0.1) +export interface DomainResource extends Resource { resourceType: "DomainResource" | "OperationOutcome" | "Patient"; - contained?: Resource[]; + contained?: T[]; text?: Narrative; } export const isDomainResource = (resource: unknown): resource is DomainResource => { diff --git a/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Element.ts b/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Element.ts index 42445e74..5ca3b3f2 100644 --- a/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Element.ts +++ b/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Element.ts @@ -3,7 +3,7 @@ // Any manual changes made to this file may be overwritten. -// CanonicalURL: http://hl7.org/fhir/StructureDefinition/Element +// CanonicalURL: http://hl7.org/fhir/StructureDefinition/Element (pkg: hl7.fhir.r4.core#4.0.1) export interface Element { id?: string; _id?: Element; diff --git a/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/HumanName.ts b/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/HumanName.ts index 8751c577..457f899c 100644 --- a/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/HumanName.ts +++ b/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/HumanName.ts @@ -8,17 +8,17 @@ import type { Period } from "../hl7-fhir-r4-core/Period"; export type { Element } from "../hl7-fhir-r4-core/Element"; export type { Period } from "../hl7-fhir-r4-core/Period"; -// CanonicalURL: http://hl7.org/fhir/StructureDefinition/HumanName +// CanonicalURL: http://hl7.org/fhir/StructureDefinition/HumanName (pkg: hl7.fhir.r4.core#4.0.1) export interface HumanName extends Element { family?: string; _family?: Element; given?: string[]; - _given?: Element; + _given?: (Element | null)[]; period?: Period; prefix?: string[]; - _prefix?: Element; + _prefix?: (Element | null)[]; suffix?: string[]; - _suffix?: Element; + _suffix?: (Element | null)[]; text?: string; _text?: Element; use?: ("usual" | "official" | "temp" | "nickname" | "anonymous" | "old" | "maiden"); diff --git a/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Identifier.ts b/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Identifier.ts index 5b41c664..2171bdbc 100644 --- a/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Identifier.ts +++ b/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Identifier.ts @@ -12,13 +12,13 @@ export type { Element } from "../hl7-fhir-r4-core/Element"; export type { Period } from "../hl7-fhir-r4-core/Period"; export type { Reference } from "../hl7-fhir-r4-core/Reference"; -// CanonicalURL: http://hl7.org/fhir/StructureDefinition/Identifier +// CanonicalURL: http://hl7.org/fhir/StructureDefinition/Identifier (pkg: hl7.fhir.r4.core#4.0.1) export interface Identifier extends Element { assigner?: Reference<"Organization">; period?: Period; system?: string; _system?: Element; - type?: CodeableConcept; + type?: CodeableConcept<("DL" | "PPN" | "BRN" | "MR" | "MCN" | "EN" | "TAX" | "NIIP" | "PRN" | "MD" | "DR" | "ACSN" | "UDI" | "SNO" | "SB" | "PLAC" | "FILL" | "JHN" | string)>; use?: ("usual" | "official" | "temp" | "secondary" | "old"); _use?: Element; value?: string; diff --git a/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Meta.ts b/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Meta.ts index 3733ed44..0f90be81 100644 --- a/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Meta.ts +++ b/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Meta.ts @@ -8,12 +8,12 @@ import type { Element } from "../hl7-fhir-r4-core/Element"; export type { Coding } from "../hl7-fhir-r4-core/Coding"; export type { Element } from "../hl7-fhir-r4-core/Element"; -// CanonicalURL: http://hl7.org/fhir/StructureDefinition/Meta +// CanonicalURL: http://hl7.org/fhir/StructureDefinition/Meta (pkg: hl7.fhir.r4.core#4.0.1) export interface Meta extends Element { lastUpdated?: string; _lastUpdated?: Element; profile?: string[]; - _profile?: Element; + _profile?: (Element | null)[]; security?: Coding[]; source?: string; _source?: Element; diff --git a/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Narrative.ts b/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Narrative.ts index db6d1913..572f0216 100644 --- a/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Narrative.ts +++ b/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Narrative.ts @@ -6,7 +6,7 @@ import type { Element } from "../hl7-fhir-r4-core/Element"; export type { Element } from "../hl7-fhir-r4-core/Element"; -// CanonicalURL: http://hl7.org/fhir/StructureDefinition/Narrative +// CanonicalURL: http://hl7.org/fhir/StructureDefinition/Narrative (pkg: hl7.fhir.r4.core#4.0.1) export interface Narrative extends Element { div: string; _div?: Element; diff --git a/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/OperationOutcome.ts b/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/OperationOutcome.ts index 86524f8d..4d1f622d 100644 --- a/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/OperationOutcome.ts +++ b/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/OperationOutcome.ts @@ -18,7 +18,7 @@ export interface OperationOutcomeIssue extends BackboneElement { severity: ("fatal" | "error" | "warning" | "information"); } -// CanonicalURL: http://hl7.org/fhir/StructureDefinition/OperationOutcome +// CanonicalURL: http://hl7.org/fhir/StructureDefinition/OperationOutcome (pkg: hl7.fhir.r4.core#4.0.1) export interface OperationOutcome extends DomainResource { resourceType: "OperationOutcome"; diff --git a/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Patient.ts b/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Patient.ts index 33551543..4f6a1cc0 100644 --- a/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Patient.ts +++ b/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Patient.ts @@ -2,46 +2,19 @@ // GitHub: https://github.com/atomic-ehr/codegen // Any manual changes made to this file may be overwritten. -import type { Address } from "../hl7-fhir-r4-core/Address"; -import type { BackboneElement } from "../hl7-fhir-r4-core/BackboneElement"; import type { CodeableConcept } from "../hl7-fhir-r4-core/CodeableConcept"; -import type { ContactPoint } from "../hl7-fhir-r4-core/ContactPoint"; import type { DomainResource } from "../hl7-fhir-r4-core/DomainResource"; import type { HumanName } from "../hl7-fhir-r4-core/HumanName"; import type { Identifier } from "../hl7-fhir-r4-core/Identifier"; -import type { Period } from "../hl7-fhir-r4-core/Period"; import type { Reference } from "../hl7-fhir-r4-core/Reference"; -export type { Address } from "../hl7-fhir-r4-core/Address"; -export type { BackboneElement } from "../hl7-fhir-r4-core/BackboneElement"; +import type { Element } from "../hl7-fhir-r4-core/Element"; export type { CodeableConcept } from "../hl7-fhir-r4-core/CodeableConcept"; -export type { ContactPoint } from "../hl7-fhir-r4-core/ContactPoint"; export type { HumanName } from "../hl7-fhir-r4-core/HumanName"; export type { Identifier } from "../hl7-fhir-r4-core/Identifier"; -export type { Period } from "../hl7-fhir-r4-core/Period"; export type { Reference } from "../hl7-fhir-r4-core/Reference"; -export interface PatientCommunication extends BackboneElement { - language: CodeableConcept; - preferred?: boolean; -} - -export interface PatientContact extends BackboneElement { - address?: Address; - gender?: ("male" | "female" | "other" | "unknown"); - name?: HumanName; - organization?: Reference<"Organization">; - period?: Period; - relationship?: CodeableConcept[]; - telecom?: ContactPoint[]; -} - -export interface PatientLink extends BackboneElement { - other: Reference<"Patient" | "RelatedPerson">; - type: ("replaced-by" | "replaces" | "refer" | "seealso"); -} - -// CanonicalURL: http://hl7.org/fhir/StructureDefinition/Patient +// CanonicalURL: http://hl7.org/fhir/StructureDefinition/Patient (pkg: hl7.fhir.r4.core#4.0.1) export interface Patient extends DomainResource { resourceType: "Patient"; @@ -58,7 +31,7 @@ export interface Patient extends DomainResource { generalPractitioner?: Reference<"Organization" | "Practitioner" | "PractitionerRole">[]; identifier?: Identifier[]; managingOrganization?: Reference<"Organization">; - maritalStatus?: CodeableConcept; + maritalStatus?: CodeableConcept<("A" | "D" | "I" | "L" | "M" | "P" | "S" | "T" | "U" | "W" | "UNK" | string)>; multipleBirthBoolean?: boolean; _multipleBirthBoolean?: Element; multipleBirthInteger?: number; diff --git a/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Period.ts b/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Period.ts index 2b903bdf..5a87c3fc 100644 --- a/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Period.ts +++ b/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Period.ts @@ -6,7 +6,7 @@ import type { Element } from "../hl7-fhir-r4-core/Element"; export type { Element } from "../hl7-fhir-r4-core/Element"; -// CanonicalURL: http://hl7.org/fhir/StructureDefinition/Period +// CanonicalURL: http://hl7.org/fhir/StructureDefinition/Period (pkg: hl7.fhir.r4.core#4.0.1) export interface Period extends Element { end?: string; _end?: Element; diff --git a/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Reference.ts b/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Reference.ts index c4d7e86b..a3e6da73 100644 --- a/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Reference.ts +++ b/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Reference.ts @@ -8,12 +8,12 @@ import type { Identifier } from "../hl7-fhir-r4-core/Identifier"; export type { Element } from "../hl7-fhir-r4-core/Element"; export type { Identifier } from "../hl7-fhir-r4-core/Identifier"; -// CanonicalURL: http://hl7.org/fhir/StructureDefinition/Reference +// CanonicalURL: http://hl7.org/fhir/StructureDefinition/Reference (pkg: hl7.fhir.r4.core#4.0.1) export interface Reference extends Element { display?: string; _display?: Element; identifier?: Identifier; - reference?: `${T}/${string}`; + reference?: `${T}/${string}` | `http://${string}` | `https://${string}` | `urn:uuid:${string}` | `urn:oid:${string}` | `#${string}`; _reference?: Element; type?: string; _type?: Element; diff --git a/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Resource.ts b/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Resource.ts index 3432fd56..709c0c04 100644 --- a/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Resource.ts +++ b/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Resource.ts @@ -4,9 +4,10 @@ import type { Meta } from "../hl7-fhir-r4-core/Meta"; +import type { Element } from "../hl7-fhir-r4-core/Element"; export type { Meta } from "../hl7-fhir-r4-core/Meta"; -// CanonicalURL: http://hl7.org/fhir/StructureDefinition/Resource +// CanonicalURL: http://hl7.org/fhir/StructureDefinition/Resource (pkg: hl7.fhir.r4.core#4.0.1) export interface Resource { resourceType: "Bundle" | "DomainResource" | "OperationOutcome" | "Patient" | "Resource"; @@ -14,7 +15,7 @@ export interface Resource { _id?: Element; implicitRules?: string; _implicitRules?: Element; - language?: string; + language?: ("ar" | "bn" | "cs" | "da" | "de" | "de-AT" | "de-CH" | "de-DE" | "el" | "en" | "en-AU" | "en-CA" | "en-GB" | "en-IN" | "en-NZ" | "en-SG" | "en-US" | "es" | "es-AR" | "es-ES" | "es-UY" | "fi" | "fr" | "fr-BE" | "fr-CH" | "fr-FR" | "fy" | "fy-NL" | "hi" | "hr" | "it" | "it-CH" | "it-IT" | "ja" | "ko" | "nl" | "nl-BE" | "nl-NL" | "no" | "no-NO" | "pa" | "pl" | "pt" | "pt-BR" | "ru" | "ru-RU" | "sr" | "sr-RS" | "sv" | "sv-SE" | "te" | "zh" | "zh-CN" | "zh-HK" | "zh-SG" | "zh-TW" | string); _language?: Element; meta?: Meta; } diff --git a/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Signature.ts b/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Signature.ts index 343cf774..5a46421f 100644 --- a/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Signature.ts +++ b/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/Signature.ts @@ -10,7 +10,7 @@ export type { Coding } from "../hl7-fhir-r4-core/Coding"; export type { Element } from "../hl7-fhir-r4-core/Element"; export type { Reference } from "../hl7-fhir-r4-core/Reference"; -// CanonicalURL: http://hl7.org/fhir/StructureDefinition/Signature +// CanonicalURL: http://hl7.org/fhir/StructureDefinition/Signature (pkg: hl7.fhir.r4.core#4.0.1) export interface Signature extends Element { data?: string; _data?: Element; @@ -19,7 +19,7 @@ export interface Signature extends Element { _sigFormat?: Element; targetFormat?: string; _targetFormat?: Element; - type: Coding[]; + type: Coding<("1.2.840.10065.1.12.1.1" | "1.2.840.10065.1.12.1.2" | "1.2.840.10065.1.12.1.3" | "1.2.840.10065.1.12.1.4" | "1.2.840.10065.1.12.1.5" | "1.2.840.10065.1.12.1.6" | "1.2.840.10065.1.12.1.7" | "1.2.840.10065.1.12.1.8" | "1.2.840.10065.1.12.1.9" | "1.2.840.10065.1.12.1.10" | "1.2.840.10065.1.12.1.11" | "1.2.840.10065.1.12.1.12" | "1.2.840.10065.1.12.1.13" | "1.2.840.10065.1.12.1.14" | "1.2.840.10065.1.12.1.15" | "1.2.840.10065.1.12.1.16" | "1.2.840.10065.1.12.1.17" | "1.2.840.10065.1.12.1.18" | string)>[]; when: string; _when?: Element; who: Reference<"Device" | "Organization" | "Patient" | "Practitioner" | "PractitionerRole" | "RelatedPerson">; diff --git a/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/index.ts b/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/index.ts index 8808128e..e00abfd3 100644 --- a/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/index.ts +++ b/packages/aidbox-client/src/fhir-types/hl7-fhir-r4-core/index.ts @@ -1,10 +1,8 @@ -export type { Address } from "./Address"; export type { BackboneElement } from "./BackboneElement"; export type { Bundle, BundleEntry, BundleEntryRequest, BundleEntryResponse, BundleEntrySearch, BundleLink } from "./Bundle"; export { isBundle } from "./Bundle"; export type { CodeableConcept } from "./CodeableConcept"; export type { Coding } from "./Coding"; -export type { ContactPoint } from "./ContactPoint"; export type { DomainResource } from "./DomainResource"; export { isDomainResource } from "./DomainResource"; export type { Element } from "./Element"; @@ -14,7 +12,7 @@ export type { Meta } from "./Meta"; export type { Narrative } from "./Narrative"; export type { OperationOutcome, OperationOutcomeIssue } from "./OperationOutcome"; export { isOperationOutcome } from "./OperationOutcome"; -export type { Patient, PatientCommunication, PatientContact, PatientLink } from "./Patient"; +export type { Patient } from "./Patient"; export { isPatient } from "./Patient"; export type { Period } from "./Period"; export type { Reference } from "./Reference"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1dba51dc..8e5f8f5b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,8 +42,8 @@ importers: version: 2.8.3 devDependencies: '@atomic-ehr/codegen': - specifier: ^0.0.8 - version: 0.0.8(typescript@5.9.3) + specifier: latest + version: 0.0.14(typescript@5.9.3) '@types/node': specifier: ^25.4.0 version: 25.4.0 @@ -402,8 +402,8 @@ packages: '@adobe/css-tools@4.4.4': resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} - '@atomic-ehr/codegen@0.0.8': - resolution: {integrity: sha512-xHXRvmchO/SC/fu6yNTcmtJrY0E0MBaN7Qd3QUu8d0b9r17DjG94u/VqefWMDNITws4hRv39iYsCrHNr79d1Pw==} + '@atomic-ehr/codegen@0.0.14': + resolution: {integrity: sha512-VBamrvDXN3FBA5IMyjA0rn6hs2lP7kTiXS834+yGb7vHDv0Q8nA6EIclyK7HQORyxIOHufF22YprRpDDckb+jw==} hasBin: true '@atomic-ehr/fhir-canonical-manager@0.0.11': @@ -412,8 +412,8 @@ packages: peerDependencies: typescript: ^5 - '@atomic-ehr/fhir-canonical-manager@0.0.21': - resolution: {integrity: sha512-MTmPXWixNJ6Wa2b9AqTeo4wg37w9u9ebDnVj0eRDwspDrNHhcgM/XYtEV3Oio7Mnzam3n9m0IPsto8iPQ87ilA==} + '@atomic-ehr/fhir-canonical-manager@0.0.24': + resolution: {integrity: sha512-3h+uGf3qqxNX2oClVx+Fz1NZ2Jm2h2dXKrbhA77gURmX5TUYYQKxdTh58a7N/tteLLsig5fW8q41wfeUqy7GdQ==} hasBin: true peerDependencies: typescript: ^5 @@ -426,13 +426,13 @@ packages: peerDependencies: typescript: ^5 - '@atomic-ehr/fhirschema@0.0.2': - resolution: {integrity: sha512-OA4CVjTUEdw43Efg5I5rj95je4GC3lRiLM/kUqadcK3Po24vnINUsB8YdOP/F3ffdUYKQcJ+z09sWQVeAC2z/A==} + '@atomic-ehr/fhirschema@0.0.11': + resolution: {integrity: sha512-oMNxhncEGspGI+QlK/FPjc7akLbfwMYw/hDfW6SbO8xF1KvSSH7NWqc3CJg/k5/309ZuJ6lKsHkgmgVDxo80sQ==} peerDependencies: typescript: ^5 - '@atomic-ehr/fhirschema@0.0.8': - resolution: {integrity: sha512-RB3ZlFHYYfP4ZaOA0YStGaxrm3T1MzpJLPAzAxW/7u2yfdmQz160e3mGYVv0mglHKtGhZ0fuuk2qqhDwUiGncg==} + '@atomic-ehr/fhirschema@0.0.2': + resolution: {integrity: sha512-OA4CVjTUEdw43Efg5I5rj95je4GC3lRiLM/kUqadcK3Po24vnINUsB8YdOP/F3ffdUYKQcJ+z09sWQVeAC2z/A==} peerDependencies: typescript: ^5 @@ -513,8 +513,8 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - '@babel/runtime@7.28.6': - resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} engines: {node: '>=6.9.0'} '@babel/template@7.28.6': @@ -4494,13 +4494,12 @@ snapshots: '@adobe/css-tools@4.4.4': {} - '@atomic-ehr/codegen@0.0.8(typescript@5.9.3)': + '@atomic-ehr/codegen@0.0.14(typescript@5.9.3)': dependencies: - '@atomic-ehr/fhir-canonical-manager': 0.0.21(typescript@5.9.3) - '@atomic-ehr/fhirschema': 0.0.8(typescript@5.9.3) + '@atomic-ehr/fhir-canonical-manager': 0.0.24(typescript@5.9.3) + '@atomic-ehr/fhirschema': 0.0.11(typescript@5.9.3) mustache: 4.2.0 picocolors: 1.1.1 - tinyglobby: 0.2.15 yaml: 2.8.3 yargs: 18.0.0 transitivePeerDependencies: @@ -4510,7 +4509,7 @@ snapshots: dependencies: typescript: 5.9.3 - '@atomic-ehr/fhir-canonical-manager@0.0.21(typescript@5.9.3)': + '@atomic-ehr/fhir-canonical-manager@0.0.24(typescript@5.9.3)': dependencies: typescript: 5.9.3 @@ -4539,11 +4538,11 @@ snapshots: fast-xml-parser: 5.7.2 typescript: 5.9.3 - '@atomic-ehr/fhirschema@0.0.2(typescript@5.9.3)': + '@atomic-ehr/fhirschema@0.0.11(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@atomic-ehr/fhirschema@0.0.8(typescript@5.9.3)': + '@atomic-ehr/fhirschema@0.0.2(typescript@5.9.3)': dependencies: typescript: 5.9.3 @@ -4645,7 +4644,7 @@ snapshots: dependencies: '@babel/types': 8.0.0-rc.2 - '@babel/runtime@7.28.6': {} + '@babel/runtime@7.29.2': {} '@babel/template@7.28.6': dependencies: @@ -6477,7 +6476,7 @@ snapshots: '@testing-library/dom@10.4.1': dependencies: '@babel/code-frame': 7.29.0 - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.29.2 '@types/aria-query': 5.0.4 aria-query: 5.3.0 dom-accessibility-api: 0.5.16 From 371c67da2b80292e232df945536ced45bbcccbbb Mon Sep 17 00:00:00 2001 From: Panthevm Date: Tue, 12 May 2026 17:37:48 +0300 Subject: [PATCH 37/55] Sidebar: align collapsible="none" styling with collapsible="icon" The "none" variant rendered with bg-sidebar (grey) and no border, while the "icon"/"offcanvas" variants render with bg-bg-primary (white) and side-aware border. Inconsistent visual when using Sidebar standalone (e.g. nested inside another page). - Switch background to bg-bg-primary - Add border-r/border-l based on `side` for `variant="sidebar"` - Expose `data-side` and `data-variant` on the root div for consistency --- .../react-components/src/shadcn/components/ui/sidebar.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/react-components/src/shadcn/components/ui/sidebar.tsx b/packages/react-components/src/shadcn/components/ui/sidebar.tsx index 487e5b57..d2ce3a79 100644 --- a/packages/react-components/src/shadcn/components/ui/sidebar.tsx +++ b/packages/react-components/src/shadcn/components/ui/sidebar.tsx @@ -170,8 +170,11 @@ function Sidebar({ return (
Date: Wed, 13 May 2026 18:53:40 +0300 Subject: [PATCH 38/55] fix(Combobox): focus search input via useEffect after popover opens Previously inputRef.current?.focus() was called synchronously inside changeOpen, which fires before Radix mounts the popover content. Move the focus into a useEffect keyed on open with a requestAnimationFrame so the input is actually focused once it exists in the DOM. --- .../src/shadcn/components/ui/combobox.tsx | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/react-components/src/shadcn/components/ui/combobox.tsx b/packages/react-components/src/shadcn/components/ui/combobox.tsx index 6d1e009f..63df34a7 100644 --- a/packages/react-components/src/shadcn/components/ui/combobox.tsx +++ b/packages/react-components/src/shadcn/components/ui/combobox.tsx @@ -114,14 +114,18 @@ export function Combobox({ const selectedOption = options.find((option) => option.value === value); const changeOpen = (newOpen: boolean) => { - if (!newOpen) { - setSearchValue(""); - } else { - inputRef.current?.focus(); - } + if (!newOpen) setSearchValue(""); setOpen(newOpen); }; + React.useEffect(() => { + if (!open) return; + const frame = requestAnimationFrame(() => { + inputRef.current?.focus(); + }); + return () => cancelAnimationFrame(frame); + }, [open]); + return ( use case. Safari honors it strictly and clips the embedded + // (search input + list) to ~30px. Chromium/Firefox grow the flex + // child past the constraint, so the bug is invisible there. + "[&_[data-radix-select-viewport]]:h-auto", ); const commandStyles = cn( From 7f17840112d48430031b606c5bfbc461a3c0207f Mon Sep 17 00:00:00 2001 From: Panthevm Date: Tue, 9 Jun 2026 14:18:01 +0300 Subject: [PATCH 45/55] CodeEditor: reserve right padding + opaque gutter with fade shadow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add paddingRight: 400px to .cm-content so the area under the floating top-right menubar (Copy / Format / Mode toggle in REST Console etc.) stays empty and horizontal scrolling reaches everything without hiding behind the menubar. Give .cm-gutters the editor background (was transparent, so scrolled text was visible through the line numbers) and a 4px var(--color-bg-primary) shadow that fades content sliding under the gutter — makes horizontal scroll discoverable. --- .../react-components/src/components/code-editor/index.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/react-components/src/components/code-editor/index.tsx b/packages/react-components/src/components/code-editor/index.tsx index dfc15bc4..f1162cc2 100644 --- a/packages/react-components/src/components/code-editor/index.tsx +++ b/packages/react-components/src/components/code-editor/index.tsx @@ -339,6 +339,7 @@ const baseTheme = EditorView.theme({ ".cm-content": { fontFamily: "var(--font-family-mono)", padding: "0", + paddingRight: "400px", }, "&.cm-focused": { outline: "none", @@ -350,8 +351,9 @@ const baseTheme = EditorView.theme({ fontFamily: "var(--font-family-mono)", }, ".cm-gutters": { - backgroundColor: "transparent", + backgroundColor: "var(--color-bg-primary)", border: "none", + boxShadow: "4px 0 6px var(--color-bg-primary)", }, ".cm-lineNumbers": { minWidth: "3.5ch", @@ -459,6 +461,7 @@ const readOnlyTheme = EditorView.theme({ ".cm-content": { fontFamily: "var(--font-family-mono)", padding: "0", + paddingRight: "400px", }, "&.cm-focused": { outline: "none", From 97014604dcec8551ea94a2c2f46da658f73fae9b Mon Sep 17 00:00:00 2001 From: Panthevm Date: Tue, 16 Jun 2026 15:19:34 +0300 Subject: [PATCH 46/55] aidbox-client: explicit Authorization header takes precedence over session cookie When a request carries an explicit Authorization header (e.g. from a REST notebook cell), treat it as the sole credential: don't attach the session cookie (credentials: omit) and don't redirect to login on 401, so the caller sees the real response. --- packages/aidbox-client/src/auth-providers.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/aidbox-client/src/auth-providers.ts b/packages/aidbox-client/src/auth-providers.ts index ebd41e45..d3acebea 100644 --- a/packages/aidbox-client/src/auth-providers.ts +++ b/packages/aidbox-client/src/auth-providers.ts @@ -1,6 +1,17 @@ import type { AuthProvider } from "./types"; import { mergeHeaders, validateBaseUrl } from "./utils"; +function hasAuthorizationHeader(headers: HeadersInit | undefined): boolean { + if (!headers) return false; + if (headers instanceof Headers) return headers.has("authorization"); + if (Array.isArray(headers)) { + return headers.some(([key]) => key.toLowerCase() === "authorization"); + } + return Object.keys(headers).some( + (key) => key.toLowerCase() === "authorization", + ); +} + export class BrowserAuthProvider implements AuthProvider { /** @ignore */ public baseUrl: string; @@ -59,16 +70,16 @@ export class BrowserAuthProvider implements AuthProvider { validateBaseUrl(input, this.baseUrl); const requestInit = init ?? {}; - requestInit.credentials = "include"; + const explicitAuth = hasAuthorizationHeader(requestInit.headers); + requestInit.credentials = explicitAuth ? "omit" : "include"; const response = await fetch(input, requestInit); - if (response.status === 401) { + if (response.status === 401 && !explicitAuth) { await this.establishSession(); throw new Error("unauthorized"); - } else { - return response; } + return response; } } From 2c61a84de25bbff3aa078dea9902fa5236cfe8ba Mon Sep 17 00:00:00 2001 From: Panthevm Date: Tue, 16 Jun 2026 20:30:30 +0300 Subject: [PATCH 47/55] code-editor: add placeholder prop --- .../src/components/code-editor/index.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/react-components/src/components/code-editor/index.tsx b/packages/react-components/src/components/code-editor/index.tsx index f1162cc2..e7b44e34 100644 --- a/packages/react-components/src/components/code-editor/index.tsx +++ b/packages/react-components/src/components/code-editor/index.tsx @@ -53,6 +53,7 @@ import { highlightSpecialChars, keymap, lineNumbers, + placeholder, rectangularSelection, type ViewUpdate, } from "@codemirror/view"; @@ -1365,6 +1366,7 @@ type CodeEditorProps = { isReadOnlyTheme?: boolean; defaultValue?: string; currentValue?: string; + placeholder?: string; onChange?: (value: string) => void; onUpdate?: (update: ViewUpdate) => void; id?: string; @@ -1398,6 +1400,7 @@ export type { export function CodeEditor({ defaultValue, currentValue, + placeholder: placeholderText, onChange, onUpdate, viewCallback, @@ -1441,6 +1444,7 @@ export function CodeEditor({ const sqlCompletionCompartment = React.useRef(new Compartment()); const fhirCompletionCompartment = React.useRef(new Compartment()); const vimCompartment = React.useRef(new Compartment()); + const placeholderCompartment = React.useRef(new Compartment()); const [sqlFunctions, setSqlFunctions] = React.useState< string[] | undefined >(); @@ -1556,6 +1560,7 @@ export function CodeEditor({ additionalExtensionsCompartment.current.of([]), sqlCompletionCompartment.current.of([]), fhirCompletionCompartment.current.of([]), + placeholderCompartment.current.of([]), ], }), }); @@ -1746,6 +1751,19 @@ export function CodeEditor({ }); }, [additionalExtensions, view, safeDispatch]); + React.useEffect(() => { + if (view === null) { + return; + } + safeDispatch({ + effects: [ + placeholderCompartment.current.reconfigure( + placeholderText ? placeholder(placeholderText) : [], + ), + ], + }); + }, [placeholderText, view, safeDispatch]); + React.useEffect(() => { if (view === null) { return; From e1608e4be6d680a66ad6c986388880223584bab2 Mon Sep 17 00:00:00 2001 From: Panthevm Date: Wed, 17 Jun 2026 13:05:52 +0300 Subject: [PATCH 48/55] aidbox-client: follow server redirect on unauthenticated requests A gated instance (unactivated license, or an SSO login gate) answers navigation with a 302 to an HTML page instead of 401. fetch follows the redirect, so the UI never saw a 401 and did not redirect. Now a redirected response on a request without an explicit Authorization header navigates the browser to the final URL, surfacing the login/activation page. --- packages/aidbox-client/src/auth-providers.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/aidbox-client/src/auth-providers.ts b/packages/aidbox-client/src/auth-providers.ts index d3acebea..91cb5d20 100644 --- a/packages/aidbox-client/src/auth-providers.ts +++ b/packages/aidbox-client/src/auth-providers.ts @@ -75,9 +75,15 @@ export class BrowserAuthProvider implements AuthProvider { const response = await fetch(input, requestInit); - if (response.status === 401 && !explicitAuth) { - await this.establishSession(); - throw new Error("unauthorized"); + if (!explicitAuth) { + if (response.status === 401) { + await this.establishSession(); + throw new Error("unauthorized"); + } + if (response.redirected) { + window.location.href = response.url; + throw new Error("unauthorized"); + } } return response; } From 2465245b1b012f69d7f436e17f9ee6686bb5bec6 Mon Sep 17 00:00:00 2001 From: Panthevm Date: Wed, 17 Jun 2026 19:20:10 +0300 Subject: [PATCH 49/55] code-editor: Tab/Shift-Tab indent instead of moving focus --- .../src/components/code-editor/index.tsx | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/packages/react-components/src/components/code-editor/index.tsx b/packages/react-components/src/components/code-editor/index.tsx index e7b44e34..c8451119 100644 --- a/packages/react-components/src/components/code-editor/index.tsx +++ b/packages/react-components/src/components/code-editor/index.tsx @@ -8,7 +8,13 @@ import { completionStatus, moveCompletionSelection, } from "@codemirror/autocomplete"; -import { defaultKeymap, history, historyKeymap } from "@codemirror/commands"; +import { + defaultKeymap, + history, + historyKeymap, + indentLess, + insertTab, +} from "@codemirror/commands"; import { json, jsonParseLinter } from "@codemirror/lang-json"; import { SQLDialect, sql } from "@codemirror/lang-sql"; import { yaml } from "@codemirror/lang-yaml"; @@ -18,6 +24,7 @@ import { foldKeymap, HighlightStyle, indentOnInput, + indentUnit, syntaxHighlighting, syntaxTree, } from "@codemirror/language"; @@ -43,6 +50,7 @@ import { StateField, } from "@codemirror/state"; import { + type Command, crosshairCursor, Decoration, drawSelection, @@ -443,6 +451,19 @@ const completionTheme = EditorView.theme({ }, }); +const smartIndentLess: Command = (view) => { + const { state } = view; + const sel = state.selection.main; + if (!sel.empty) return indentLess(view); + const line = state.doc.lineAt(sel.head); + const firstNonWs = line.text.search(/\S/); + const cursorCol = sel.head - line.from; + if (firstNonWs === -1 || cursorCol <= firstNonWs) { + return indentLess(view); + } + return true; +}; + const readOnlyTheme = EditorView.theme({ "&": { backgroundColor: "var(--color-bg-secondary)", @@ -1488,6 +1509,7 @@ export function CodeEditor({ dropCursor(), EditorState.allowMultipleSelections.of(true), indentOnInput(), + indentUnit.of("\t"), languageCompartment.current.of([]), bracketMatching(), closeBrackets(), @@ -1517,7 +1539,8 @@ export function CodeEditor({ if (completionStatus(v.state) === "active") { return moveCompletionSelection(true)(v); } - return false; + if (v.state.readOnly) return false; + return insertTab(v); }, }, { @@ -1526,7 +1549,8 @@ export function CodeEditor({ if (completionStatus(v.state) === "active") { return moveCompletionSelection(false)(v); } - return false; + if (v.state.readOnly) return false; + return smartIndentLess(v); }, }, { From b731d769dc14d3a3ba4453db51497fb0168a2e4e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 04:33:11 +0000 Subject: [PATCH 50/55] chore(deps): bump actions/checkout from 6 to 7 Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/aidbox-client.yaml | 2 +- .github/workflows/common.yaml | 6 +++--- .github/workflows/pages.yaml | 2 +- .github/workflows/release.yaml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/aidbox-client.yaml b/.github/workflows/aidbox-client.yaml index be4e465d..3294c52a 100644 --- a/.github/workflows/aidbox-client.yaml +++ b/.github/workflows/aidbox-client.yaml @@ -11,7 +11,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} diff --git a/.github/workflows/common.yaml b/.github/workflows/common.yaml index 709bf7c0..c0eef0b1 100644 --- a/.github/workflows/common.yaml +++ b/.github/workflows/common.yaml @@ -8,7 +8,7 @@ jobs: lint: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Install pnpm uses: pnpm/action-setup@v6 - name: Install node @@ -23,7 +23,7 @@ jobs: audit: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Install pnpm uses: pnpm/action-setup@v6 - name: Install node @@ -38,7 +38,7 @@ jobs: typecheck: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Install pnpm uses: pnpm/action-setup@v6 - name: Install node diff --git a/.github/workflows/pages.yaml b/.github/workflows/pages.yaml index c7746440..f560f90e 100644 --- a/.github/workflows/pages.yaml +++ b/.github/workflows/pages.yaml @@ -6,7 +6,7 @@ jobs: build-sites: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Install pnpm uses: pnpm/action-setup@v6 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 35291f91..9b6a1fa0 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -21,7 +21,7 @@ jobs: if: github.repository == 'HealthSamurai/aidbox-ts-sdk' && github.event_name == 'workflow_dispatch' runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Install pnpm uses: pnpm/action-setup@v6 - name: Install node From 47d2cd1312505c725672d85071fdec8faab2bab9 Mon Sep 17 00:00:00 2001 From: Panthevm Date: Wed, 24 Jun 2026 16:18:39 +0300 Subject: [PATCH 51/55] TableHead: support reverseIcon and contentStyle props --- .../src/shadcn/components/ui/table.tsx | 40 +++++++++++++++---- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/packages/react-components/src/shadcn/components/ui/table.tsx b/packages/react-components/src/shadcn/components/ui/table.tsx index 8d1ff534..e0d6ddd3 100644 --- a/packages/react-components/src/shadcn/components/ui/table.tsx +++ b/packages/react-components/src/shadcn/components/ui/table.tsx @@ -198,12 +198,16 @@ function TableRow({ type TableHeadProps = React.ComponentProps<"th"> & { sortable?: boolean | undefined; sorted?: "asc" | "desc" | false | undefined; + reverseIcon?: boolean | undefined; + contentStyle?: React.CSSProperties | undefined; }; function TableHead({ className, sortable = false, sorted = false, + reverseIcon = false, + contentStyle, children, ...props }: TableHeadProps) { @@ -214,25 +218,45 @@ function TableHead({ ? ArrowDownIcon : ArrowUpDownIcon; + const sortIcon = ( + + ); + return ( {sortable ? ( -
- {children} - +
+ {reverseIcon ? ( + <> + {sortIcon} + {children} + + ) : ( + <> + {children} + {sortIcon} + + )}
) : ( children From 28218100b60807a70c1260be431a731bb55a10d5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 04:33:21 +0000 Subject: [PATCH 52/55] chore(deps): bump actions/setup-node from 6 to 7 Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6 to 7. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/aidbox-client.yaml | 2 +- .github/workflows/common.yaml | 6 +++--- .github/workflows/pages.yaml | 2 +- .github/workflows/release.yaml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/aidbox-client.yaml b/.github/workflows/aidbox-client.yaml index 3294c52a..20797a7a 100644 --- a/.github/workflows/aidbox-client.yaml +++ b/.github/workflows/aidbox-client.yaml @@ -18,7 +18,7 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@v6 - name: Install node - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version: 24 cache: "pnpm" diff --git a/.github/workflows/common.yaml b/.github/workflows/common.yaml index c0eef0b1..353693cb 100644 --- a/.github/workflows/common.yaml +++ b/.github/workflows/common.yaml @@ -12,7 +12,7 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@v6 - name: Install node - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version: 24 cache: "pnpm" @@ -27,7 +27,7 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@v6 - name: Install node - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version: 24 cache: "pnpm" @@ -42,7 +42,7 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@v6 - name: Install node - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version: 24 cache: "pnpm" diff --git a/.github/workflows/pages.yaml b/.github/workflows/pages.yaml index f560f90e..9bb87277 100644 --- a/.github/workflows/pages.yaml +++ b/.github/workflows/pages.yaml @@ -12,7 +12,7 @@ jobs: uses: pnpm/action-setup@v6 - name: Install node - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version: 24 cache: "pnpm" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 9b6a1fa0..80748da2 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -25,7 +25,7 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@v6 - name: Install node - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version: 24 cache: "pnpm" From 6e75111fa388d693a98e77bf287441539b254d55 Mon Sep 17 00:00:00 2001 From: Andrey Listopadov Date: Wed, 22 Jul 2026 12:09:42 +0300 Subject: [PATCH 53/55] Fix brace-expansion override for minimatch compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The merge pinned brace-expansion to ^5.0.7 (from master's audit fix), but 5.x is ESM-only with named exports, breaking minimatch@9.0.9 which does a default import (`import expand from 'brace-expansion'`) — e.g. typedoc's `doc` script failed. The tree now has both minimatch@9 (needs 2.x) and minimatch@10 (needs 5.x), so no single major works. Replace the blanket pin with per-major security patches covering only the vulnerable ranges (GHSA-v6h2-p8h4-qcjw), letting each consumer resolve its compatible major: brace-expansion 2.1.2 for minimatch@9, 5.0.7 for minimatch@10. --- package.json | 5 ++++- pnpm-lock.yaml | 19 +++++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index d508a0cd..8b8c2a86 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,10 @@ "yaml": ">=2.8.3", "picomatch": ">=4.0.4", "file-type": ">=21.3.2", - "brace-expansion": "^5.0.7", + "brace-expansion@<1.1.12": "1.1.12", + "brace-expansion@>=2.0.0 <2.0.2": "2.0.2", + "brace-expansion@>=3.0.0 <3.0.1": "3.0.1", + "brace-expansion@>=4.0.0 <4.0.1": "4.0.1", "defu": ">=6.1.5", "vite": "^8.1.5", "fast-xml-parser@<5.7.0": ">=5.7.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ab744886..80cb01aa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,10 @@ overrides: yaml: '>=2.8.3' picomatch: '>=4.0.4' file-type: '>=21.3.2' - brace-expansion: ^5.0.7 + brace-expansion@<1.1.12: 1.1.12 + brace-expansion@>=2.0.0 <2.0.2: 2.0.2 + brace-expansion@>=3.0.0 <3.0.1: 3.0.1 + brace-expansion@>=4.0.0 <4.0.1: 4.0.1 defu: '>=6.1.5' vite: ^8.1.5 fast-xml-parser@<5.7.0: '>=5.7.0' @@ -3139,6 +3142,9 @@ packages: react-native-b4a: optional: true + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} @@ -3204,6 +3210,9 @@ packages: birpc@4.0.0: resolution: {integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==} + brace-expansion@2.1.2: + resolution: {integrity: sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==} + brace-expansion@5.0.7: resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} engines: {node: 18 || 20 || >=22} @@ -7443,6 +7452,8 @@ snapshots: b4a@1.8.0: {} + balanced-match@1.0.2: {} + balanced-match@4.0.4: {} bare-events@2.8.2: {} @@ -7497,6 +7508,10 @@ snapshots: birpc@4.0.0: {} + brace-expansion@2.1.2: + dependencies: + balanced-match: 1.0.2 + brace-expansion@5.0.7: dependencies: balanced-match: 4.0.4 @@ -8213,7 +8228,7 @@ snapshots: minimatch@9.0.9: dependencies: - brace-expansion: 5.0.7 + brace-expansion: 2.1.2 minimist@1.2.8: {} From 484b967fd576638120d3eff0d4bbf1449847cb5e Mon Sep 17 00:00:00 2001 From: Panthevm Date: Wed, 29 Jul 2026 18:07:24 +0300 Subject: [PATCH 54/55] code-editor: indent with two spaces instead of tabs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit indentUnit was "\t", so getIndentUnit() resolved to tabSize (4) while every document in the app is pretty-printed with 2 spaces — JSON.stringify(x, null, 2) and YAML.dump({indent: 2}). continuedIndent() therefore asked for 4 columns and indentString() materialized them as a literal tab: pressing Enter inside a JSON object put the caret one level too deep and mixed tabs into space-indented content. Tab had the same problem through insertTab, which always inserts "\t" — invalid indentation in YAML. Use indentMore so Tab follows indentUnit, matching Enter and the existing Shift-Tab (indentLess) behaviour. --- .../react-components/src/components/code-editor/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-components/src/components/code-editor/index.tsx b/packages/react-components/src/components/code-editor/index.tsx index c8451119..01f2ace9 100644 --- a/packages/react-components/src/components/code-editor/index.tsx +++ b/packages/react-components/src/components/code-editor/index.tsx @@ -13,7 +13,7 @@ import { history, historyKeymap, indentLess, - insertTab, + indentMore, } from "@codemirror/commands"; import { json, jsonParseLinter } from "@codemirror/lang-json"; import { SQLDialect, sql } from "@codemirror/lang-sql"; @@ -1509,7 +1509,7 @@ export function CodeEditor({ dropCursor(), EditorState.allowMultipleSelections.of(true), indentOnInput(), - indentUnit.of("\t"), + indentUnit.of(" "), languageCompartment.current.of([]), bracketMatching(), closeBrackets(), @@ -1540,7 +1540,7 @@ export function CodeEditor({ return moveCompletionSelection(true)(v); } if (v.state.readOnly) return false; - return insertTab(v); + return indentMore(v); }, }, { From 6a29dc05a950372f997f67003fe85364f27ecce5 Mon Sep 17 00:00:00 2001 From: Aleksandr Kislitsyn <40058255+aleksandrkislitsyn@users.noreply.github.com> Date: Wed, 5 Aug 2026 14:28:25 +0200 Subject: [PATCH 55/55] fix(code-editor): allow extension on nested elements (onto development) (#151) * fix(code-editor): allow extension on nested elements resolveElements did not include the universal element properties (id, extension, and modifierExtension for BackboneElements) for nested backbone or complex elements, because a StructureDefinition differential does not repeat inherited base-type elements. This caused the FHIR editor to flag a valid `extension` on e.g. QuestionnaireResponse.item or Patient.contact as an "Unknown property", and to omit it from autocomplete. Append the universal properties to resolveElements output (when not already present), gated by whether the context is a BackboneElement/resource (modifierExtension) or a plain complex datatype (id/extension only). Re-applies aidbox-ts-sdk#150 onto development (the branch aidbox-ui tracks); the original landed on the stale master branch. Fixes HealthSamurai/sansara#8121 Co-Authored-By: Aleksandr Kislitsyn Co-Authored-By: Claude Opus 4.8 (1M context) * chore(deps): bump brace-expansion overrides to patched versions CI `pnpm audit --audit-level=high` flagged brace-expansion advisories (GHSA-mh99-v99m-4gvg, GHSA-rgw5-rvv9-x895) in transitive deps via @swc/cli>minimatch and @storybook/react-*. The existing overrides did not cover the vulnerable ranges. Override brace-expansion 2.x to >=2.1.4 and 3.x-5.x to >=5.0.9. Audit at --audit-level=high now passes. Co-Authored-By: Aleksandr Kislitsyn Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- package.json | 5 ++- .../code-editor/fhir-autocomplete.ts | 34 +++++++++++++++++++ pnpm-lock.yaml | 23 ++++++------- 3 files changed, 47 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 8b8c2a86..8dac487e 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,8 @@ "picomatch": ">=4.0.4", "file-type": ">=21.3.2", "brace-expansion@<1.1.12": "1.1.12", - "brace-expansion@>=2.0.0 <2.0.2": "2.0.2", - "brace-expansion@>=3.0.0 <3.0.1": "3.0.1", - "brace-expansion@>=4.0.0 <4.0.1": "4.0.1", + "brace-expansion@>=2.0.0 <2.1.4": "2.1.4", + "brace-expansion@>=3.0.0 <5.0.9": "5.0.9", "defu": ">=6.1.5", "vite": "^8.1.5", "fast-xml-parser@<5.7.0": ">=5.7.0", diff --git a/packages/react-components/src/components/code-editor/fhir-autocomplete.ts b/packages/react-components/src/components/code-editor/fhir-autocomplete.ts index 37b42f46..eb0cd006 100644 --- a/packages/react-components/src/components/code-editor/fhir-autocomplete.ts +++ b/packages/react-components/src/components/code-editor/fhir-autocomplete.ts @@ -315,6 +315,10 @@ async function resolveElements( let currentPath = resourceType; let currentElements = result.elements; + // Whether the element the path currently points into is a BackboneElement + // (or a resource, at the root). BackboneElements and resources allow + // `modifierExtension`; plain complex datatypes (e.g. HumanName) do not. + let isBackbone = path.length === 0; for (const key of path) { if (key === "resourceType") return []; @@ -324,6 +328,7 @@ async function resolveElements( if (el.contentReference) { currentPath = el.contentReference.replace(/^#/, ""); + isBackbone = true; continue; } @@ -332,6 +337,7 @@ async function resolveElements( if (typeCode === "BackboneElement") { currentPath = el.path; + isBackbone = true; continue; } @@ -339,6 +345,7 @@ async function resolveElements( if (!typeResult) return []; currentPath = typeResult.basePath; currentElements = typeResult.elements; + isBackbone = false; } const children = directChildren(currentElements, currentPath); @@ -362,9 +369,36 @@ async function resolveElements( } } + // Every FHIR element allows `id` and `extension`; BackboneElements and + // resources additionally allow `modifierExtension`. StructureDefinition + // differentials don't repeat these inherited base-type elements for nested + // backbone/complex elements, so add them here (unless already present) so + // both autocomplete and validation see them. + appendUniversalElements(expanded, currentPath, isBackbone); + return expanded; } +function appendUniversalElements( + elements: FhirElement[], + basePath: string, + isBackbone: boolean, +): void { + const present = new Set(elements.map((el) => fieldName(el))); + const universal: { name: string; type: string }[] = [ + { name: "id", type: "string" }, + { name: "extension", type: "Extension" }, + ]; + if (isBackbone) { + universal.push({ name: "modifierExtension", type: "Extension" }); + } + for (const { name, type } of universal) { + if (!present.has(name)) { + elements.push({ path: `${basePath}.${name}`, type: [{ code: type }] }); + } + } +} + async function findResourceBoundary( path: string[], resourceType: string, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 80cb01aa..e47fac38 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,9 +11,8 @@ overrides: picomatch: '>=4.0.4' file-type: '>=21.3.2' brace-expansion@<1.1.12: 1.1.12 - brace-expansion@>=2.0.0 <2.0.2: 2.0.2 - brace-expansion@>=3.0.0 <3.0.1: 3.0.1 - brace-expansion@>=4.0.0 <4.0.1: 4.0.1 + brace-expansion@>=2.0.0 <2.1.4: 2.1.4 + brace-expansion@>=3.0.0 <5.0.9: 5.0.9 defu: '>=6.1.5' vite: ^8.1.5 fast-xml-parser@<5.7.0: '>=5.7.0' @@ -3210,12 +3209,12 @@ packages: birpc@4.0.0: resolution: {integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==} - brace-expansion@2.1.2: - resolution: {integrity: sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==} + brace-expansion@2.1.4: + resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==} - brace-expansion@5.0.7: - resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} - engines: {node: 18 || 20 || >=22} + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + engines: {node: 20 || >=22} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} @@ -7508,11 +7507,11 @@ snapshots: birpc@4.0.0: {} - brace-expansion@2.1.2: + brace-expansion@2.1.4: dependencies: balanced-match: 1.0.2 - brace-expansion@5.0.7: + brace-expansion@5.0.9: dependencies: balanced-match: 4.0.4 @@ -8224,11 +8223,11 @@ snapshots: minimatch@10.2.4: dependencies: - brace-expansion: 5.0.7 + brace-expansion: 5.0.9 minimatch@9.0.9: dependencies: - brace-expansion: 2.1.2 + brace-expansion: 2.1.4 minimist@1.2.8: {}