@health-samurai/react-components 0.0.0-alpha.2 → 0.0.0-alpha.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (571) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +102 -1
  3. package/dist/bundle.css +2349 -754
  4. package/dist/src/components/button-dropdown.d.ts +10 -0
  5. package/dist/src/components/button-dropdown.d.ts.map +1 -0
  6. package/dist/src/components/button-dropdown.js +70 -0
  7. package/dist/src/components/button-dropdown.js.map +1 -0
  8. package/dist/src/components/button-dropdown.stories.js +48 -0
  9. package/dist/src/components/button-dropdown.stories.js.map +1 -0
  10. package/dist/src/components/code-editor/fhir-autocomplete.d.ts +70 -0
  11. package/dist/src/components/code-editor/fhir-autocomplete.d.ts.map +1 -0
  12. package/dist/src/components/code-editor/fhir-autocomplete.js +1850 -0
  13. package/dist/src/components/code-editor/fhir-autocomplete.js.map +1 -0
  14. package/dist/src/components/code-editor/fhir-autocomplete.test.js +1099 -0
  15. package/dist/src/components/code-editor/fhir-autocomplete.test.js.map +1 -0
  16. package/dist/src/components/code-editor/http/grammar/http.d.ts +3 -0
  17. package/dist/src/components/code-editor/http/grammar/http.d.ts.map +1 -0
  18. package/dist/src/components/code-editor/http/grammar/http.grammar +74 -0
  19. package/dist/src/components/code-editor/http/grammar/http.js +38 -0
  20. package/dist/src/components/code-editor/http/grammar/http.js.map +1 -0
  21. package/dist/src/components/code-editor/http/grammar/http.terms.d.ts +2 -0
  22. package/dist/src/components/code-editor/http/grammar/http.terms.d.ts.map +1 -0
  23. package/dist/src/components/code-editor/http/grammar/http.terms.js +4 -0
  24. package/dist/src/components/code-editor/http/grammar/http.terms.js.map +1 -0
  25. package/dist/src/components/code-editor/http/grammar/http.test.js +80 -0
  26. package/dist/src/components/code-editor/http/grammar/http.test.js.map +1 -0
  27. package/dist/src/components/code-editor/http/index.d.ts +12 -0
  28. package/dist/src/components/code-editor/http/index.d.ts.map +1 -0
  29. package/dist/src/components/code-editor/http/index.js +486 -0
  30. package/dist/src/components/code-editor/http/index.js.map +1 -0
  31. package/dist/src/components/code-editor/index.d.ts +39 -1
  32. package/dist/src/components/code-editor/index.d.ts.map +1 -1
  33. package/dist/src/components/code-editor/index.js +1792 -45
  34. package/dist/src/components/code-editor/index.js.map +1 -1
  35. package/dist/src/components/code-editor/json-ast.d.ts +46 -0
  36. package/dist/src/components/code-editor/json-ast.d.ts.map +1 -0
  37. package/dist/src/components/code-editor/json-ast.js +465 -0
  38. package/dist/src/components/code-editor/json-ast.js.map +1 -0
  39. package/dist/src/components/code-editor/json-ast.test.js +206 -0
  40. package/dist/src/components/code-editor/json-ast.test.js.map +1 -0
  41. package/dist/src/components/code-editor/sql-completion.d.ts +22 -0
  42. package/dist/src/components/code-editor/sql-completion.d.ts.map +1 -0
  43. package/dist/src/components/code-editor/sql-completion.js +897 -0
  44. package/dist/src/components/code-editor/sql-completion.js.map +1 -0
  45. package/dist/src/components/code-editor.stories.js +280 -3
  46. package/dist/src/components/code-editor.stories.js.map +1 -1
  47. package/dist/src/components/copy-icon.d.ts +5 -1
  48. package/dist/src/components/copy-icon.d.ts.map +1 -1
  49. package/dist/src/components/copy-icon.js +41 -3
  50. package/dist/src/components/copy-icon.js.map +1 -1
  51. package/dist/src/components/data-table.d.ts +9 -0
  52. package/dist/src/components/data-table.d.ts.map +1 -0
  53. package/dist/src/components/data-table.js +66 -0
  54. package/dist/src/components/data-table.js.map +1 -0
  55. package/dist/src/components/data-table.stories.js +240 -0
  56. package/dist/src/components/data-table.stories.js.map +1 -0
  57. package/dist/src/components/date-picker-input.d.ts +10 -0
  58. package/dist/src/components/date-picker-input.d.ts.map +1 -0
  59. package/dist/src/components/date-picker-input.js +90 -0
  60. package/dist/src/components/date-picker-input.js.map +1 -0
  61. package/dist/src/components/date-picker-input.stories.js +76 -0
  62. package/dist/src/components/date-picker-input.stories.js.map +1 -0
  63. package/dist/src/components/fhir-structure-view.d.ts +34 -0
  64. package/dist/src/components/fhir-structure-view.d.ts.map +1 -0
  65. package/dist/src/components/fhir-structure-view.js +230 -0
  66. package/dist/src/components/fhir-structure-view.js.map +1 -0
  67. package/dist/src/components/fhir-structure-view.stories.js +447 -0
  68. package/dist/src/components/fhir-structure-view.stories.js.map +1 -0
  69. package/dist/src/components/icon-button.d.ts +12 -0
  70. package/dist/src/components/icon-button.d.ts.map +1 -0
  71. package/dist/src/components/icon-button.js +41 -0
  72. package/dist/src/components/icon-button.js.map +1 -0
  73. package/dist/src/components/icon-button.stories.js +157 -0
  74. package/dist/src/components/icon-button.stories.js.map +1 -0
  75. package/dist/src/components/operation-outcome-view.d.ts +27 -0
  76. package/dist/src/components/operation-outcome-view.d.ts.map +1 -0
  77. package/dist/src/components/operation-outcome-view.js +198 -0
  78. package/dist/src/components/operation-outcome-view.js.map +1 -0
  79. package/dist/src/components/operation-outcome-view.stories.js +207 -0
  80. package/dist/src/components/operation-outcome-view.stories.js.map +1 -0
  81. package/dist/src/components/request-line-editor.d.ts +13 -35
  82. package/dist/src/components/request-line-editor.d.ts.map +1 -1
  83. package/dist/src/components/request-line-editor.js +73 -49
  84. package/dist/src/components/request-line-editor.js.map +1 -1
  85. package/dist/src/components/request-line-editor.stories.js +17 -53
  86. package/dist/src/components/request-line-editor.stories.js.map +1 -1
  87. package/dist/src/components/sandbox.d.ts +13 -0
  88. package/dist/src/components/sandbox.d.ts.map +1 -0
  89. package/dist/src/components/sandbox.js +107 -0
  90. package/dist/src/components/sandbox.js.map +1 -0
  91. package/dist/src/components/sandbox.stories.js +126 -0
  92. package/dist/src/components/sandbox.stories.js.map +1 -0
  93. package/dist/src/components/segment-control.d.ts +13 -0
  94. package/dist/src/components/segment-control.d.ts.map +1 -0
  95. package/dist/src/components/segment-control.js +33 -0
  96. package/dist/src/components/segment-control.js.map +1 -0
  97. package/dist/src/components/segment-control.stories.js +68 -0
  98. package/dist/src/components/segment-control.stories.js.map +1 -0
  99. package/dist/src/components/split-button.d.ts +12 -0
  100. package/dist/src/components/split-button.d.ts.map +1 -0
  101. package/dist/src/components/split-button.js +33 -0
  102. package/dist/src/components/split-button.js.map +1 -0
  103. package/dist/src/components/split-button.stories.js +84 -0
  104. package/dist/src/components/split-button.stories.js.map +1 -0
  105. package/dist/src/components/tag.d.ts +16 -0
  106. package/dist/src/components/tag.d.ts.map +1 -0
  107. package/dist/src/components/tag.js +198 -0
  108. package/dist/src/components/tag.js.map +1 -0
  109. package/dist/src/components/tag.stories.js +459 -0
  110. package/dist/src/components/tag.stories.js.map +1 -0
  111. package/dist/src/components/tile.d.ts +15 -0
  112. package/dist/src/components/tile.d.ts.map +1 -0
  113. package/dist/src/components/tile.js +76 -0
  114. package/dist/src/components/tile.js.map +1 -0
  115. package/dist/src/components/tile.stories.js +167 -0
  116. package/dist/src/components/tile.stories.js.map +1 -0
  117. package/dist/src/components/toolbar.d.ts +18 -0
  118. package/dist/src/components/toolbar.d.ts.map +1 -0
  119. package/dist/src/components/toolbar.js +61 -0
  120. package/dist/src/components/toolbar.js.map +1 -0
  121. package/dist/src/components/toolbar.stories.js +69 -0
  122. package/dist/src/components/toolbar.stories.js.map +1 -0
  123. package/dist/src/components/tree-view.d.ts +47 -0
  124. package/dist/src/components/tree-view.d.ts.map +1 -0
  125. package/dist/src/components/tree-view.js +122 -0
  126. package/dist/src/components/tree-view.js.map +1 -0
  127. package/dist/src/components/tree-view.stories.js +283 -0
  128. package/dist/src/components/tree-view.stories.js.map +1 -0
  129. package/dist/src/icons.d.ts +11 -0
  130. package/dist/src/icons.d.ts.map +1 -0
  131. package/dist/src/icons.js +328 -0
  132. package/dist/src/icons.js.map +1 -0
  133. package/dist/src/index.css +358 -74
  134. package/dist/src/index.d.ts +17 -1
  135. package/dist/src/index.d.ts.map +1 -1
  136. package/dist/src/index.js +17 -1
  137. package/dist/src/index.js.map +1 -1
  138. package/dist/src/shadcn/components/ui/accordion.d.ts +2 -2
  139. package/dist/src/shadcn/components/ui/accordion.d.ts.map +1 -1
  140. package/dist/src/shadcn/components/ui/accordion.js +35 -9
  141. package/dist/src/shadcn/components/ui/accordion.js.map +1 -1
  142. package/dist/src/shadcn/components/ui/alert-dialog.d.ts +12 -4
  143. package/dist/src/shadcn/components/ui/alert-dialog.d.ts.map +1 -1
  144. package/dist/src/shadcn/components/ui/alert-dialog.js +128 -18
  145. package/dist/src/shadcn/components/ui/alert-dialog.js.map +1 -1
  146. package/dist/src/shadcn/components/ui/alert-dialog.stories.js +269 -19
  147. package/dist/src/shadcn/components/ui/alert-dialog.stories.js.map +1 -1
  148. package/dist/src/shadcn/components/ui/alert.d.ts +29 -6
  149. package/dist/src/shadcn/components/ui/alert.d.ts.map +1 -1
  150. package/dist/src/shadcn/components/ui/alert.js +50 -19
  151. package/dist/src/shadcn/components/ui/alert.js.map +1 -1
  152. package/dist/src/shadcn/components/ui/alert.stories.js +140 -36
  153. package/dist/src/shadcn/components/ui/alert.stories.js.map +1 -1
  154. package/dist/src/shadcn/components/ui/aspect-ratio.d.ts.map +1 -1
  155. package/dist/src/shadcn/components/ui/aspect-ratio.js +1 -0
  156. package/dist/src/shadcn/components/ui/aspect-ratio.js.map +1 -1
  157. package/dist/src/shadcn/components/ui/avatar.d.ts.map +1 -1
  158. package/dist/src/shadcn/components/ui/avatar.js +4 -3
  159. package/dist/src/shadcn/components/ui/avatar.js.map +1 -1
  160. package/dist/src/shadcn/components/ui/avatar.stories.js +68 -2
  161. package/dist/src/shadcn/components/ui/avatar.stories.js.map +1 -1
  162. package/dist/src/shadcn/components/ui/badge.d.ts +1 -1
  163. package/dist/src/shadcn/components/ui/badge.d.ts.map +1 -1
  164. package/dist/src/shadcn/components/ui/badge.js +16 -5
  165. package/dist/src/shadcn/components/ui/badge.js.map +1 -1
  166. package/dist/src/shadcn/components/ui/breadcrumb.d.ts +5 -2
  167. package/dist/src/shadcn/components/ui/breadcrumb.d.ts.map +1 -1
  168. package/dist/src/shadcn/components/ui/breadcrumb.js +98 -13
  169. package/dist/src/shadcn/components/ui/breadcrumb.js.map +1 -1
  170. package/dist/src/shadcn/components/ui/breadcrumb.stories.js +205 -45
  171. package/dist/src/shadcn/components/ui/breadcrumb.stories.js.map +1 -1
  172. package/dist/src/shadcn/components/ui/button.d.ts.map +1 -1
  173. package/dist/src/shadcn/components/ui/button.js +65 -11
  174. package/dist/src/shadcn/components/ui/button.js.map +1 -1
  175. package/dist/src/shadcn/components/ui/button.stories.js +99 -17
  176. package/dist/src/shadcn/components/ui/button.stories.js.map +1 -1
  177. package/dist/src/shadcn/components/ui/calendar.d.ts +1 -1
  178. package/dist/src/shadcn/components/ui/calendar.d.ts.map +1 -1
  179. package/dist/src/shadcn/components/ui/calendar.js +1 -0
  180. package/dist/src/shadcn/components/ui/calendar.js.map +1 -1
  181. package/dist/src/shadcn/components/ui/card.d.ts +5 -1
  182. package/dist/src/shadcn/components/ui/card.d.ts.map +1 -1
  183. package/dist/src/shadcn/components/ui/card.js +28 -7
  184. package/dist/src/shadcn/components/ui/card.js.map +1 -1
  185. package/dist/src/shadcn/components/ui/card.stories.js +23 -2
  186. package/dist/src/shadcn/components/ui/card.stories.js.map +1 -1
  187. package/dist/src/shadcn/components/ui/carousel.d.ts +1 -1
  188. package/dist/src/shadcn/components/ui/carousel.d.ts.map +1 -1
  189. package/dist/src/shadcn/components/ui/carousel.js +1 -0
  190. package/dist/src/shadcn/components/ui/carousel.js.map +1 -1
  191. package/dist/src/shadcn/components/ui/chart.d.ts +5 -5
  192. package/dist/src/shadcn/components/ui/chart.d.ts.map +1 -1
  193. package/dist/src/shadcn/components/ui/chart.js +4 -3
  194. package/dist/src/shadcn/components/ui/chart.js.map +1 -1
  195. package/dist/src/shadcn/components/ui/checkbox.d.ts +5 -1
  196. package/dist/src/shadcn/components/ui/checkbox.d.ts.map +1 -1
  197. package/dist/src/shadcn/components/ui/checkbox.js +46 -6
  198. package/dist/src/shadcn/components/ui/checkbox.js.map +1 -1
  199. package/dist/src/shadcn/components/ui/checkbox.stories.js +156 -46
  200. package/dist/src/shadcn/components/ui/checkbox.stories.js.map +1 -1
  201. package/dist/src/shadcn/components/ui/combobox.d.ts +29 -0
  202. package/dist/src/shadcn/components/ui/combobox.d.ts.map +1 -0
  203. package/dist/src/shadcn/components/ui/combobox.js +226 -0
  204. package/dist/src/shadcn/components/ui/combobox.js.map +1 -0
  205. package/dist/src/shadcn/components/ui/combobox.stories.js +167 -0
  206. package/dist/src/shadcn/components/ui/combobox.stories.js.map +1 -0
  207. package/dist/src/shadcn/components/ui/command.d.ts +4 -2
  208. package/dist/src/shadcn/components/ui/command.d.ts.map +1 -1
  209. package/dist/src/shadcn/components/ui/command.js +75 -13
  210. package/dist/src/shadcn/components/ui/command.js.map +1 -1
  211. package/dist/src/shadcn/components/ui/command.stories.js +277 -57
  212. package/dist/src/shadcn/components/ui/command.stories.js.map +1 -1
  213. package/dist/src/shadcn/components/ui/context-menu.d.ts +7 -3
  214. package/dist/src/shadcn/components/ui/context-menu.d.ts.map +1 -1
  215. package/dist/src/shadcn/components/ui/context-menu.js +120 -13
  216. package/dist/src/shadcn/components/ui/context-menu.js.map +1 -1
  217. package/dist/src/shadcn/components/ui/dialog.d.ts.map +1 -1
  218. package/dist/src/shadcn/components/ui/dialog.js +35 -7
  219. package/dist/src/shadcn/components/ui/dialog.js.map +1 -1
  220. package/dist/src/shadcn/components/ui/drawer.d.ts.map +1 -1
  221. package/dist/src/shadcn/components/ui/drawer.js +27 -5
  222. package/dist/src/shadcn/components/ui/drawer.js.map +1 -1
  223. package/dist/src/shadcn/components/ui/dropdown-menu.d.ts +7 -3
  224. package/dist/src/shadcn/components/ui/dropdown-menu.d.ts.map +1 -1
  225. package/dist/src/shadcn/components/ui/dropdown-menu.js +122 -14
  226. package/dist/src/shadcn/components/ui/dropdown-menu.js.map +1 -1
  227. package/dist/src/shadcn/components/ui/dropdown-menu.stories.js +22 -5
  228. package/dist/src/shadcn/components/ui/dropdown-menu.stories.js.map +1 -1
  229. package/dist/src/shadcn/components/ui/form.d.ts +2 -2
  230. package/dist/src/shadcn/components/ui/form.d.ts.map +1 -1
  231. package/dist/src/shadcn/components/ui/form.js +17 -8
  232. package/dist/src/shadcn/components/ui/form.js.map +1 -1
  233. package/dist/src/shadcn/components/ui/hover-card.d.ts.map +1 -1
  234. package/dist/src/shadcn/components/ui/hover-card.js +2 -1
  235. package/dist/src/shadcn/components/ui/hover-card.js.map +1 -1
  236. package/dist/src/shadcn/components/ui/input-otp.d.ts.map +1 -1
  237. package/dist/src/shadcn/components/ui/input-otp.js +1 -0
  238. package/dist/src/shadcn/components/ui/input-otp.js.map +1 -1
  239. package/dist/src/shadcn/components/ui/input.d.ts +3 -1
  240. package/dist/src/shadcn/components/ui/input.d.ts.map +1 -1
  241. package/dist/src/shadcn/components/ui/input.js +126 -17
  242. package/dist/src/shadcn/components/ui/input.js.map +1 -1
  243. package/dist/src/shadcn/components/ui/input.stories.js +218 -29
  244. package/dist/src/shadcn/components/ui/input.stories.js.map +1 -1
  245. package/dist/src/shadcn/components/ui/label.d.ts.map +1 -1
  246. package/dist/src/shadcn/components/ui/label.js +9 -1
  247. package/dist/src/shadcn/components/ui/label.js.map +1 -1
  248. package/dist/src/shadcn/components/ui/menubar.d.ts.map +1 -1
  249. package/dist/src/shadcn/components/ui/menubar.js +35 -13
  250. package/dist/src/shadcn/components/ui/menubar.js.map +1 -1
  251. package/dist/src/shadcn/components/ui/pagination.d.ts +9 -2
  252. package/dist/src/shadcn/components/ui/pagination.d.ts.map +1 -1
  253. package/dist/src/shadcn/components/ui/pagination.js +41 -24
  254. package/dist/src/shadcn/components/ui/pagination.js.map +1 -1
  255. package/dist/src/shadcn/components/ui/pagination.stories.js +44 -37
  256. package/dist/src/shadcn/components/ui/pagination.stories.js.map +1 -1
  257. package/dist/src/shadcn/components/ui/popover.d.ts.map +1 -1
  258. package/dist/src/shadcn/components/ui/popover.js +13 -1
  259. package/dist/src/shadcn/components/ui/popover.js.map +1 -1
  260. package/dist/src/shadcn/components/ui/progress.d.ts.map +1 -1
  261. package/dist/src/shadcn/components/ui/progress.js +6 -2
  262. package/dist/src/shadcn/components/ui/progress.js.map +1 -1
  263. package/dist/src/shadcn/components/ui/radio-button-group.d.ts +21 -0
  264. package/dist/src/shadcn/components/ui/radio-button-group.d.ts.map +1 -0
  265. package/dist/src/shadcn/components/ui/radio-button-group.js +148 -0
  266. package/dist/src/shadcn/components/ui/radio-button-group.js.map +1 -0
  267. package/dist/src/shadcn/components/ui/radio-button-group.stories.js +283 -0
  268. package/dist/src/shadcn/components/ui/radio-button-group.stories.js.map +1 -0
  269. package/dist/src/shadcn/components/ui/radio-group.d.ts +5 -1
  270. package/dist/src/shadcn/components/ui/radio-group.d.ts.map +1 -1
  271. package/dist/src/shadcn/components/ui/radio-group.js +40 -7
  272. package/dist/src/shadcn/components/ui/radio-group.js.map +1 -1
  273. package/dist/src/shadcn/components/ui/radio-group.stories.js +107 -32
  274. package/dist/src/shadcn/components/ui/radio-group.stories.js.map +1 -1
  275. package/dist/src/shadcn/components/ui/resizable.d.ts.map +1 -1
  276. package/dist/src/shadcn/components/ui/resizable.js +2 -1
  277. package/dist/src/shadcn/components/ui/resizable.js.map +1 -1
  278. package/dist/src/shadcn/components/ui/resizable.stories.js +2 -2
  279. package/dist/src/shadcn/components/ui/resizable.stories.js.map +1 -1
  280. package/dist/src/shadcn/components/ui/scroll-area.d.ts.map +1 -1
  281. package/dist/src/shadcn/components/ui/scroll-area.js +10 -3
  282. package/dist/src/shadcn/components/ui/scroll-area.js.map +1 -1
  283. package/dist/src/shadcn/components/ui/select.d.ts +1 -2
  284. package/dist/src/shadcn/components/ui/select.d.ts.map +1 -1
  285. package/dist/src/shadcn/components/ui/select.js +49 -19
  286. package/dist/src/shadcn/components/ui/select.js.map +1 -1
  287. package/dist/src/shadcn/components/ui/select.stories.js +193 -70
  288. package/dist/src/shadcn/components/ui/select.stories.js.map +1 -1
  289. package/dist/src/shadcn/components/ui/separator.d.ts.map +1 -1
  290. package/dist/src/shadcn/components/ui/separator.js +8 -1
  291. package/dist/src/shadcn/components/ui/separator.js.map +1 -1
  292. package/dist/src/shadcn/components/ui/sheet.js +1 -1
  293. package/dist/src/shadcn/components/ui/sheet.js.map +1 -1
  294. package/dist/src/shadcn/components/ui/sidebar.d.ts +4 -4
  295. package/dist/src/shadcn/components/ui/sidebar.d.ts.map +1 -1
  296. package/dist/src/shadcn/components/ui/sidebar.js +21 -6
  297. package/dist/src/shadcn/components/ui/sidebar.js.map +1 -1
  298. package/dist/src/shadcn/components/ui/skeleton.d.ts.map +1 -1
  299. package/dist/src/shadcn/components/ui/skeleton.js +3 -1
  300. package/dist/src/shadcn/components/ui/skeleton.js.map +1 -1
  301. package/dist/src/shadcn/components/ui/slider.d.ts.map +1 -1
  302. package/dist/src/shadcn/components/ui/slider.js +35 -4
  303. package/dist/src/shadcn/components/ui/slider.js.map +1 -1
  304. package/dist/src/shadcn/components/ui/sonner.d.ts +24 -2
  305. package/dist/src/shadcn/components/ui/sonner.d.ts.map +1 -1
  306. package/dist/src/shadcn/components/ui/sonner.js +127 -9
  307. package/dist/src/shadcn/components/ui/sonner.js.map +1 -1
  308. package/dist/src/shadcn/components/ui/sonner.stories.js +251 -12
  309. package/dist/src/shadcn/components/ui/sonner.stories.js.map +1 -1
  310. package/dist/src/shadcn/components/ui/switch.d.ts +7 -1
  311. package/dist/src/shadcn/components/ui/switch.d.ts.map +1 -1
  312. package/dist/src/shadcn/components/ui/switch.js +55 -3
  313. package/dist/src/shadcn/components/ui/switch.js.map +1 -1
  314. package/dist/src/shadcn/components/ui/switch.stories.js +84 -9
  315. package/dist/src/shadcn/components/ui/switch.stories.js.map +1 -1
  316. package/dist/src/shadcn/components/ui/table.d.ts +23 -6
  317. package/dist/src/shadcn/components/ui/table.d.ts.map +1 -1
  318. package/dist/src/shadcn/components/ui/table.js +65 -20
  319. package/dist/src/shadcn/components/ui/table.js.map +1 -1
  320. package/dist/src/shadcn/components/ui/table.stories.js +217 -97
  321. package/dist/src/shadcn/components/ui/table.stories.js.map +1 -1
  322. package/dist/src/shadcn/components/ui/tabs.d.ts +30 -5
  323. package/dist/src/shadcn/components/ui/tabs.d.ts.map +1 -1
  324. package/dist/src/shadcn/components/ui/tabs.js +470 -23
  325. package/dist/src/shadcn/components/ui/tabs.js.map +1 -1
  326. package/dist/src/shadcn/components/ui/tabs.stories.js +405 -181
  327. package/dist/src/shadcn/components/ui/tabs.stories.js.map +1 -1
  328. package/dist/src/shadcn/components/ui/textarea.d.ts +8 -1
  329. package/dist/src/shadcn/components/ui/textarea.d.ts.map +1 -1
  330. package/dist/src/shadcn/components/ui/textarea.js +30 -2
  331. package/dist/src/shadcn/components/ui/textarea.js.map +1 -1
  332. package/dist/src/shadcn/components/ui/textarea.stories.js +85 -4
  333. package/dist/src/shadcn/components/ui/textarea.stories.js.map +1 -1
  334. package/dist/src/shadcn/components/ui/toggle-group.d.ts +3 -3
  335. package/dist/src/shadcn/components/ui/toggle-group.d.ts.map +1 -1
  336. package/dist/src/shadcn/components/ui/toggle-group.js +14 -12
  337. package/dist/src/shadcn/components/ui/toggle-group.js.map +1 -1
  338. package/dist/src/shadcn/components/ui/toggle.d.ts +3 -4
  339. package/dist/src/shadcn/components/ui/toggle.d.ts.map +1 -1
  340. package/dist/src/shadcn/components/ui/toggle.js +44 -16
  341. package/dist/src/shadcn/components/ui/toggle.js.map +1 -1
  342. package/dist/src/shadcn/components/ui/toggle.stories.js +130 -7
  343. package/dist/src/shadcn/components/ui/toggle.stories.js.map +1 -1
  344. package/dist/src/shadcn/components/ui/tooltip.d.ts.map +1 -1
  345. package/dist/src/shadcn/components/ui/tooltip.js +12 -1
  346. package/dist/src/shadcn/components/ui/tooltip.js.map +1 -1
  347. package/dist/src/shadcn/components/ui/tree.d.ts +29 -0
  348. package/dist/src/shadcn/components/ui/tree.d.ts.map +1 -0
  349. package/dist/src/shadcn/components/ui/tree.js +135 -0
  350. package/dist/src/shadcn/components/ui/tree.js.map +1 -0
  351. package/dist/src/shadcn/shadcn.css +4 -4
  352. package/dist/src/tokens.css +50 -20
  353. package/dist/src/typography.css +78 -15
  354. package/package.json +84 -64
  355. package/src/components/button-dropdown.stories.tsx +41 -0
  356. package/src/components/button-dropdown.tsx +97 -0
  357. package/src/components/code-editor/fhir-autocomplete.test.ts +993 -0
  358. package/src/components/code-editor/fhir-autocomplete.ts +2322 -0
  359. package/src/components/code-editor/http/grammar/http.grammar +74 -0
  360. package/src/components/code-editor/http/grammar/http.terms.ts +9 -0
  361. package/src/components/code-editor/http/grammar/http.test.ts +110 -0
  362. package/src/components/code-editor/http/grammar/http.ts +21 -0
  363. package/src/components/code-editor/http/index.ts +424 -0
  364. package/src/components/code-editor/index.tsx +1944 -42
  365. package/src/components/code-editor/json-ast.test.ts +230 -0
  366. package/src/components/code-editor/json-ast.ts +590 -0
  367. package/src/components/code-editor/sql-completion.ts +1112 -0
  368. package/src/components/code-editor.stories.tsx +325 -2
  369. package/src/components/copy-icon.tsx +57 -3
  370. package/src/components/data-table.stories.tsx +91 -0
  371. package/src/components/data-table.tsx +126 -0
  372. package/src/components/date-picker-input.stories.tsx +79 -0
  373. package/src/components/date-picker-input.tsx +104 -0
  374. package/src/components/fhir-structure-view.stories.tsx +439 -0
  375. package/src/components/fhir-structure-view.tsx +233 -0
  376. package/src/components/icon-button.stories.tsx +86 -0
  377. package/src/components/icon-button.tsx +57 -0
  378. package/src/components/operation-outcome-view.stories.tsx +163 -0
  379. package/src/components/operation-outcome-view.tsx +254 -0
  380. package/src/components/request-line-editor.stories.tsx +17 -27
  381. package/src/components/request-line-editor.tsx +103 -61
  382. package/src/components/sandbox.stories.tsx +131 -0
  383. package/src/components/sandbox.tsx +191 -0
  384. package/src/components/segment-control.stories.tsx +61 -0
  385. package/src/components/segment-control.tsx +83 -0
  386. package/src/components/split-button.stories.tsx +68 -0
  387. package/src/components/split-button.tsx +74 -0
  388. package/src/components/tag.stories.tsx +371 -0
  389. package/src/components/tag.tsx +236 -0
  390. package/src/components/tile.stories.tsx +149 -0
  391. package/src/components/tile.tsx +105 -0
  392. package/src/components/toolbar.stories.tsx +64 -0
  393. package/src/components/toolbar.tsx +98 -0
  394. package/src/components/tree-view.stories.tsx +265 -0
  395. package/src/components/tree-view.tsx +246 -0
  396. package/src/icons.tsx +331 -0
  397. package/src/index.css +358 -74
  398. package/src/index.tsx +17 -3
  399. package/src/shadcn/components/ui/accordion.tsx +91 -10
  400. package/src/shadcn/components/ui/alert-dialog.stories.tsx +209 -15
  401. package/src/shadcn/components/ui/alert-dialog.tsx +236 -26
  402. package/src/shadcn/components/ui/alert.stories.tsx +120 -21
  403. package/src/shadcn/components/ui/alert.tsx +125 -28
  404. package/src/shadcn/components/ui/aspect-ratio.tsx +1 -0
  405. package/src/shadcn/components/ui/avatar.stories.tsx +74 -1
  406. package/src/shadcn/components/ui/avatar.tsx +22 -6
  407. package/src/shadcn/components/ui/badge.tsx +67 -18
  408. package/src/shadcn/components/ui/breadcrumb.stories.tsx +161 -41
  409. package/src/shadcn/components/ui/breadcrumb.tsx +172 -23
  410. package/src/shadcn/components/ui/button.stories.tsx +106 -18
  411. package/src/shadcn/components/ui/button.tsx +151 -55
  412. package/src/shadcn/components/ui/calendar.tsx +1 -0
  413. package/src/shadcn/components/ui/card.stories.tsx +17 -3
  414. package/src/shadcn/components/ui/card.tsx +89 -14
  415. package/src/shadcn/components/ui/carousel.tsx +1 -0
  416. package/src/shadcn/components/ui/chart.tsx +9 -5
  417. package/src/shadcn/components/ui/checkbox.stories.tsx +78 -30
  418. package/src/shadcn/components/ui/checkbox.tsx +91 -8
  419. package/src/shadcn/components/ui/combobox.stories.tsx +148 -0
  420. package/src/shadcn/components/ui/combobox.tsx +324 -0
  421. package/src/shadcn/components/ui/command.stories.tsx +184 -39
  422. package/src/shadcn/components/ui/command.tsx +218 -37
  423. package/src/shadcn/components/ui/context-menu.tsx +333 -40
  424. package/src/shadcn/components/ui/dialog.tsx +101 -13
  425. package/src/shadcn/components/ui/drawer.tsx +94 -18
  426. package/src/shadcn/components/ui/dropdown-menu.stories.tsx +18 -2
  427. package/src/shadcn/components/ui/dropdown-menu.tsx +334 -68
  428. package/src/shadcn/components/ui/form.tsx +22 -11
  429. package/src/shadcn/components/ui/hover-card.tsx +2 -1
  430. package/src/shadcn/components/ui/input-otp.tsx +1 -0
  431. package/src/shadcn/components/ui/input.stories.tsx +235 -27
  432. package/src/shadcn/components/ui/input.tsx +400 -29
  433. package/src/shadcn/components/ui/label.tsx +22 -4
  434. package/src/shadcn/components/ui/menubar.tsx +188 -43
  435. package/src/shadcn/components/ui/pagination.stories.tsx +8 -2
  436. package/src/shadcn/components/ui/pagination.tsx +65 -8
  437. package/src/shadcn/components/ui/popover.tsx +36 -4
  438. package/src/shadcn/components/ui/progress.tsx +21 -5
  439. package/src/shadcn/components/ui/radio-button-group.stories.tsx +247 -0
  440. package/src/shadcn/components/ui/radio-button-group.tsx +188 -0
  441. package/src/shadcn/components/ui/radio-group.stories.tsx +70 -14
  442. package/src/shadcn/components/ui/radio-group.tsx +85 -9
  443. package/src/shadcn/components/ui/resizable.stories.tsx +2 -2
  444. package/src/shadcn/components/ui/resizable.tsx +2 -1
  445. package/src/shadcn/components/ui/scroll-area.tsx +34 -5
  446. package/src/shadcn/components/ui/select.stories.tsx +108 -32
  447. package/src/shadcn/components/ui/select.tsx +182 -36
  448. package/src/shadcn/components/ui/separator.tsx +16 -5
  449. package/src/shadcn/components/ui/sheet.tsx +1 -1
  450. package/src/shadcn/components/ui/sidebar.tsx +69 -26
  451. package/src/shadcn/components/ui/skeleton.tsx +4 -1
  452. package/src/shadcn/components/ui/slider.tsx +83 -11
  453. package/src/shadcn/components/ui/sonner.stories.tsx +238 -17
  454. package/src/shadcn/components/ui/sonner.tsx +254 -11
  455. package/src/shadcn/components/ui/switch.stories.tsx +52 -5
  456. package/src/shadcn/components/ui/switch.tsx +92 -7
  457. package/src/shadcn/components/ui/table.stories.tsx +252 -72
  458. package/src/shadcn/components/ui/table.tsx +204 -26
  459. package/src/shadcn/components/ui/tabs.stories.tsx +235 -123
  460. package/src/shadcn/components/ui/tabs.tsx +694 -36
  461. package/src/shadcn/components/ui/textarea.stories.tsx +94 -2
  462. package/src/shadcn/components/ui/textarea.tsx +70 -5
  463. package/src/shadcn/components/ui/toggle-group.tsx +35 -13
  464. package/src/shadcn/components/ui/toggle.stories.tsx +92 -5
  465. package/src/shadcn/components/ui/toggle.tsx +96 -23
  466. package/src/shadcn/components/ui/tooltip.tsx +34 -8
  467. package/src/shadcn/components/ui/tree.tsx +257 -0
  468. package/src/shadcn/shadcn.css +4 -4
  469. package/src/tokens.css +50 -20
  470. package/src/typography.css +78 -15
  471. package/dist/src/components/code-editor.stories.d.ts +0 -7
  472. package/dist/src/components/code-editor.stories.d.ts.map +0 -1
  473. package/dist/src/components/request-line-editor.stories.d.ts +0 -11
  474. package/dist/src/components/request-line-editor.stories.d.ts.map +0 -1
  475. package/dist/src/index.stories.d.ts +0 -14
  476. package/dist/src/index.stories.d.ts.map +0 -1
  477. package/dist/src/index.stories.js +0 -19
  478. package/dist/src/index.stories.js.map +0 -1
  479. package/dist/src/shadcn/components/ui/accordion.stories.d.ts +0 -8
  480. package/dist/src/shadcn/components/ui/accordion.stories.d.ts.map +0 -1
  481. package/dist/src/shadcn/components/ui/alert-dialog.stories.d.ts +0 -8
  482. package/dist/src/shadcn/components/ui/alert-dialog.stories.d.ts.map +0 -1
  483. package/dist/src/shadcn/components/ui/alert.stories.d.ts +0 -8
  484. package/dist/src/shadcn/components/ui/alert.stories.d.ts.map +0 -1
  485. package/dist/src/shadcn/components/ui/aspect-ratio.stories.d.ts +0 -8
  486. package/dist/src/shadcn/components/ui/aspect-ratio.stories.d.ts.map +0 -1
  487. package/dist/src/shadcn/components/ui/avatar.stories.d.ts +0 -8
  488. package/dist/src/shadcn/components/ui/avatar.stories.d.ts.map +0 -1
  489. package/dist/src/shadcn/components/ui/badge.stories.d.ts +0 -8
  490. package/dist/src/shadcn/components/ui/badge.stories.d.ts.map +0 -1
  491. package/dist/src/shadcn/components/ui/breadcrumb.stories.d.ts +0 -8
  492. package/dist/src/shadcn/components/ui/breadcrumb.stories.d.ts.map +0 -1
  493. package/dist/src/shadcn/components/ui/button.stories.d.ts +0 -23
  494. package/dist/src/shadcn/components/ui/button.stories.d.ts.map +0 -1
  495. package/dist/src/shadcn/components/ui/calendar.stories.d.ts +0 -8
  496. package/dist/src/shadcn/components/ui/calendar.stories.d.ts.map +0 -1
  497. package/dist/src/shadcn/components/ui/card.stories.d.ts +0 -8
  498. package/dist/src/shadcn/components/ui/card.stories.d.ts.map +0 -1
  499. package/dist/src/shadcn/components/ui/carousel.stories.d.ts +0 -8
  500. package/dist/src/shadcn/components/ui/carousel.stories.d.ts.map +0 -1
  501. package/dist/src/shadcn/components/ui/chart.stories.d.ts +0 -8
  502. package/dist/src/shadcn/components/ui/chart.stories.d.ts.map +0 -1
  503. package/dist/src/shadcn/components/ui/checkbox.stories.d.ts +0 -8
  504. package/dist/src/shadcn/components/ui/checkbox.stories.d.ts.map +0 -1
  505. package/dist/src/shadcn/components/ui/collapsible.stories.d.ts +0 -8
  506. package/dist/src/shadcn/components/ui/collapsible.stories.d.ts.map +0 -1
  507. package/dist/src/shadcn/components/ui/command.stories.d.ts +0 -8
  508. package/dist/src/shadcn/components/ui/command.stories.d.ts.map +0 -1
  509. package/dist/src/shadcn/components/ui/context-menu.stories.d.ts +0 -8
  510. package/dist/src/shadcn/components/ui/context-menu.stories.d.ts.map +0 -1
  511. package/dist/src/shadcn/components/ui/dialog.stories.d.ts +0 -8
  512. package/dist/src/shadcn/components/ui/dialog.stories.d.ts.map +0 -1
  513. package/dist/src/shadcn/components/ui/drawer.stories.d.ts +0 -8
  514. package/dist/src/shadcn/components/ui/drawer.stories.d.ts.map +0 -1
  515. package/dist/src/shadcn/components/ui/dropdown-menu.stories.d.ts +0 -8
  516. package/dist/src/shadcn/components/ui/dropdown-menu.stories.d.ts.map +0 -1
  517. package/dist/src/shadcn/components/ui/form.stories.d.ts +0 -8
  518. package/dist/src/shadcn/components/ui/form.stories.d.ts.map +0 -1
  519. package/dist/src/shadcn/components/ui/hover-card.stories.d.ts +0 -8
  520. package/dist/src/shadcn/components/ui/hover-card.stories.d.ts.map +0 -1
  521. package/dist/src/shadcn/components/ui/input-otp.stories.d.ts +0 -8
  522. package/dist/src/shadcn/components/ui/input-otp.stories.d.ts.map +0 -1
  523. package/dist/src/shadcn/components/ui/input.stories.d.ts +0 -18
  524. package/dist/src/shadcn/components/ui/input.stories.d.ts.map +0 -1
  525. package/dist/src/shadcn/components/ui/label.stories.d.ts +0 -8
  526. package/dist/src/shadcn/components/ui/label.stories.d.ts.map +0 -1
  527. package/dist/src/shadcn/components/ui/menubar.stories.d.ts +0 -8
  528. package/dist/src/shadcn/components/ui/menubar.stories.d.ts.map +0 -1
  529. package/dist/src/shadcn/components/ui/navigation-menu.stories.d.ts +0 -8
  530. package/dist/src/shadcn/components/ui/navigation-menu.stories.d.ts.map +0 -1
  531. package/dist/src/shadcn/components/ui/pagination.stories.d.ts +0 -8
  532. package/dist/src/shadcn/components/ui/pagination.stories.d.ts.map +0 -1
  533. package/dist/src/shadcn/components/ui/popover.stories.d.ts +0 -8
  534. package/dist/src/shadcn/components/ui/popover.stories.d.ts.map +0 -1
  535. package/dist/src/shadcn/components/ui/progress.stories.d.ts +0 -8
  536. package/dist/src/shadcn/components/ui/progress.stories.d.ts.map +0 -1
  537. package/dist/src/shadcn/components/ui/radio-group.stories.d.ts +0 -8
  538. package/dist/src/shadcn/components/ui/radio-group.stories.d.ts.map +0 -1
  539. package/dist/src/shadcn/components/ui/resizable.stories.d.ts +0 -8
  540. package/dist/src/shadcn/components/ui/resizable.stories.d.ts.map +0 -1
  541. package/dist/src/shadcn/components/ui/scroll-area.stories.d.ts +0 -8
  542. package/dist/src/shadcn/components/ui/scroll-area.stories.d.ts.map +0 -1
  543. package/dist/src/shadcn/components/ui/select.stories.d.ts +0 -11
  544. package/dist/src/shadcn/components/ui/select.stories.d.ts.map +0 -1
  545. package/dist/src/shadcn/components/ui/separator.stories.d.ts +0 -8
  546. package/dist/src/shadcn/components/ui/separator.stories.d.ts.map +0 -1
  547. package/dist/src/shadcn/components/ui/sheet.stories.d.ts +0 -8
  548. package/dist/src/shadcn/components/ui/sheet.stories.d.ts.map +0 -1
  549. package/dist/src/shadcn/components/ui/sidebar.stories.d.ts +0 -11
  550. package/dist/src/shadcn/components/ui/sidebar.stories.d.ts.map +0 -1
  551. package/dist/src/shadcn/components/ui/skeleton.stories.d.ts +0 -8
  552. package/dist/src/shadcn/components/ui/skeleton.stories.d.ts.map +0 -1
  553. package/dist/src/shadcn/components/ui/slider.stories.d.ts +0 -8
  554. package/dist/src/shadcn/components/ui/slider.stories.d.ts.map +0 -1
  555. package/dist/src/shadcn/components/ui/sonner.stories.d.ts +0 -8
  556. package/dist/src/shadcn/components/ui/sonner.stories.d.ts.map +0 -1
  557. package/dist/src/shadcn/components/ui/switch.stories.d.ts +0 -8
  558. package/dist/src/shadcn/components/ui/switch.stories.d.ts.map +0 -1
  559. package/dist/src/shadcn/components/ui/table.stories.d.ts +0 -8
  560. package/dist/src/shadcn/components/ui/table.stories.d.ts.map +0 -1
  561. package/dist/src/shadcn/components/ui/tabs.stories.d.ts +0 -32
  562. package/dist/src/shadcn/components/ui/tabs.stories.d.ts.map +0 -1
  563. package/dist/src/shadcn/components/ui/textarea.stories.d.ts +0 -8
  564. package/dist/src/shadcn/components/ui/textarea.stories.d.ts.map +0 -1
  565. package/dist/src/shadcn/components/ui/toggle-group.stories.d.ts +0 -8
  566. package/dist/src/shadcn/components/ui/toggle-group.stories.d.ts.map +0 -1
  567. package/dist/src/shadcn/components/ui/toggle.stories.d.ts +0 -8
  568. package/dist/src/shadcn/components/ui/toggle.stories.d.ts.map +0 -1
  569. package/dist/src/shadcn/components/ui/tooltip.stories.d.ts +0 -8
  570. package/dist/src/shadcn/components/ui/tooltip.stories.d.ts.map +0 -1
  571. package/src/index.stories.tsx +0 -21
@@ -0,0 +1,2322 @@
1
+ import {
2
+ type Completion,
3
+ type CompletionContext,
4
+ type CompletionResult,
5
+ type CompletionSource,
6
+ completionStatus,
7
+ startCompletion,
8
+ } from "@codemirror/autocomplete";
9
+ import { jsonLanguage } from "@codemirror/lang-json";
10
+ import { ensureSyntaxTree, syntaxTree } from "@codemirror/language";
11
+ import {
12
+ type Extension,
13
+ RangeSet,
14
+ StateEffect,
15
+ StateField,
16
+ } from "@codemirror/state";
17
+ import {
18
+ Decoration,
19
+ EditorView,
20
+ GutterMarker,
21
+ gutterLineClass,
22
+ ViewPlugin,
23
+ type ViewUpdate,
24
+ } from "@codemirror/view";
25
+ import {
26
+ buildJsonDocumentContext,
27
+ type DocumentContext,
28
+ findRootJsonObject,
29
+ type PropertyInfo,
30
+ walkJsonProperties,
31
+ } from "./json-ast";
32
+
33
+ // ── Types ──────────────────────────────────────────────────────────────
34
+
35
+ interface FhirElementType {
36
+ code: string;
37
+ profile?: string[];
38
+ targetProfile?: string[];
39
+ }
40
+
41
+ interface FhirElement {
42
+ path: string;
43
+ short?: string;
44
+ definition?: string;
45
+ min?: number;
46
+ max?: string;
47
+ type?: FhirElementType[];
48
+ binding?: { valueSet: string; strength: string };
49
+ contentReference?: string;
50
+ sliceName?: string;
51
+ fixedUri?: string;
52
+ fixedString?: string;
53
+ fixedCode?: string;
54
+ }
55
+
56
+ interface StructureDefinition {
57
+ type: string;
58
+ url?: string;
59
+ name?: string;
60
+ baseDefinition?: string;
61
+ context?: { expression: string; type: string }[];
62
+ differential?: { element: FhirElement[] };
63
+ }
64
+
65
+ export interface StructureDefinitionSearchParams {
66
+ type?: string;
67
+ url?: string;
68
+ derivation?: string;
69
+ "derivation:missing"?: string;
70
+ kind?: string;
71
+ _count?: string;
72
+ _elements?: string;
73
+ _ilike?: string;
74
+ }
75
+
76
+ export type GetStructureDefinitions = (
77
+ params: StructureDefinitionSearchParams,
78
+ ) => Promise<StructureDefinition[]>;
79
+
80
+ export type ExpandValueSet = (
81
+ url: string,
82
+ filter: string,
83
+ ) => Promise<{ code: string; display?: string; system?: string }[]>;
84
+
85
+ // ── Constants ──────────────────────────────────────────────────────────
86
+
87
+ const PRIMITIVE_TYPES = new Set([
88
+ "boolean",
89
+ "integer",
90
+ "string",
91
+ "decimal",
92
+ "uri",
93
+ "url",
94
+ "canonical",
95
+ "base64Binary",
96
+ "instant",
97
+ "date",
98
+ "dateTime",
99
+ "time",
100
+ "code",
101
+ "oid",
102
+ "id",
103
+ "markdown",
104
+ "unsignedInt",
105
+ "positiveInt",
106
+ "uuid",
107
+ "xhtml",
108
+ ]);
109
+
110
+ function isPrimitiveType(typeCode: string): boolean {
111
+ return (
112
+ PRIMITIVE_TYPES.has(typeCode) ||
113
+ typeCode.startsWith("http://hl7.org/fhirpath/System.")
114
+ );
115
+ }
116
+
117
+ const FHIR_STRING_TYPES = new Set([
118
+ "string",
119
+ "code",
120
+ "uri",
121
+ "url",
122
+ "canonical",
123
+ "id",
124
+ "markdown",
125
+ "oid",
126
+ "uuid",
127
+ "base64Binary",
128
+ "xhtml",
129
+ "http://hl7.org/fhirpath/System.String",
130
+ ]);
131
+
132
+ const FHIR_NUMBER_TYPES = new Set([
133
+ "boolean",
134
+ "integer",
135
+ "decimal",
136
+ "positiveInt",
137
+ "unsignedInt",
138
+ "http://hl7.org/fhirpath/System.Boolean",
139
+ "http://hl7.org/fhirpath/System.Integer",
140
+ "http://hl7.org/fhirpath/System.Decimal",
141
+ ]);
142
+
143
+ // ── Cache ──────────────────────────────────────────────────────────────
144
+
145
+ const sdCache = new Map<string, StructureDefinition | null>();
146
+ const pendingRequests = new Map<string, Promise<StructureDefinition | null>>();
147
+ const listCache = new Map<string, StructureDefinition[]>();
148
+ const pendingListRequests = new Map<string, Promise<StructureDefinition[]>>();
149
+
150
+ const SD_ELEMENTS = "differential,type,name,baseDefinition,url,context";
151
+
152
+ function cacheKey(params: StructureDefinitionSearchParams): string {
153
+ return JSON.stringify(params);
154
+ }
155
+
156
+ async function getCachedSDList(
157
+ params: StructureDefinitionSearchParams,
158
+ getSDs: GetStructureDefinitions,
159
+ ): Promise<StructureDefinition[]> {
160
+ const key = cacheKey(params);
161
+ if (listCache.has(key)) return listCache.get(key) ?? [];
162
+
163
+ let pending = pendingListRequests.get(key);
164
+ if (!pending) {
165
+ pending = getSDs(params)
166
+ .then((list) => {
167
+ listCache.set(key, list);
168
+ pendingListRequests.delete(key);
169
+ for (const sd of list) {
170
+ if (sd.differential?.element) {
171
+ sdCache.set(sd.type, sd);
172
+ }
173
+ }
174
+ return list;
175
+ })
176
+ .catch(() => {
177
+ pendingListRequests.delete(key);
178
+ listCache.set(key, []);
179
+ return [];
180
+ });
181
+ pendingListRequests.set(key, pending);
182
+ }
183
+ return pending;
184
+ }
185
+
186
+ async function getCachedSD(
187
+ type: string,
188
+ getSDs: GetStructureDefinitions,
189
+ ): Promise<StructureDefinition | null> {
190
+ if (sdCache.has(type)) return sdCache.get(type) ?? null;
191
+
192
+ const key = `single:${type}`;
193
+ let pending = pendingRequests.get(key);
194
+ if (!pending) {
195
+ const isUrl = type.includes("/");
196
+ const searchByType = (params: StructureDefinitionSearchParams) =>
197
+ getSDs(params).then((list) => list[0] ?? null);
198
+
199
+ pending = (
200
+ isUrl
201
+ ? searchByType({ url: type, _elements: SD_ELEMENTS, _count: "1" })
202
+ : searchByType({
203
+ type,
204
+ derivation: "specialization",
205
+ _elements: SD_ELEMENTS,
206
+ _count: "1",
207
+ }).then(
208
+ (sd) =>
209
+ sd ??
210
+ searchByType({
211
+ type,
212
+ "derivation:missing": "true",
213
+ _elements: SD_ELEMENTS,
214
+ _count: "1",
215
+ }),
216
+ )
217
+ )
218
+ .then((sd) => {
219
+ sdCache.set(type, sd);
220
+ pendingRequests.delete(key);
221
+ return sd;
222
+ })
223
+ .catch(() => {
224
+ pendingRequests.delete(key);
225
+ return null;
226
+ });
227
+ pendingRequests.set(key, pending);
228
+ }
229
+ return pending;
230
+ }
231
+
232
+ // ── Element helpers ────────────────────────────────────────────────────
233
+
234
+ function fieldName(element: FhirElement): string {
235
+ const parts = element.path.split(".");
236
+ return (parts[parts.length - 1] ?? "").replace("[x]", "");
237
+ }
238
+
239
+ function directChildren(
240
+ elements: FhirElement[],
241
+ parentPath: string,
242
+ ): FhirElement[] {
243
+ const prefix = `${parentPath}.`;
244
+ return elements.filter((el) => {
245
+ if (!el.path.startsWith(prefix)) return false;
246
+ const rest = el.path.slice(prefix.length);
247
+ return !rest.includes(".");
248
+ });
249
+ }
250
+
251
+ function findElement(
252
+ elements: FhirElement[],
253
+ parentPath: string,
254
+ key: string,
255
+ ): FhirElement | undefined {
256
+ const direct = elements.find((el) => {
257
+ if (!el.path.startsWith(`${parentPath}.`)) return false;
258
+ const name = fieldName(el);
259
+ return name === key || name.toLowerCase() === key.toLowerCase();
260
+ });
261
+ if (direct) return direct;
262
+
263
+ for (const el of elements) {
264
+ if (!el.path.endsWith("[x]")) continue;
265
+ if (!el.path.startsWith(`${parentPath}.`)) continue;
266
+ const baseName = fieldName(el);
267
+ if (!key.toLowerCase().startsWith(baseName.toLowerCase())) continue;
268
+ const typeSuffix = key.slice(baseName.length).toLowerCase();
269
+ const matchedType = el.type?.find(
270
+ (t) => t.code.toLowerCase() === typeSuffix,
271
+ );
272
+ if (matchedType) {
273
+ return { ...el, type: [matchedType] };
274
+ }
275
+ }
276
+ return undefined;
277
+ }
278
+
279
+ // ── Resolve completions at path ────────────────────────────────────────
280
+
281
+ async function collectAllElements(
282
+ type: string,
283
+ getSDs: GetStructureDefinitions,
284
+ ): Promise<{ elements: FhirElement[]; basePath: string } | null> {
285
+ const sd = await getCachedSD(type, getSDs);
286
+ if (!sd?.differential?.element) return null;
287
+
288
+ const elements = [...sd.differential.element];
289
+
290
+ if (sd.baseDefinition) {
291
+ const base = await collectAllElements(sd.baseDefinition, getSDs);
292
+ if (base) {
293
+ for (const baseEl of base.elements) {
294
+ const remappedPath = baseEl.path.replace(
295
+ new RegExp(`^${base.basePath}`),
296
+ sd.type,
297
+ );
298
+ if (!elements.some((e) => e.path === remappedPath)) {
299
+ elements.push({ ...baseEl, path: remappedPath });
300
+ }
301
+ }
302
+ }
303
+ }
304
+
305
+ return { elements, basePath: sd.type };
306
+ }
307
+
308
+ async function resolveElements(
309
+ path: string[],
310
+ resourceType: string,
311
+ getSDs: GetStructureDefinitions,
312
+ ): Promise<FhirElement[]> {
313
+ const result = await collectAllElements(resourceType, getSDs);
314
+ if (!result) return [];
315
+
316
+ let currentPath = resourceType;
317
+ let currentElements = result.elements;
318
+
319
+ for (const key of path) {
320
+ if (key === "resourceType") return [];
321
+
322
+ const el = findElement(currentElements, currentPath, key);
323
+ if (!el) return [];
324
+
325
+ if (el.contentReference) {
326
+ currentPath = el.contentReference.replace(/^#/, "");
327
+ continue;
328
+ }
329
+
330
+ if (!el.type?.[0]) return [];
331
+ const typeCode = el.type[0].code;
332
+
333
+ if (typeCode === "BackboneElement") {
334
+ currentPath = el.path;
335
+ continue;
336
+ }
337
+
338
+ const typeResult = await collectAllElements(typeCode, getSDs);
339
+ if (!typeResult) return [];
340
+ currentPath = typeResult.basePath;
341
+ currentElements = typeResult.elements;
342
+ }
343
+
344
+ const children = directChildren(currentElements, currentPath);
345
+ const expanded: FhirElement[] = [];
346
+
347
+ for (const el of children) {
348
+ const isChoiceType = el.path.endsWith("[x]");
349
+ if (isChoiceType && el.type && el.type.length > 0) {
350
+ for (const t of el.type) {
351
+ expanded.push({
352
+ ...el,
353
+ path: el.path.replace(
354
+ "[x]",
355
+ t.code.charAt(0).toUpperCase() + t.code.slice(1),
356
+ ),
357
+ type: [t],
358
+ });
359
+ }
360
+ } else {
361
+ expanded.push(el);
362
+ }
363
+ }
364
+
365
+ return expanded;
366
+ }
367
+
368
+ async function findResourceBoundary(
369
+ path: string[],
370
+ resourceType: string,
371
+ getSDs: GetStructureDefinitions,
372
+ ): Promise<number | null> {
373
+ if (path.length === 0) return null;
374
+
375
+ const result = await collectAllElements(resourceType, getSDs);
376
+ if (!result) return null;
377
+
378
+ let currentPath = resourceType;
379
+ let currentElements = result.elements;
380
+
381
+ for (let i = 0; i < path.length; i++) {
382
+ const key = path[i]!;
383
+ if (key === "resourceType") return null;
384
+
385
+ const el = findElement(currentElements, currentPath, key);
386
+ if (!el) return null;
387
+
388
+ if (el.type?.some((t) => t.code === "Resource")) {
389
+ return i;
390
+ }
391
+
392
+ if (el.contentReference) {
393
+ currentPath = el.contentReference.replace(/^#/, "");
394
+ continue;
395
+ }
396
+ if (!el.type?.[0]) return null;
397
+ const typeCode = el.type[0].code;
398
+ if (typeCode === "BackboneElement") {
399
+ currentPath = el.path;
400
+ continue;
401
+ }
402
+ const typeResult = await collectAllElements(typeCode, getSDs);
403
+ if (!typeResult) return null;
404
+ currentPath = typeResult.basePath;
405
+ currentElements = typeResult.elements;
406
+ }
407
+ return null;
408
+ }
409
+
410
+ // ── Snippet & Completion Builders ──────────────────────────────────────
411
+
412
+ type SnippetKind =
413
+ | "array-complex"
414
+ | "array-primitive"
415
+ | "array-extension"
416
+ | "object"
417
+ | "string"
418
+ | "number"
419
+ | "bare";
420
+
421
+ function snippetKind(element: FhirElement): SnippetKind {
422
+ const isArray = element.max === "*";
423
+ const typeCode = element.type?.[0]?.code;
424
+ if (!typeCode) {
425
+ if (element.contentReference) return isArray ? "array-complex" : "object";
426
+ return "bare";
427
+ }
428
+ if (typeCode === "Extension" && isArray) return "array-extension";
429
+ if (isArray)
430
+ return isPrimitiveType(typeCode) ? "array-primitive" : "array-complex";
431
+ if (FHIR_NUMBER_TYPES.has(typeCode)) return "number";
432
+ if (isPrimitiveType(typeCode)) return "string";
433
+ return "object";
434
+ }
435
+
436
+ function buildSnippet(
437
+ name: string,
438
+ kind: SnippetKind,
439
+ indent: string,
440
+ ): { text: string; cursorOffset: number } {
441
+ const inner = `${indent} `;
442
+ const innerInner = `${inner} `;
443
+ switch (kind) {
444
+ case "array-complex": {
445
+ const text = `"${name}": [\n${inner}{\n${innerInner}\n${inner}}\n${indent}]`;
446
+ return {
447
+ text,
448
+ cursorOffset: text.indexOf(innerInner) + innerInner.length,
449
+ };
450
+ }
451
+ case "array-extension": {
452
+ const text = `"${name}": [\n${inner}{\n${innerInner}"url": ""\n${inner}}\n${indent}]`;
453
+ return { text, cursorOffset: text.lastIndexOf('""') + 1 };
454
+ }
455
+ case "array-primitive": {
456
+ const text = `"${name}": [\n${inner}\n${indent}]`;
457
+ return { text, cursorOffset: text.indexOf(`${inner}\n`) + inner.length };
458
+ }
459
+ case "object": {
460
+ const text = `"${name}": {\n${inner}\n${indent}}`;
461
+ return { text, cursorOffset: text.indexOf(`${inner}\n`) + inner.length };
462
+ }
463
+ case "string": {
464
+ const text = `"${name}": ""`;
465
+ return { text, cursorOffset: text.length - 1 };
466
+ }
467
+ default: {
468
+ const text = `"${name}": `;
469
+ return { text, cursorOffset: text.length };
470
+ }
471
+ }
472
+ }
473
+
474
+ function toCompletion(element: FhirElement): Completion {
475
+ const name = fieldName(element);
476
+ const types = element.type?.map((t) => t.code).join(" | ") ?? "";
477
+ const kind = snippetKind(element);
478
+
479
+ const completion: Completion = {
480
+ label: name,
481
+ type: "property",
482
+ detail: types,
483
+ boost: element.min && element.min > 0 ? 2 : 0,
484
+ apply: (view, _completion, from, to) => {
485
+ const doc = view.state.doc.toString();
486
+ let actualFrom = from;
487
+ let actualTo = to;
488
+ if (actualFrom > 0 && doc[actualFrom - 1] === '"') actualFrom--;
489
+ if (actualTo < doc.length && doc[actualTo] === '"') actualTo++;
490
+
491
+ const afterName = doc.slice(actualTo).match(/^\s*:/);
492
+ if (afterName) {
493
+ const insert = `"${name}"`;
494
+ view.dispatch({
495
+ changes: { from: actualFrom, to: actualTo, insert },
496
+ selection: { anchor: actualFrom + insert.length },
497
+ });
498
+ return;
499
+ }
500
+
501
+ const line = view.state.doc.lineAt(actualFrom);
502
+ const lineText = line.text;
503
+ const indent = lineText.match(/^(\s*)/)?.[1] ?? "";
504
+
505
+ const { text, cursorOffset } = buildSnippet(name, kind, indent);
506
+
507
+ view.dispatch({
508
+ changes: { from: actualFrom, to: actualTo, insert: text },
509
+ selection: { anchor: actualFrom + cursorOffset },
510
+ });
511
+
512
+ if (
513
+ kind === "string" ||
514
+ kind === "number" ||
515
+ kind === "array-primitive" ||
516
+ kind === "array-extension"
517
+ ) {
518
+ setTimeout(() => startCompletion(view), 0);
519
+ }
520
+ },
521
+ };
522
+ if (element.short) completion.info = element.short;
523
+ return completion;
524
+ }
525
+
526
+ function toParameterPropertyCompletion(element: FhirElement): Completion {
527
+ const name = fieldName(element);
528
+ if (name !== "parameter" && name !== "part") return toCompletion(element);
529
+
530
+ const types = element.type?.map((t) => t.code).join(" | ") ?? "";
531
+ const completion: Completion = {
532
+ label: name,
533
+ type: "property",
534
+ detail: types,
535
+ boost: element.min && element.min > 0 ? 2 : 0,
536
+ apply: (view, _completion, from, to) => {
537
+ const doc = view.state.doc.toString();
538
+ let actualFrom = from;
539
+ let actualTo = to;
540
+ if (actualFrom > 0 && doc[actualFrom - 1] === '"') actualFrom--;
541
+ if (actualTo < doc.length && doc[actualTo] === '"') actualTo++;
542
+
543
+ const afterName = doc.slice(actualTo).match(/^\s*:/);
544
+ if (afterName) {
545
+ const insert = `"${name}"`;
546
+ view.dispatch({
547
+ changes: { from: actualFrom, to: actualTo, insert },
548
+ selection: { anchor: actualFrom + insert.length },
549
+ });
550
+ return;
551
+ }
552
+
553
+ const line = view.state.doc.lineAt(actualFrom);
554
+ const indent = line.text.match(/^(\s*)/)?.[1] ?? "";
555
+ const inner = `${indent} `;
556
+ const innerInner = `${inner} `;
557
+ const text = `"${name}": [\n${inner}{\n${innerInner}"name": ""\n${inner}}\n${indent}]`;
558
+ view.dispatch({
559
+ changes: { from: actualFrom, to: actualTo, insert: text },
560
+ selection: { anchor: actualFrom + text.lastIndexOf('""') + 1 },
561
+ });
562
+ setTimeout(() => startCompletion(view), 0);
563
+ },
564
+ };
565
+ if (element.short) completion.info = element.short;
566
+ return completion;
567
+ }
568
+
569
+ function elementsToCompletions(
570
+ elements: FhirElement[],
571
+ mapFn: (el: FhirElement) => Completion,
572
+ ): Completion[] {
573
+ const completions: Completion[] = [];
574
+ for (const el of elements) {
575
+ completions.push(mapFn(el));
576
+ const name = fieldName(el);
577
+ const firstTypeCode = el.type?.[0]?.code;
578
+ if (
579
+ el.type?.length === 1 &&
580
+ firstTypeCode &&
581
+ PRIMITIVE_TYPES.has(firstTypeCode)
582
+ ) {
583
+ const ext: Completion = {
584
+ label: `_${name}`,
585
+ type: "property",
586
+ detail: "Element",
587
+ boost: -1,
588
+ };
589
+ ext.info = "Primitive element extension";
590
+ completions.push(ext);
591
+ }
592
+ }
593
+ return completions;
594
+ }
595
+
596
+ // ── Extension helpers ──────────────────────────────────────────────────
597
+
598
+ interface ExtensionInfo {
599
+ url: string;
600
+ name?: string | undefined;
601
+ isNested: boolean;
602
+ valueTypes: string[];
603
+ slices: { sliceName: string; fixedUri: string; short?: string | undefined }[];
604
+ }
605
+
606
+ function analyzeExtensionSD(sd: StructureDefinition): ExtensionInfo | null {
607
+ if (!sd.differential?.element) return null;
608
+ const elements = sd.differential.element;
609
+
610
+ const valueEl = elements.find((e) => e.path === "Extension.value[x]");
611
+ const isNested = valueEl?.max === "0";
612
+ const valueTypes = isNested ? [] : (valueEl?.type?.map((t) => t.code) ?? []);
613
+
614
+ const slices: ExtensionInfo["slices"] = [];
615
+ for (const el of elements) {
616
+ if (el.path === "Extension.extension" && el.sliceName) {
617
+ const sliceName = el.sliceName;
618
+ const urlEl = elements.find(
619
+ (e) =>
620
+ e.path === "Extension.extension.url" &&
621
+ e.fixedUri &&
622
+ elements.indexOf(e) > elements.indexOf(el),
623
+ );
624
+ const fixedUri = urlEl?.fixedUri ?? sliceName;
625
+ slices.push({ sliceName, fixedUri, short: el.short });
626
+ }
627
+ }
628
+
629
+ return {
630
+ url: sd.url ?? sd.type,
631
+ name: sd.name,
632
+ isNested,
633
+ valueTypes,
634
+ slices,
635
+ };
636
+ }
637
+
638
+ // ── Parameters slice helpers ────────────────────────────────────────────
639
+
640
+ const parametersTypeCache = new Map<string, boolean>();
641
+
642
+ async function isParametersType(
643
+ resourceType: string,
644
+ getSDs: GetStructureDefinitions,
645
+ ): Promise<boolean> {
646
+ if (resourceType === "Parameters") return true;
647
+ if (parametersTypeCache.has(resourceType))
648
+ return parametersTypeCache.get(resourceType)!;
649
+
650
+ const sd = await getCachedSD(resourceType, getSDs);
651
+ if (!sd?.baseDefinition) {
652
+ parametersTypeCache.set(resourceType, false);
653
+ return false;
654
+ }
655
+ const baseType = sd.baseDefinition.split("/").pop()?.split("|")[0] ?? "";
656
+ const result = await isParametersType(baseType, getSDs);
657
+ parametersTypeCache.set(resourceType, result);
658
+ return result;
659
+ }
660
+
661
+ interface ParameterSlice {
662
+ sliceName: string;
663
+ fixedName: string;
664
+ min: number;
665
+ max: string;
666
+ valueTypes: string[];
667
+ short?: string;
668
+ }
669
+
670
+ async function getParameterSlices(
671
+ profileUrls: string[],
672
+ getSDs: GetStructureDefinitions,
673
+ ): Promise<ParameterSlice[]> {
674
+ const slices: ParameterSlice[] = [];
675
+
676
+ for (const profileUrl of profileUrls) {
677
+ const sd = await getCachedSD(profileUrl, getSDs);
678
+ if (!sd?.differential?.element) continue;
679
+
680
+ const elements = sd.differential.element;
681
+ // Find the parameter path dynamically: "X.parameter" where X is the profile's type
682
+ const paramPath = `${sd.type}.parameter`;
683
+
684
+ let current: {
685
+ sliceName: string;
686
+ min: number;
687
+ max: string;
688
+ fixedName: string | null;
689
+ valueTypes: string[];
690
+ short: string | undefined;
691
+ } | null = null;
692
+
693
+ const flush = () => {
694
+ if (current?.fixedName) {
695
+ const s: ParameterSlice = {
696
+ sliceName: current.sliceName,
697
+ fixedName: current.fixedName,
698
+ min: current.min,
699
+ max: current.max,
700
+ valueTypes: current.valueTypes,
701
+ };
702
+ if (current.short != null) s.short = current.short;
703
+ slices.push(s);
704
+ }
705
+ };
706
+
707
+ for (const el of elements) {
708
+ if (el.path === paramPath && el.sliceName) {
709
+ flush();
710
+ current = {
711
+ sliceName: el.sliceName,
712
+ min: el.min ?? 0,
713
+ max: el.max ?? "*",
714
+ fixedName: null,
715
+ valueTypes: [],
716
+ short: el.short,
717
+ };
718
+ continue;
719
+ }
720
+
721
+ if (!current) continue;
722
+
723
+ if (el.path === `${paramPath}.name` && el.fixedString) {
724
+ current.fixedName = el.fixedString;
725
+ }
726
+ if (el.path === `${paramPath}.value[x]` && el.type) {
727
+ current.valueTypes = el.type.map((t) => t.code);
728
+ }
729
+ }
730
+
731
+ flush();
732
+ }
733
+
734
+ return slices;
735
+ }
736
+
737
+ // ── Fixed value helpers ────────────────────────────────────────────────
738
+
739
+ async function getFixedValues(
740
+ effectivePath: string[],
741
+ valueKey: string,
742
+ resourceType: string,
743
+ profileUrls: string[],
744
+ getSDs: GetStructureDefinitions,
745
+ ): Promise<string | null> {
746
+ for (const profileUrl of profileUrls) {
747
+ const sd = await getCachedSD(profileUrl, getSDs);
748
+ if (!sd?.differential?.element) continue;
749
+
750
+ const fhirPath = `${resourceType}.${[...effectivePath, valueKey].join(".")}`;
751
+ for (const el of sd.differential.element) {
752
+ if (el.path !== fhirPath) continue;
753
+ if (el.fixedString != null) return el.fixedString;
754
+ if (el.fixedUri != null) return el.fixedUri;
755
+ if (el.fixedCode != null) return el.fixedCode;
756
+ }
757
+ }
758
+ return null;
759
+ }
760
+
761
+ /** @internal — exported for tests only */
762
+ export function buildParameterSnippet(
763
+ name: string,
764
+ valueTypes: string[],
765
+ indent: string,
766
+ ): { text: string; cursorOffset: number } {
767
+ const inner = `${indent} `;
768
+
769
+ if (valueTypes.length === 1 && FHIR_STRING_TYPES.has(valueTypes[0]!)) {
770
+ const tc = valueTypes[0]!;
771
+ const vf = `value${tc.charAt(0).toUpperCase()}${tc.slice(1)}`;
772
+ const text = `{\n${inner}"name": "${name}",\n${inner}"${vf}": ""\n${indent}}`;
773
+ return { text, cursorOffset: text.lastIndexOf('""') + 1 };
774
+ }
775
+ if (valueTypes.length === 1 && FHIR_NUMBER_TYPES.has(valueTypes[0]!)) {
776
+ const tc = valueTypes[0]!;
777
+ const vf = `value${tc.charAt(0).toUpperCase()}${tc.slice(1)}`;
778
+ const text = `{\n${inner}"name": "${name}",\n${inner}"${vf}": \n${indent}}`;
779
+ return {
780
+ text,
781
+ cursorOffset: text.indexOf(`"${vf}": \n`) + `"${vf}": `.length,
782
+ };
783
+ }
784
+ if (valueTypes.length === 1) {
785
+ const tc = valueTypes[0]!;
786
+ const vf = `value${tc.charAt(0).toUpperCase()}${tc.slice(1)}`;
787
+ const innerInner = `${inner} `;
788
+ const text = `{\n${inner}"name": "${name}",\n${inner}"${vf}": {\n${innerInner}\n${inner}}\n${indent}}`;
789
+ return {
790
+ text,
791
+ cursorOffset:
792
+ text.indexOf(`${innerInner}\n${inner}}`) + innerInner.length,
793
+ };
794
+ }
795
+ // Default to valueString when no value type constraint
796
+ const text = `{\n${inner}"name": "${name}",\n${inner}"valueString": ""\n${indent}}`;
797
+ if (name === "") {
798
+ // Generic template: cursor in name
799
+ return { text, cursorOffset: text.indexOf('""') + 1 };
800
+ }
801
+ return { text, cursorOffset: text.lastIndexOf('""') + 1 };
802
+ }
803
+
804
+ // ── Binding & Reference Resolution ─────────────────────────────────────
805
+
806
+ function buildFhirElementPath(
807
+ resourceType: string,
808
+ path: string[],
809
+ valueKey: string,
810
+ ): string {
811
+ return `${resourceType}.${[...path, valueKey].join(".")}`;
812
+ }
813
+
814
+ async function findProfileBinding(
815
+ profileUrls: string[],
816
+ resourceType: string,
817
+ path: string[],
818
+ valueKey: string,
819
+ getSDs: GetStructureDefinitions,
820
+ ): Promise<string | null> {
821
+ if (profileUrls.length === 0) return null;
822
+
823
+ for (const profileUrl of profileUrls) {
824
+ const sd = await getCachedSD(profileUrl, getSDs);
825
+ if (!sd?.differential?.element) continue;
826
+
827
+ const directPath = buildFhirElementPath(resourceType, path, valueKey);
828
+ for (const el of sd.differential.element) {
829
+ if (el.path === directPath && el.binding?.valueSet) {
830
+ return el.binding.valueSet;
831
+ }
832
+ }
833
+
834
+ if (valueKey === "code") {
835
+ for (let i = path.length; i > 0; i--) {
836
+ const parentFhirPath = buildFhirElementPath(
837
+ resourceType,
838
+ path.slice(0, i - 1),
839
+ path[i - 1]!,
840
+ );
841
+ for (const el of sd.differential.element) {
842
+ if (el.path === parentFhirPath && el.binding?.valueSet) {
843
+ return el.binding.valueSet;
844
+ }
845
+ }
846
+ }
847
+ }
848
+ }
849
+ return null;
850
+ }
851
+
852
+ async function findExtensionBinding(
853
+ doc: string,
854
+ pos: number,
855
+ getSDs: GetStructureDefinitions,
856
+ ): Promise<string | null> {
857
+ const textBefore = doc.slice(0, pos);
858
+
859
+ const urlMatches = [...textBefore.matchAll(/"url"\s*:\s*"([^"]+)"/g)];
860
+ if (urlMatches.length === 0) return null;
861
+
862
+ for (let i = urlMatches.length - 1; i >= 0; i--) {
863
+ const extUrl = urlMatches[i]![1]!;
864
+ if (
865
+ !extUrl.includes("StructureDefinition/") &&
866
+ !extUrl.includes("Extension")
867
+ ) {
868
+ const parentUrlMatches = [
869
+ ...textBefore
870
+ .slice(0, urlMatches[i]?.index)
871
+ .matchAll(/"url"\s*:\s*"([^"]+)"/g),
872
+ ];
873
+ for (let j = parentUrlMatches.length - 1; j >= 0; j--) {
874
+ const parentUrl = parentUrlMatches[j]![1]!;
875
+ if (!parentUrl.includes("/")) continue;
876
+ const parentSD = await getCachedSD(parentUrl, getSDs);
877
+ if (!parentSD?.differential?.element) continue;
878
+ let inSlice = false;
879
+ for (const el of parentSD.differential.element) {
880
+ if (el.path === "Extension.extension" && el.sliceName) {
881
+ const urlEl = parentSD.differential.element.find(
882
+ (e) =>
883
+ e.path === "Extension.extension.url" &&
884
+ e.fixedUri &&
885
+ parentSD.differential!.element.indexOf(e) >
886
+ parentSD.differential!.element.indexOf(el),
887
+ );
888
+ if ((urlEl?.fixedUri ?? el.sliceName) === extUrl) {
889
+ inSlice = true;
890
+ continue;
891
+ }
892
+ if (inSlice) break;
893
+ }
894
+ if (
895
+ inSlice &&
896
+ el.path === "Extension.extension.value[x]" &&
897
+ el.binding?.valueSet
898
+ ) {
899
+ return el.binding.valueSet;
900
+ }
901
+ }
902
+ if (inSlice) break;
903
+ }
904
+ continue;
905
+ }
906
+ const sd = await getCachedSD(extUrl, getSDs);
907
+ if (!sd?.differential?.element) continue;
908
+ for (const el of sd.differential.element) {
909
+ if (el.path === "Extension.value[x]" && el.binding?.valueSet) {
910
+ return el.binding.valueSet;
911
+ }
912
+ }
913
+ }
914
+ return null;
915
+ }
916
+
917
+ async function findBindingForValue(
918
+ path: string[],
919
+ valueKey: string,
920
+ resourceType: string,
921
+ getSDs: GetStructureDefinitions,
922
+ profileUrls: string[] = [],
923
+ doc?: string,
924
+ pos?: number,
925
+ ): Promise<string | null> {
926
+ if (doc != null && pos != null) {
927
+ const inExtension = path.some(
928
+ (p) => p === "extension" || p === "modifierExtension",
929
+ );
930
+ if (inExtension) {
931
+ const extBinding = await findExtensionBinding(doc, pos, getSDs);
932
+ if (extBinding) return extBinding;
933
+ }
934
+ }
935
+
936
+ const profileBinding = await findProfileBinding(
937
+ profileUrls,
938
+ resourceType,
939
+ path,
940
+ valueKey,
941
+ getSDs,
942
+ );
943
+ if (profileBinding) return profileBinding;
944
+
945
+ const elements = await resolveElements(path, resourceType, getSDs);
946
+ for (const el of elements) {
947
+ if (fieldName(el) === valueKey && el.binding?.valueSet) {
948
+ return el.binding.valueSet;
949
+ }
950
+ }
951
+
952
+ if (valueKey === "code") {
953
+ for (let i = path.length; i > 0; i--) {
954
+ const parentElements = await resolveElements(
955
+ path.slice(0, i - 1),
956
+ resourceType,
957
+ getSDs,
958
+ );
959
+ for (const el of parentElements) {
960
+ if (fieldName(el) === path[i - 1] && el.binding?.valueSet) {
961
+ return el.binding.valueSet;
962
+ }
963
+ }
964
+ }
965
+ }
966
+
967
+ return null;
968
+ }
969
+
970
+ async function findCanonicalTargetType(
971
+ path: string[],
972
+ arrayKey: string,
973
+ resourceType: string,
974
+ getSDs: GetStructureDefinitions,
975
+ ): Promise<string | null> {
976
+ const elements = await resolveElements(path, resourceType, getSDs);
977
+ for (const el of elements) {
978
+ if (fieldName(el) !== arrayKey) continue;
979
+ if (el.max !== "*") continue;
980
+ const t = el.type?.[0];
981
+ if (t?.code !== "canonical" || !t.targetProfile?.length) continue;
982
+ return t.targetProfile[0]?.split("/").pop() ?? null;
983
+ }
984
+ return null;
985
+ }
986
+
987
+ async function resolveReferenceTargets(
988
+ path: string[],
989
+ resourceType: string,
990
+ getSDs: GetStructureDefinitions,
991
+ ): Promise<string[] | null> {
992
+ const result = await collectAllElements(resourceType, getSDs);
993
+ if (!result) return null;
994
+
995
+ let currentPath = resourceType;
996
+ let currentElements = result.elements;
997
+
998
+ for (let i = 0; i < path.length - 1; i++) {
999
+ const key = path[i]!;
1000
+ if (key === "resourceType") return null;
1001
+
1002
+ const el = findElement(currentElements, currentPath, key);
1003
+ if (!el) return null;
1004
+
1005
+ if (el.contentReference) {
1006
+ currentPath = el.contentReference.replace(/^#/, "");
1007
+ continue;
1008
+ }
1009
+ if (!el.type?.[0]) return null;
1010
+ const typeCode = el.type[0].code;
1011
+ if (typeCode === "BackboneElement") {
1012
+ currentPath = el.path;
1013
+ continue;
1014
+ }
1015
+ const typeResult = await collectAllElements(typeCode, getSDs);
1016
+ if (!typeResult) return null;
1017
+ currentPath = typeResult.basePath;
1018
+ currentElements = typeResult.elements;
1019
+ }
1020
+
1021
+ const lastKey = path[path.length - 1];
1022
+ if (!lastKey) return null;
1023
+
1024
+ const el = findElement(currentElements, currentPath, lastKey);
1025
+ if (!el?.type) return null;
1026
+
1027
+ const targets: string[] = [];
1028
+ for (const t of el.type) {
1029
+ if (t.code === "Reference" && t.targetProfile) {
1030
+ for (const profile of t.targetProfile) {
1031
+ const rt = profile.split("/").pop();
1032
+ if (rt) targets.push(rt);
1033
+ }
1034
+ }
1035
+ }
1036
+ return targets.length > 0 ? targets : null;
1037
+ }
1038
+
1039
+ // ── Unified completion handler ─────────────────────────────────────────
1040
+
1041
+ async function fhirComplete(
1042
+ ctx: DocumentContext,
1043
+ getSDs: GetStructureDefinitions,
1044
+ resourceTypeHint: string | undefined,
1045
+ expandValueSet: ExpandValueSet | undefined,
1046
+ completionContext: CompletionContext,
1047
+ ): Promise<CompletionResult | null> {
1048
+ const { pos, doc } = ctx;
1049
+
1050
+ // 1. Resolve context: resourceType, effectivePath, profileUrls
1051
+ // Search from root (outermost scope) inward to find the root resourceType.
1052
+ // If not found in doc, fall back to resourceTypeHint (derived from URL).
1053
+ let resourceType: string | undefined;
1054
+ let hasExplicitResourceType = false;
1055
+ let rtScope = ctx.getScope(0);
1056
+ for (let level = ctx.fullPath.length; level >= 0; level--) {
1057
+ const s = ctx.getScope(level);
1058
+ const rt = s.getString("resourceType");
1059
+ if (rt) {
1060
+ resourceType = rt;
1061
+ hasExplicitResourceType = true;
1062
+ rtScope = s;
1063
+ break;
1064
+ }
1065
+ }
1066
+ // If no resourceType found in any scope, use hint.
1067
+ // If found only in an inner scope (not the root), still prefer hint for
1068
+ // boundary detection — the inner RT will be picked up via getScope later.
1069
+ if (!resourceType) {
1070
+ resourceType = resourceTypeHint;
1071
+ rtScope = ctx.getScope(ctx.fullPath.length);
1072
+ } else if (resourceTypeHint && ctx.fullPath.length > 0) {
1073
+ // Check if the found RT is actually from an inner scope, not the root.
1074
+ // If the root scope has no RT but hint is available, use hint as the
1075
+ // outer RT so that findResourceBoundary can resolve the path correctly.
1076
+ const rootRT = ctx.getScope(ctx.fullPath.length).getString("resourceType");
1077
+ if (!rootRT) {
1078
+ resourceType = resourceTypeHint;
1079
+ hasExplicitResourceType = false;
1080
+ }
1081
+ }
1082
+
1083
+ let effectivePath = ctx.fullPath;
1084
+ let profileUrls: string[] = [];
1085
+
1086
+ // Detect Resource-typed boundary (e.g. contained, Bundle.entry.resource)
1087
+ if (resourceType && effectivePath.length > 0) {
1088
+ const boundaryIdx = await findResourceBoundary(
1089
+ effectivePath,
1090
+ resourceType,
1091
+ getSDs,
1092
+ );
1093
+ if (boundaryIdx !== null) {
1094
+ const innerPath = effectivePath.slice(boundaryIdx + 1);
1095
+ const innerScope = ctx.getScope(innerPath.length);
1096
+ const innerRT = innerScope.getString("resourceType");
1097
+ effectivePath = innerPath;
1098
+ if (innerRT) {
1099
+ resourceType = innerRT;
1100
+ hasExplicitResourceType = true;
1101
+ } else {
1102
+ resourceType = "DomainResource";
1103
+ hasExplicitResourceType = false;
1104
+ }
1105
+ profileUrls = innerScope.getStringArray("meta", "profile");
1106
+ } else {
1107
+ profileUrls = rtScope.getStringArray("meta", "profile");
1108
+ }
1109
+ } else {
1110
+ profileUrls = rtScope.getStringArray("meta", "profile");
1111
+ }
1112
+
1113
+ // 2. Handle cursor position kinds
1114
+ const cp = ctx.cursorPosition;
1115
+
1116
+ if (cp.kind === "value") {
1117
+ return handleValueCompletion(
1118
+ cp.key,
1119
+ effectivePath,
1120
+ resourceType,
1121
+ hasExplicitResourceType,
1122
+ profileUrls,
1123
+ doc,
1124
+ pos,
1125
+ getSDs,
1126
+ expandValueSet,
1127
+ completionContext,
1128
+ ctx,
1129
+ );
1130
+ }
1131
+
1132
+ if (cp.kind === "array-item") {
1133
+ return handleArrayItemCompletion(
1134
+ cp.parentKey,
1135
+ effectivePath,
1136
+ resourceType,
1137
+ doc,
1138
+ pos,
1139
+ getSDs,
1140
+ completionContext,
1141
+ profileUrls,
1142
+ );
1143
+ }
1144
+
1145
+ if (cp.kind === "property") {
1146
+ // Don't offer property completions inside arrays
1147
+ if (ctx.isInsideArray()) return null;
1148
+
1149
+ return handlePropertyCompletion(
1150
+ effectivePath,
1151
+ resourceType,
1152
+ hasExplicitResourceType,
1153
+ doc,
1154
+ pos,
1155
+ getSDs,
1156
+ completionContext,
1157
+ ctx,
1158
+ );
1159
+ }
1160
+
1161
+ return null;
1162
+ }
1163
+
1164
+ // ── Value completion ───────────────────────────────────────────────────
1165
+
1166
+ async function handleValueCompletion(
1167
+ valueKey: string,
1168
+ effectivePath: string[],
1169
+ resourceType: string | undefined,
1170
+ _hasExplicitResourceType: boolean,
1171
+ profileUrls: string[],
1172
+ doc: string,
1173
+ pos: number,
1174
+ getSDs: GetStructureDefinitions,
1175
+ expandValueSet: ExpandValueSet | undefined,
1176
+ completionContext: CompletionContext,
1177
+ ctx: DocumentContext,
1178
+ ): Promise<CompletionResult | null> {
1179
+ // resourceType value
1180
+ if (valueKey === "resourceType") {
1181
+ const sds = await getCachedSDList(
1182
+ {
1183
+ derivation: "specialization",
1184
+ kind: "resource",
1185
+ _elements: "type",
1186
+ _count: "500",
1187
+ },
1188
+ getSDs,
1189
+ );
1190
+ const options: Completion[] = sds.map((sd) => ({
1191
+ label: sd.type,
1192
+ type: "type",
1193
+ apply: (view: EditorView, _c: Completion, from: number, to: number) => {
1194
+ const d = view.state.doc.toString();
1195
+ let actualTo = to;
1196
+ if (actualTo < d.length && d[actualTo] === '"') actualTo++;
1197
+ view.dispatch({
1198
+ changes: { from, to: actualTo, insert: `${sd.type}"` },
1199
+ selection: { anchor: from + sd.type.length + 1 },
1200
+ });
1201
+ },
1202
+ }));
1203
+ if (options.length === 0) return null;
1204
+ const word = completionContext.matchBefore(/[\w]*/);
1205
+ return { from: word?.from ?? pos, options, validFor: /^\w*$/ };
1206
+ }
1207
+
1208
+ // Parameters.parameter.name → slice names from profile
1209
+ if (
1210
+ valueKey === "name" &&
1211
+ resourceType &&
1212
+ effectivePath.length > 0 &&
1213
+ effectivePath[effectivePath.length - 1] === "parameter" &&
1214
+ (await isParametersType(resourceType, getSDs))
1215
+ ) {
1216
+ const slices = await getParameterSlices(profileUrls, getSDs);
1217
+ if (slices.length > 0) {
1218
+ const options: Completion[] = slices.map((slice) => ({
1219
+ label: slice.fixedName,
1220
+ type: "text",
1221
+ detail: slice.min > 0 ? "required" : "optional",
1222
+ boost: slice.min > 0 ? 2 : 0,
1223
+ ...(slice.short ? { info: slice.short } : {}),
1224
+ apply: (view: EditorView, _c: Completion, from: number, to: number) => {
1225
+ const d = view.state.doc.toString();
1226
+ let actualTo = to;
1227
+ if (actualTo < d.length && d[actualTo] === '"') actualTo++;
1228
+ view.dispatch({
1229
+ changes: {
1230
+ from,
1231
+ to: actualTo,
1232
+ insert: `${slice.fixedName}"`,
1233
+ },
1234
+ selection: { anchor: from + slice.fixedName.length + 1 },
1235
+ });
1236
+ // Auto-insert value[x] field based on slice type
1237
+ const vTypes = slice.valueTypes;
1238
+ if (vTypes.length === 0) vTypes.push("string");
1239
+ if (vTypes.length === 1) {
1240
+ setTimeout(() => {
1241
+ const cp = view.state.selection.main.head;
1242
+ const cd = view.state.doc.toString();
1243
+ const after = cd.slice(cp);
1244
+ if (!/^\s*\n\s*\}/.test(after)) return;
1245
+ const lineObj = view.state.doc.lineAt(cp);
1246
+ const ind = lineObj.text.match(/^(\s*)/)?.[1] ?? "";
1247
+ const tc = vTypes[0]!;
1248
+ const vf = `value${tc.charAt(0).toUpperCase()}${tc.slice(1)}`;
1249
+ let ins: string;
1250
+ let cOff: number;
1251
+ if (FHIR_STRING_TYPES.has(tc)) {
1252
+ ins = `,\n${ind}"${vf}": ""`;
1253
+ cOff = ins.length - 1;
1254
+ } else if (FHIR_NUMBER_TYPES.has(tc)) {
1255
+ ins = `,\n${ind}"${vf}": `;
1256
+ cOff = ins.length;
1257
+ } else {
1258
+ const inner = `${ind} `;
1259
+ ins = `,\n${ind}"${vf}": {\n${inner}\n${ind}}`;
1260
+ cOff = ins.indexOf(`${inner}\n${ind}}`) + inner.length;
1261
+ }
1262
+ view.dispatch({
1263
+ changes: { from: cp, insert: ins },
1264
+ selection: { anchor: cp + cOff },
1265
+ });
1266
+ setTimeout(() => startCompletion(view), 0);
1267
+ }, 10);
1268
+ }
1269
+ },
1270
+ }));
1271
+ const word = completionContext.matchBefore(/[\w]*/);
1272
+ return { from: word?.from ?? pos, options, validFor: /^\w*$/ };
1273
+ }
1274
+ }
1275
+
1276
+ // Fixed value from profile (fixedString, fixedUri, fixedCode)
1277
+ if (resourceType && profileUrls.length > 0) {
1278
+ const fixedVal = await getFixedValues(
1279
+ effectivePath,
1280
+ valueKey,
1281
+ resourceType,
1282
+ profileUrls,
1283
+ getSDs,
1284
+ );
1285
+ if (fixedVal != null) {
1286
+ const option: Completion = {
1287
+ label: fixedVal,
1288
+ type: "text",
1289
+ boost: 10,
1290
+ apply: (view: EditorView, _c: Completion, from: number, to: number) => {
1291
+ const d = view.state.doc.toString();
1292
+ let actualTo = to;
1293
+ if (actualTo < d.length && d[actualTo] === '"') actualTo++;
1294
+ view.dispatch({
1295
+ changes: { from, to: actualTo, insert: `${fixedVal}"` },
1296
+ selection: { anchor: from + fixedVal.length + 1 },
1297
+ });
1298
+ },
1299
+ };
1300
+ const word = completionContext.matchBefore(/[\w.:/-]*/);
1301
+ return {
1302
+ from: word?.from ?? pos,
1303
+ options: [option],
1304
+ validFor: /^[\w.:/-]*$/,
1305
+ };
1306
+ }
1307
+ }
1308
+
1309
+ // reference value
1310
+ if (valueKey === "reference" && resourceType) {
1311
+ const targets = await resolveReferenceTargets(
1312
+ effectivePath,
1313
+ resourceType,
1314
+ getSDs,
1315
+ );
1316
+ if (targets) {
1317
+ const options: Completion[] = targets.map((rt) => ({
1318
+ label: `${rt}/`,
1319
+ type: "type",
1320
+ apply: (view: EditorView, _c: Completion, from: number, to: number) => {
1321
+ view.dispatch({
1322
+ changes: { from, to, insert: `${rt}/` },
1323
+ selection: { anchor: from + rt.length + 1 },
1324
+ });
1325
+ },
1326
+ }));
1327
+ const word = completionContext.matchBefore(/[\w/]*/);
1328
+ return { from: word?.from ?? pos, options, validFor: /^[\w/]*$/ };
1329
+ }
1330
+ }
1331
+
1332
+ // Extension URL value
1333
+ if (valueKey === "url") {
1334
+ const lastSeg = ctx.fullPath[ctx.fullPath.length - 1];
1335
+ if (lastSeg === "extension" || lastSeg === "modifierExtension") {
1336
+ return handleExtensionUrlCompletion(
1337
+ effectivePath,
1338
+ resourceType,
1339
+ profileUrls,
1340
+ doc,
1341
+ pos,
1342
+ getSDs,
1343
+ completionContext,
1344
+ ctx,
1345
+ );
1346
+ }
1347
+ }
1348
+
1349
+ // Boolean value
1350
+ if (resourceType) {
1351
+ const elements = await resolveElements(effectivePath, resourceType, getSDs);
1352
+ const el = elements.find((e) => fieldName(e) === valueKey);
1353
+ if (el?.type?.length === 1 && el.type[0]?.code === "boolean") {
1354
+ const word = completionContext.matchBefore(/[\w]*/);
1355
+ const options: Completion[] = ["true", "false"].map((v) => ({
1356
+ label: v,
1357
+ type: "keyword",
1358
+ apply: (view: EditorView, _c: Completion, from: number, to: number) => {
1359
+ const d = view.state.doc.toString();
1360
+ let actualFrom = from;
1361
+ let actualTo = to;
1362
+ // Remove surrounding quotes if present
1363
+ if (actualFrom > 0 && d[actualFrom - 1] === '"') actualFrom--;
1364
+ if (actualTo < d.length && d[actualTo] === '"') actualTo++;
1365
+ view.dispatch({
1366
+ changes: { from: actualFrom, to: actualTo, insert: v },
1367
+ selection: { anchor: actualFrom + v.length },
1368
+ });
1369
+ },
1370
+ }));
1371
+ return { from: word?.from ?? pos, options, validFor: /^\w*$/ };
1372
+ }
1373
+ }
1374
+
1375
+ // Terminology binding
1376
+ if (
1377
+ valueKey !== "resourceType" &&
1378
+ valueKey !== "reference" &&
1379
+ valueKey !== "url" &&
1380
+ expandValueSet &&
1381
+ resourceType
1382
+ ) {
1383
+ const valueSetUrl = await findBindingForValue(
1384
+ effectivePath,
1385
+ valueKey,
1386
+ resourceType,
1387
+ getSDs,
1388
+ profileUrls,
1389
+ doc,
1390
+ pos,
1391
+ );
1392
+ if (valueSetUrl) {
1393
+ const quoteWord = completionContext.matchBefore(/"[\w-]*/);
1394
+ const from = quoteWord?.from ?? pos;
1395
+ const filter = quoteWord ? quoteWord.text.replace(/^"/, "") : "";
1396
+ try {
1397
+ const codes = await expandValueSet(valueSetUrl, filter);
1398
+ if (codes.length > 0) {
1399
+ const options: Completion[] = codes.map((c) => ({
1400
+ label: c.code,
1401
+ ...(c.display ? { info: c.display } : {}),
1402
+ type: "text",
1403
+ apply: (
1404
+ view: EditorView,
1405
+ _c: Completion,
1406
+ applyFrom: number,
1407
+ applyTo: number,
1408
+ ) => {
1409
+ const d = view.state.doc.toString();
1410
+ let actualFrom = applyFrom;
1411
+ let actualTo = applyTo;
1412
+ if (d[actualFrom] === '"') actualFrom++;
1413
+ if (actualTo < d.length && d[actualTo] === '"') actualTo++;
1414
+ view.dispatch({
1415
+ changes: {
1416
+ from: actualFrom,
1417
+ to: actualTo,
1418
+ insert: `${c.code}"`,
1419
+ },
1420
+ selection: { anchor: actualFrom + c.code.length + 1 },
1421
+ });
1422
+ },
1423
+ }));
1424
+ return { from, options, filter: false };
1425
+ }
1426
+ } catch {
1427
+ // expand failed
1428
+ }
1429
+ }
1430
+ }
1431
+
1432
+ return null;
1433
+ }
1434
+
1435
+ // ── Extension URL completion ───────────────────────────────────────────
1436
+
1437
+ async function handleExtensionUrlCompletion(
1438
+ _effectivePath: string[],
1439
+ resourceType: string | undefined,
1440
+ profileUrls: string[],
1441
+ doc: string,
1442
+ pos: number,
1443
+ getSDs: GetStructureDefinitions,
1444
+ completionContext: CompletionContext,
1445
+ ctx: DocumentContext,
1446
+ ): Promise<CompletionResult | null> {
1447
+ // Check for nested extension (parent has url)
1448
+ const urlKeyPos = doc.lastIndexOf('"url"', pos);
1449
+ let scanEnd = urlKeyPos !== -1 ? urlKeyPos : pos;
1450
+ for (let i = scanEnd - 1; i >= 0; i--) {
1451
+ const c = doc[i];
1452
+ if (c === "{") {
1453
+ scanEnd = i;
1454
+ break;
1455
+ }
1456
+ if (c === "}" || c === "]" || c === "[") break;
1457
+ }
1458
+ const textBefore = doc.slice(0, scanEnd);
1459
+ let parentExtUrl: string | null = null;
1460
+ let depth = 0;
1461
+ let inStr = false;
1462
+ let esc = false;
1463
+ let foundExtArray = false;
1464
+ for (let i = textBefore.length - 1; i >= 0; i--) {
1465
+ const ch = textBefore[i];
1466
+ if (esc) {
1467
+ esc = false;
1468
+ continue;
1469
+ }
1470
+ if (ch === "\\") {
1471
+ esc = true;
1472
+ continue;
1473
+ }
1474
+ if (ch === '"') {
1475
+ inStr = !inStr;
1476
+ continue;
1477
+ }
1478
+ if (inStr) continue;
1479
+ if (ch === "}" || ch === "]") {
1480
+ depth++;
1481
+ } else if (ch === "[") {
1482
+ if (depth === 0) {
1483
+ foundExtArray = true;
1484
+ continue;
1485
+ }
1486
+ depth--;
1487
+ } else if (ch === "{") {
1488
+ if (depth === 0 && foundExtArray) {
1489
+ const objText = textBefore.slice(i);
1490
+ const urlMatch = objText.match(/^\{[\s\S]*?"url"\s*:\s*"([^"]+)"/);
1491
+ if (urlMatch?.[1]?.includes("/")) {
1492
+ parentExtUrl = urlMatch[1];
1493
+ }
1494
+ break;
1495
+ }
1496
+ if (depth === 0) break;
1497
+ depth--;
1498
+ }
1499
+ }
1500
+
1501
+ if (parentExtUrl) {
1502
+ return handleNestedExtensionSlices(
1503
+ parentExtUrl,
1504
+ pos,
1505
+ getSDs,
1506
+ completionContext,
1507
+ );
1508
+ }
1509
+
1510
+ if (!resourceType) return null;
1511
+
1512
+ // Top-level extension URL completions
1513
+ const path = ctx.fullPath;
1514
+ const contextTypes: string[] = [
1515
+ resourceType,
1516
+ "DomainResource",
1517
+ "Resource",
1518
+ "Element",
1519
+ ];
1520
+ const extIdx = path.lastIndexOf("extension");
1521
+ if (extIdx > 0) {
1522
+ let currentRT = resourceType;
1523
+ for (let i = 0; i < extIdx; i++) {
1524
+ const seg = path[i];
1525
+ if (!seg) break;
1526
+ const elements = await resolveElements([], currentRT, getSDs);
1527
+ const el = elements.find((e) => fieldName(e) === seg);
1528
+ if (el?.type?.[0]?.code && !isPrimitiveType(el.type[0].code))
1529
+ currentRT = el.type[0].code;
1530
+ else break;
1531
+ }
1532
+ if (currentRT !== resourceType) {
1533
+ contextTypes.length = 0;
1534
+ contextTypes.push(
1535
+ currentRT,
1536
+ "Element",
1537
+ `${resourceType}.${path.slice(0, extIdx).join(".")}`,
1538
+ );
1539
+ }
1540
+ }
1541
+
1542
+ // Profile extensions
1543
+ const profileExtUrls: string[] = [];
1544
+ if (contextTypes.includes(resourceType)) {
1545
+ for (const pUrl of profileUrls) {
1546
+ const profileSD = await getCachedSD(pUrl, getSDs);
1547
+ if (!profileSD?.differential?.element) continue;
1548
+ for (const el of profileSD.differential.element) {
1549
+ for (const t of el.type ?? []) {
1550
+ if (t.code === "Extension") {
1551
+ for (const p of t.profile ?? []) {
1552
+ const clean = p.includes("|") ? p.slice(0, p.indexOf("|")) : p;
1553
+ if (!profileExtUrls.includes(clean)) profileExtUrls.push(clean);
1554
+ }
1555
+ }
1556
+ }
1557
+ }
1558
+ }
1559
+ }
1560
+
1561
+ const bareWord = completionContext.matchBefore(/[\w.:/-]*/);
1562
+ const filter = bareWord?.text ?? "";
1563
+ const searchParams: StructureDefinitionSearchParams = {
1564
+ type: "Extension",
1565
+ derivation: "constraint",
1566
+ _elements: "url,context",
1567
+ _count: "500",
1568
+ };
1569
+ if (filter) searchParams._ilike = filter;
1570
+ const results = await getCachedSDList(searchParams, getSDs);
1571
+
1572
+ const containerType = contextTypes[0];
1573
+ const fhirPath = contextTypes.find((c) => c.includes("."));
1574
+ const contextExts = results.filter((sd) =>
1575
+ sd.context?.some(
1576
+ (c) => c.type === "element" && contextTypes.includes(c.expression),
1577
+ ),
1578
+ );
1579
+ const seen = new Set<string>();
1580
+ const allExts: { url: string; boost: number }[] = [];
1581
+ if (contextTypes.includes(resourceType)) {
1582
+ for (const u of profileExtUrls) {
1583
+ if (!seen.has(u)) {
1584
+ seen.add(u);
1585
+ allExts.push({ url: u, boost: 20 });
1586
+ }
1587
+ }
1588
+ }
1589
+ for (const sd of contextExts) {
1590
+ const u = sd.url ?? sd.type;
1591
+ if (seen.has(u)) continue;
1592
+ seen.add(u);
1593
+ const ctxExprs =
1594
+ sd.context
1595
+ ?.filter((c) => c.type === "element")
1596
+ .map((c) => c.expression) ?? [];
1597
+ let boost = 0;
1598
+ if (fhirPath && ctxExprs.includes(fhirPath)) boost = 15;
1599
+ else if (containerType && ctxExprs.includes(containerType)) boost = 10;
1600
+ else if (ctxExprs.includes(resourceType)) boost = 5;
1601
+ else if (ctxExprs.some((e) => e === "DomainResource" || e === "Resource"))
1602
+ boost = 2;
1603
+ else if (ctxExprs.includes("Element")) boost = 1;
1604
+ allExts.push({ url: u, boost });
1605
+ }
1606
+ const lf = filter.toLowerCase();
1607
+ const filtered = (
1608
+ lf ? allExts.filter((e) => e.url.toLowerCase().includes(lf)) : allExts
1609
+ ).sort((a, b) => b.boost - a.boost);
1610
+ if (filtered.length > 0) {
1611
+ const options: Completion[] = filtered.map((ext) => ({
1612
+ label: ext.url,
1613
+ type: "text",
1614
+ boost: ext.boost,
1615
+ apply: (
1616
+ view: EditorView,
1617
+ _c: Completion,
1618
+ applyFrom: number,
1619
+ applyTo: number,
1620
+ ) => {
1621
+ const d = view.state.doc.toString();
1622
+ let actualTo = applyTo;
1623
+ if (actualTo < d.length && d[actualTo] === '"') actualTo++;
1624
+ view.dispatch({
1625
+ changes: { from: applyFrom, to: actualTo, insert: `${ext.url}"` },
1626
+ selection: { anchor: applyFrom + ext.url.length + 1 },
1627
+ });
1628
+ setTimeout(async () => {
1629
+ const fullSD = await getCachedSD(ext.url, getSDs);
1630
+ if (!fullSD) return;
1631
+ const extInfo = analyzeExtensionSD(fullSD);
1632
+ if (!extInfo) return;
1633
+ const cursorPos = view.state.selection.main.head;
1634
+ const curDoc = view.state.doc.toString();
1635
+ const after = curDoc.slice(cursorPos);
1636
+ if (!/^\s*\n\s*\}/.test(after)) return;
1637
+ const lineObj = view.state.doc.lineAt(cursorPos);
1638
+ const ind = lineObj.text.match(/^(\s*)/)?.[1] ?? "";
1639
+ let ins: string;
1640
+ let cOff: number;
1641
+ if (extInfo.isNested) {
1642
+ const inner = `${ind} `;
1643
+ const innerInner = `${inner} `;
1644
+ ins = `,\n${ind}"extension": [\n${inner}{\n${innerInner}"url": ""\n${inner}}\n${ind}]`;
1645
+ cOff = ins.lastIndexOf('""') + 1;
1646
+ } else if (extInfo.valueTypes.length === 1) {
1647
+ const tc = extInfo.valueTypes[0]!;
1648
+ const vf = `value${tc.charAt(0).toUpperCase()}${tc.slice(1)}`;
1649
+ if (FHIR_STRING_TYPES.has(tc) || tc === "code") {
1650
+ ins = `,\n${ind}"${vf}": ""`;
1651
+ cOff = ins.length - 1;
1652
+ } else if (FHIR_NUMBER_TYPES.has(tc)) {
1653
+ ins = `,\n${ind}"${vf}": `;
1654
+ cOff = ins.length;
1655
+ } else {
1656
+ const inner = `${ind} `;
1657
+ ins = `,\n${ind}"${vf}": {\n${inner}\n${ind}}`;
1658
+ cOff = ins.indexOf(`${inner}\n${ind}`) + inner.length;
1659
+ }
1660
+ } else {
1661
+ return;
1662
+ }
1663
+ view.dispatch({
1664
+ changes: { from: cursorPos, insert: ins },
1665
+ selection: { anchor: cursorPos + cOff },
1666
+ });
1667
+ setTimeout(() => startCompletion(view), 0);
1668
+ }, 10);
1669
+ },
1670
+ }));
1671
+ return { from: bareWord?.from ?? pos, options, filter: false };
1672
+ }
1673
+ return null;
1674
+ }
1675
+
1676
+ async function handleNestedExtensionSlices(
1677
+ parentExtUrl: string,
1678
+ pos: number,
1679
+ getSDs: GetStructureDefinitions,
1680
+ completionContext: CompletionContext,
1681
+ ): Promise<CompletionResult | null> {
1682
+ const parentSD = await getCachedSD(parentExtUrl, getSDs);
1683
+ if (!parentSD) return null;
1684
+ const info = analyzeExtensionSD(parentSD);
1685
+ if (!info?.slices.length) return null;
1686
+
1687
+ const word = completionContext.matchBefore(/[\w.:/-]*/);
1688
+ const filter = word?.text.toLowerCase() ?? "";
1689
+ const matching = filter
1690
+ ? info.slices.filter(
1691
+ (s) =>
1692
+ s.fixedUri.toLowerCase().includes(filter) ||
1693
+ (s.short?.toLowerCase().includes(filter) ?? false),
1694
+ )
1695
+ : info.slices;
1696
+
1697
+ const options: Completion[] = matching.map((slice) => {
1698
+ const sliceElements = parentSD.differential?.element ?? [];
1699
+ let sliceValueTypes: string[] = [];
1700
+ let inSl = false;
1701
+ for (const el of sliceElements) {
1702
+ if (
1703
+ el.path === "Extension.extension" &&
1704
+ el.sliceName === slice.sliceName
1705
+ ) {
1706
+ inSl = true;
1707
+ continue;
1708
+ }
1709
+ if (inSl && el.path === "Extension.extension.value[x]") {
1710
+ sliceValueTypes = el.type?.map((t) => t.code) ?? [];
1711
+ break;
1712
+ }
1713
+ if (inSl && el.path === "Extension.extension" && el.sliceName) break;
1714
+ }
1715
+ return {
1716
+ label: slice.fixedUri,
1717
+ ...(slice.short ? { info: slice.short } : {}),
1718
+ type: "text",
1719
+ apply: (view: EditorView, _c: Completion, from: number, to: number) => {
1720
+ const d = view.state.doc.toString();
1721
+ let actualTo = to;
1722
+ if (actualTo < d.length && d[actualTo] === '"') actualTo++;
1723
+ view.dispatch({
1724
+ changes: { from, to: actualTo, insert: `${slice.fixedUri}"` },
1725
+ selection: { anchor: from + slice.fixedUri.length + 1 },
1726
+ });
1727
+ if (sliceValueTypes.length === 1) {
1728
+ setTimeout(() => {
1729
+ const cp = view.state.selection.main.head;
1730
+ const cd = view.state.doc.toString();
1731
+ const af = cd.slice(cp);
1732
+ if (!/^\s*\n\s*\}/.test(af)) return;
1733
+ const lo = view.state.doc.lineAt(cp);
1734
+ const ind = lo.text.match(/^(\s*)/)?.[1] ?? "";
1735
+ const tc = sliceValueTypes[0]!;
1736
+ const vf = `value${tc.charAt(0).toUpperCase()}${tc.slice(1)}`;
1737
+ let ins: string;
1738
+ let cOff: number;
1739
+ if (FHIR_STRING_TYPES.has(tc) || tc === "code") {
1740
+ ins = `,\n${ind}"${vf}": ""`;
1741
+ cOff = ins.length - 1;
1742
+ } else if (FHIR_NUMBER_TYPES.has(tc)) {
1743
+ ins = `,\n${ind}"${vf}": `;
1744
+ cOff = ins.length;
1745
+ } else {
1746
+ const inner = `${ind} `;
1747
+ ins = `,\n${ind}"${vf}": {\n${inner}\n${ind}}`;
1748
+ cOff = ins.indexOf(`${inner}\n${ind}`) + inner.length;
1749
+ }
1750
+ view.dispatch({
1751
+ changes: { from: cp, insert: ins },
1752
+ selection: { anchor: cp + cOff },
1753
+ });
1754
+ setTimeout(() => startCompletion(view), 0);
1755
+ }, 10);
1756
+ }
1757
+ },
1758
+ };
1759
+ });
1760
+ if (options.length > 0)
1761
+ return { from: word?.from ?? pos, options, filter: false };
1762
+ return null;
1763
+ }
1764
+
1765
+ // ── Array item completion ──────────────────────────────────────────────
1766
+
1767
+ async function handleArrayItemCompletion(
1768
+ parentKey: string,
1769
+ effectivePath: string[],
1770
+ resourceType: string | undefined,
1771
+ _doc: string,
1772
+ pos: number,
1773
+ getSDs: GetStructureDefinitions,
1774
+ completionContext: CompletionContext,
1775
+ profileUrls: string[],
1776
+ ): Promise<CompletionResult | null> {
1777
+ if (!resourceType) return null;
1778
+
1779
+ // Parameters.parameter or part → snippet completions from profile slices
1780
+ if (
1781
+ (parentKey === "parameter" || parentKey === "part") &&
1782
+ (await isParametersType(resourceType, getSDs))
1783
+ ) {
1784
+ const slices =
1785
+ parentKey === "parameter"
1786
+ ? await getParameterSlices(profileUrls, getSDs)
1787
+ : [];
1788
+
1789
+ const options: Completion[] = [];
1790
+
1791
+ for (const slice of slices) {
1792
+ options.push({
1793
+ label: slice.fixedName,
1794
+ type: "text",
1795
+ detail:
1796
+ slice.min > 0 ? `${slice.min}..${slice.max}` : `0..${slice.max}`,
1797
+ boost: slice.min > 0 ? 2 : 0,
1798
+ ...(slice.short ? { info: slice.short } : {}),
1799
+ apply: (view: EditorView, _c: Completion, from: number, to: number) => {
1800
+ const line = view.state.doc.lineAt(from);
1801
+ const indent = line.text.match(/^(\s*)/)?.[1] ?? "";
1802
+ const { text, cursorOffset } = buildParameterSnippet(
1803
+ slice.fixedName,
1804
+ slice.valueTypes,
1805
+ indent,
1806
+ );
1807
+
1808
+ view.dispatch({
1809
+ changes: { from, to, insert: text },
1810
+ selection: { anchor: from + cursorOffset },
1811
+ });
1812
+ setTimeout(() => startCompletion(view), 0);
1813
+ },
1814
+ });
1815
+ }
1816
+
1817
+ // Generic parameter template (always available)
1818
+ options.push({
1819
+ label: "parameter",
1820
+ type: "text",
1821
+ boost: -1,
1822
+ info: "Custom parameter",
1823
+ apply: (view: EditorView, _c: Completion, from: number, to: number) => {
1824
+ const line = view.state.doc.lineAt(from);
1825
+ const indent = line.text.match(/^(\s*)/)?.[1] ?? "";
1826
+ const { text, cursorOffset } = buildParameterSnippet("", [], indent);
1827
+ view.dispatch({
1828
+ changes: { from, to, insert: text },
1829
+ selection: { anchor: from + cursorOffset },
1830
+ });
1831
+ setTimeout(() => startCompletion(view), 0);
1832
+ },
1833
+ });
1834
+
1835
+ const word = completionContext.matchBefore(/[\w]*/);
1836
+ return { from: word?.from ?? pos, options };
1837
+ }
1838
+
1839
+ // Get path excluding the array key itself
1840
+ const parentPath =
1841
+ effectivePath.length > 0 &&
1842
+ effectivePath[effectivePath.length - 1] === parentKey
1843
+ ? effectivePath.slice(0, -1)
1844
+ : effectivePath;
1845
+
1846
+ const targetType = await findCanonicalTargetType(
1847
+ parentPath,
1848
+ parentKey,
1849
+ resourceType,
1850
+ getSDs,
1851
+ );
1852
+ if (targetType === "StructureDefinition") {
1853
+ const allSDs = await getCachedSDList(
1854
+ {
1855
+ type: `${resourceType},DomainResource,Resource`,
1856
+ derivation: "constraint",
1857
+ _elements: "url,name",
1858
+ _count: "50",
1859
+ },
1860
+ getSDs,
1861
+ );
1862
+ const seen = new Set<string>();
1863
+ const uniqueSDs = allSDs.filter((sd) => {
1864
+ const u = sd.url ?? sd.type;
1865
+ if (seen.has(u)) return false;
1866
+ seen.add(u);
1867
+ return true;
1868
+ });
1869
+ if (uniqueSDs.length > 0) {
1870
+ const quoteWord = completionContext.matchBefore(/"[^"]*/);
1871
+ const bareWord = completionContext.matchBefore(/[\w.:/-]*/);
1872
+ const from = quoteWord?.from ?? bareWord?.from ?? pos;
1873
+ const filter = quoteWord
1874
+ ? quoteWord.text.replace(/^"/, "").toLowerCase()
1875
+ : (bareWord?.text.toLowerCase() ?? "");
1876
+ const filtered = filter
1877
+ ? uniqueSDs.filter(
1878
+ (sd) =>
1879
+ sd.name?.toLowerCase().includes(filter) ||
1880
+ sd.url?.toLowerCase().includes(filter),
1881
+ )
1882
+ : uniqueSDs;
1883
+ const options: Completion[] = filtered.map((sd) => {
1884
+ const url = sd.url ?? sd.type;
1885
+ return {
1886
+ label: url,
1887
+ ...(sd.name ? { info: sd.name } : {}),
1888
+ type: "text",
1889
+ apply: (
1890
+ view: EditorView,
1891
+ _c: Completion,
1892
+ applyFrom: number,
1893
+ applyTo: number,
1894
+ ) => {
1895
+ const d = view.state.doc.toString();
1896
+ let actualTo = applyTo;
1897
+ if (actualTo < d.length && d[actualTo] === '"') actualTo++;
1898
+ view.dispatch({
1899
+ changes: { from: applyFrom, to: actualTo, insert: `"${url}"` },
1900
+ selection: { anchor: applyFrom + url.length + 2 },
1901
+ });
1902
+ },
1903
+ };
1904
+ });
1905
+ if (options.length > 0) {
1906
+ return { from, options, filter: false };
1907
+ }
1908
+ }
1909
+ }
1910
+ if (targetType) return null;
1911
+ return null;
1912
+ }
1913
+
1914
+ // ── Property completion ────────────────────────────────────────────────
1915
+
1916
+ async function handlePropertyCompletion(
1917
+ effectivePath: string[],
1918
+ resourceType: string | undefined,
1919
+ hasExplicitResourceType: boolean,
1920
+ doc: string,
1921
+ pos: number,
1922
+ getSDs: GetStructureDefinitions,
1923
+ completionContext: CompletionContext,
1924
+ ctx: DocumentContext,
1925
+ ): Promise<CompletionResult | null> {
1926
+ const line = completionContext.state.doc.lineAt(pos);
1927
+ const beforeCursor = line.text.slice(0, pos - line.from).trimStart();
1928
+
1929
+ // Only auto-trigger property completions when user has started typing
1930
+ if (
1931
+ !completionContext.explicit &&
1932
+ /,\s*"?\s*$/.test(beforeCursor) &&
1933
+ !completionContext.matchBefore(/\w+/)
1934
+ )
1935
+ return null;
1936
+
1937
+ const makeJsonRtCompletion = (): Completion => {
1938
+ const c: Completion = {
1939
+ label: "resourceType",
1940
+ type: "property",
1941
+ detail: "string",
1942
+ boost: 10,
1943
+ apply: (view, _completion, from, to) => {
1944
+ const d = view.state.doc.toString();
1945
+ let actualFrom = from;
1946
+ let actualTo = to;
1947
+ if (actualFrom > 0 && d[actualFrom - 1] === '"') actualFrom--;
1948
+ if (actualTo < d.length && d[actualTo] === '"') actualTo++;
1949
+ const text = '"resourceType": ""';
1950
+ view.dispatch({
1951
+ changes: { from: actualFrom, to: actualTo, insert: text },
1952
+ selection: { anchor: actualFrom + text.length - 1 },
1953
+ });
1954
+ },
1955
+ };
1956
+ c.info = "FHIR resource type";
1957
+ return c;
1958
+ };
1959
+
1960
+ let completions: Completion[];
1961
+ if (resourceType) {
1962
+ const elements = await resolveElements(effectivePath, resourceType, getSDs);
1963
+ const isParams = await isParametersType(resourceType, getSDs);
1964
+ const mapFn = isParams
1965
+ ? (el: FhirElement) => toParameterPropertyCompletion(el)
1966
+ : toCompletion;
1967
+ completions = elementsToCompletions(elements, mapFn);
1968
+ if (!hasExplicitResourceType && effectivePath.length === 0) {
1969
+ completions = [makeJsonRtCompletion(), ...completions];
1970
+ }
1971
+ } else if (effectivePath.length === 0) {
1972
+ const domainElements = await resolveElements(
1973
+ effectivePath,
1974
+ "DomainResource",
1975
+ getSDs,
1976
+ );
1977
+ completions = [
1978
+ makeJsonRtCompletion(),
1979
+ ...elementsToCompletions(domainElements, toCompletion),
1980
+ ];
1981
+ } else {
1982
+ return null;
1983
+ }
1984
+
1985
+ // Filter out properties already present in current object
1986
+ const existingKeys = new Set(ctx.getScope(0).getKeys());
1987
+ completions = completions.filter((c) => !existingKeys.has(c.label));
1988
+
1989
+ if (completions.length === 0) return null;
1990
+
1991
+ const word = completionContext.matchBefore(/"?\w*/);
1992
+ let from = word?.from ?? pos;
1993
+ if (from < doc.length && doc[from] === '"') from++;
1994
+
1995
+ return { from, options: completions, validFor: /^\w*$/ };
1996
+ }
1997
+
1998
+ // ── Thin wrapper ───────────────────────────────────────────────────────
1999
+
2000
+ /** @internal — exported for tests only */
2001
+ export function jsonCompletionSource(
2002
+ getSDs: GetStructureDefinitions,
2003
+ resourceTypeHint?: string,
2004
+ expandValueSet?: ExpandValueSet,
2005
+ ): CompletionSource {
2006
+ return async (cc: CompletionContext): Promise<CompletionResult | null> => {
2007
+ const ctx = buildJsonDocumentContext(cc.state.doc.toString(), cc.pos);
2008
+ return fhirComplete(ctx, getSDs, resourceTypeHint, expandValueSet, cc);
2009
+ };
2010
+ }
2011
+
2012
+ // ── Validation ─────────────────────────────────────────────────────────
2013
+
2014
+ type FhirDiagnostic = {
2015
+ from: number;
2016
+ to: number;
2017
+ message: string;
2018
+ };
2019
+
2020
+ async function validateFhirProperties(
2021
+ properties: PropertyInfo[],
2022
+ getSDs: GetStructureDefinitions,
2023
+ ): Promise<FhirDiagnostic[]> {
2024
+ const groups = new Map<
2025
+ string,
2026
+ { resourceType: string; path: string[]; props: PropertyInfo[] }
2027
+ >();
2028
+ for (const prop of properties) {
2029
+ const key = `${prop.resourceType}|${prop.path.join(".")}`;
2030
+ let group = groups.get(key);
2031
+ if (!group) {
2032
+ group = {
2033
+ resourceType: prop.resourceType,
2034
+ path: [...prop.path],
2035
+ props: [],
2036
+ };
2037
+ groups.set(key, group);
2038
+ }
2039
+ group.props.push(prop);
2040
+ }
2041
+
2042
+ const diagnostics: FhirDiagnostic[] = [];
2043
+
2044
+ for (const { resourceType, path, props } of groups.values()) {
2045
+ const elements = await resolveElements(path, resourceType, getSDs);
2046
+ if (elements.length === 0) continue;
2047
+
2048
+ const validNames = new Set<string>();
2049
+ for (const el of elements) {
2050
+ const name = fieldName(el);
2051
+ validNames.add(name);
2052
+ const typeCode = el.type?.[0]?.code;
2053
+ if (el.type?.length === 1 && typeCode && isPrimitiveType(typeCode)) {
2054
+ validNames.add(`_${name}`);
2055
+ }
2056
+ }
2057
+ if (path.length === 0) {
2058
+ validNames.add("resourceType");
2059
+ }
2060
+
2061
+ for (const prop of props) {
2062
+ if (!validNames.has(prop.name)) {
2063
+ diagnostics.push({
2064
+ from: prop.from,
2065
+ to: prop.to,
2066
+ message: `Unknown property "${prop.name}"`,
2067
+ });
2068
+ }
2069
+ }
2070
+ }
2071
+
2072
+ return diagnostics;
2073
+ }
2074
+
2075
+ function buildFhirValidationPlugin(
2076
+ getSDs: GetStructureDefinitions,
2077
+ resourceTypeHint?: string,
2078
+ ): Extension {
2079
+ return ViewPlugin.define((view) => {
2080
+ let timeout: ReturnType<typeof setTimeout> | null = null;
2081
+ let destroyed = false;
2082
+
2083
+ function hasActiveDiagnostics() {
2084
+ try {
2085
+ return view.state.field(fhirDiagnosticsField).messages.size > 0;
2086
+ } catch {
2087
+ return false;
2088
+ }
2089
+ }
2090
+
2091
+ function scheduleCheck() {
2092
+ if (timeout) clearTimeout(timeout);
2093
+ const delay = hasActiveDiagnostics() ? 0 : 1500;
2094
+ timeout = setTimeout(() => check(), delay);
2095
+ }
2096
+
2097
+ async function check() {
2098
+ if (destroyed) return;
2099
+ const currentDoc = view.state.doc.toString();
2100
+ const tree =
2101
+ ensureSyntaxTree(view.state, view.state.doc.length, 1000) ??
2102
+ syntaxTree(view.state);
2103
+
2104
+ const { properties, emptyStrings } = walkJsonProperties(
2105
+ currentDoc,
2106
+ tree,
2107
+ resourceTypeHint ?? null,
2108
+ );
2109
+
2110
+ if (!findRootJsonObject(currentDoc, tree)) {
2111
+ try {
2112
+ view.dispatch({ effects: setFhirDiagnosticsEffect.of([]) });
2113
+ } catch {
2114
+ /* view destroyed */
2115
+ }
2116
+ return;
2117
+ }
2118
+
2119
+ if (properties.length === 0 && emptyStrings.length === 0) {
2120
+ try {
2121
+ view.dispatch({ effects: setFhirDiagnosticsEffect.of([]) });
2122
+ } catch {
2123
+ /* view destroyed */
2124
+ }
2125
+ return;
2126
+ }
2127
+
2128
+ const rawDiags = await validateFhirProperties(properties, getSDs);
2129
+ if (destroyed) return;
2130
+ if (view.state.doc.toString() !== currentDoc) return;
2131
+
2132
+ // for (const es of emptyStrings) {
2133
+ // rawDiags.push({
2134
+ // from: es.from,
2135
+ // to: es.to,
2136
+ // message: "Value must not be empty",
2137
+ // });
2138
+ // }
2139
+
2140
+ const diags: FhirDiagnosticWithLine[] = rawDiags.map((d) => ({
2141
+ ...d,
2142
+ line: view.state.doc.lineAt(d.from).number,
2143
+ }));
2144
+
2145
+ try {
2146
+ view.dispatch({ effects: setFhirDiagnosticsEffect.of(diags) });
2147
+ } catch {
2148
+ /* view destroyed */
2149
+ }
2150
+ }
2151
+
2152
+ scheduleCheck();
2153
+
2154
+ return {
2155
+ update(update: ViewUpdate) {
2156
+ if (update.docChanged) {
2157
+ scheduleCheck();
2158
+ }
2159
+ },
2160
+ destroy() {
2161
+ destroyed = true;
2162
+ if (timeout) clearTimeout(timeout);
2163
+ },
2164
+ };
2165
+ });
2166
+ }
2167
+
2168
+ // ── FHIR validation decorations ───────────────────────────────────────
2169
+
2170
+ type FhirDiagnosticWithLine = FhirDiagnostic & { line: number };
2171
+
2172
+ const setFhirDiagnosticsEffect = StateEffect.define<FhirDiagnosticWithLine[]>();
2173
+
2174
+ const fhirUnderline = Decoration.mark({ class: "cm-fhir-error-underline" });
2175
+ const fhirErrorLineDecoration = Decoration.line({ class: "cm-errorLine" });
2176
+
2177
+ class FhirGutterMarker extends GutterMarker {
2178
+ elementClass = "cm-errorLineGutter";
2179
+ }
2180
+ const fhirGutterMarker = new FhirGutterMarker();
2181
+
2182
+ export const fhirDiagnosticsField = StateField.define<{
2183
+ marks: RangeSet<Decoration>;
2184
+ lineDecos: RangeSet<Decoration>;
2185
+ gutterMarkers: RangeSet<GutterMarker>;
2186
+ messages: Map<number, string>;
2187
+ }>({
2188
+ create() {
2189
+ return {
2190
+ marks: Decoration.none,
2191
+ lineDecos: Decoration.none,
2192
+ gutterMarkers: RangeSet.empty,
2193
+ messages: new Map(),
2194
+ };
2195
+ },
2196
+ update(value, tr) {
2197
+ for (const effect of tr.effects) {
2198
+ if (effect.is(setFhirDiagnosticsEffect)) {
2199
+ const diags = effect.value;
2200
+ if (diags.length === 0) {
2201
+ return {
2202
+ marks: Decoration.none,
2203
+ lineDecos: Decoration.none,
2204
+ gutterMarkers: RangeSet.empty,
2205
+ messages: new Map(),
2206
+ };
2207
+ }
2208
+
2209
+ const marks: { from: number; to: number; value: Decoration }[] = [];
2210
+ const lineDecos: { from: number; to: number; value: Decoration }[] = [];
2211
+ const gutter: { from: number; to: number; value: GutterMarker }[] = [];
2212
+ const messages = new Map<number, string>();
2213
+
2214
+ for (const d of diags) {
2215
+ marks.push(fhirUnderline.range(d.from, d.to));
2216
+ const existing = messages.get(d.line);
2217
+ if (existing) {
2218
+ messages.set(d.line, `${existing}\n${d.message}`);
2219
+ } else {
2220
+ messages.set(d.line, d.message);
2221
+ const line = tr.state.doc.line(d.line);
2222
+ lineDecos.push(fhirErrorLineDecoration.range(line.from));
2223
+ gutter.push(fhirGutterMarker.range(line.from));
2224
+ }
2225
+ }
2226
+
2227
+ return {
2228
+ marks: Decoration.set(marks, true),
2229
+ lineDecos: Decoration.set(lineDecos, true),
2230
+ gutterMarkers: RangeSet.of(gutter, true),
2231
+ messages,
2232
+ };
2233
+ }
2234
+ }
2235
+ if (tr.docChanged) {
2236
+ try {
2237
+ return {
2238
+ marks: value.marks.map(tr.changes),
2239
+ lineDecos: value.lineDecos.map(tr.changes),
2240
+ gutterMarkers: value.gutterMarkers.map(tr.changes),
2241
+ messages: value.messages,
2242
+ };
2243
+ } catch {
2244
+ return {
2245
+ marks: Decoration.none,
2246
+ lineDecos: Decoration.none,
2247
+ gutterMarkers: RangeSet.empty,
2248
+ messages: new Map(),
2249
+ };
2250
+ }
2251
+ }
2252
+ return value;
2253
+ },
2254
+ provide(field) {
2255
+ return [
2256
+ EditorView.decorations.from(field, (v) => v.marks),
2257
+ EditorView.decorations.from(field, (v) => v.lineDecos),
2258
+ gutterLineClass.from(field, (v) => v.gutterMarkers),
2259
+ ];
2260
+ },
2261
+ });
2262
+
2263
+ const fhirLinterTheme = EditorView.theme({
2264
+ ".cm-fhir-error-underline": {
2265
+ textDecorationLine: "underline",
2266
+ textDecorationStyle: "wavy",
2267
+ textDecorationColor: "var(--color-text-error-primary)",
2268
+ textUnderlineOffset: "3px",
2269
+ },
2270
+ ".cm-lineNumbers .cm-gutterElement.cm-errorLineGutter": {
2271
+ color: "var(--color-text-error-primary)",
2272
+ backgroundColor:
2273
+ "color-mix(in srgb, var(--color-text-error-primary) 7%, transparent)",
2274
+ },
2275
+ });
2276
+
2277
+ // ── Public API ─────────────────────────────────────────────────────────
2278
+
2279
+ export function buildFhirCompletionExtension(
2280
+ getSDs: GetStructureDefinitions,
2281
+ resourceTypeHint?: string,
2282
+ expandValueSet?: ExpandValueSet,
2283
+ ): Extension {
2284
+ const jsonSource = jsonCompletionSource(
2285
+ getSDs,
2286
+ resourceTypeHint,
2287
+ expandValueSet,
2288
+ );
2289
+
2290
+ const autoTrigger = EditorView.updateListener.of((update) => {
2291
+ if (!update.docChanged) return;
2292
+ if (completionStatus(update.view.state)) return;
2293
+ const { state } = update.view;
2294
+ const pos = state.selection.main.head;
2295
+ const doc = state.doc.toString();
2296
+ const line = state.doc.lineAt(pos);
2297
+ const beforeCursor = line.text.slice(0, pos - line.from).trimStart();
2298
+ // Trigger on empty lines (including after snippet insertion),
2299
+ // after [ (array open), or after " (string value start)
2300
+ const shouldTrigger =
2301
+ beforeCursor === "" ||
2302
+ (pos > 0 && doc[pos - 1] === "[") ||
2303
+ (pos > 0 && doc[pos - 1] === '"' && pos > 1 && doc[pos - 2] !== "\\");
2304
+ if (!shouldTrigger) return;
2305
+ // Skip bulk replacements (e.g. tab switch, currentValue update)
2306
+ // but allow snippets — check only if the ENTIRE doc was replaced
2307
+ let totalInserted = 0;
2308
+ update.changes.iterChanges((_fA, _tA, _fB, _tB, ins) => {
2309
+ totalInserted += ins.length;
2310
+ });
2311
+ if (totalInserted > doc.length * 0.5) return;
2312
+ setTimeout(() => startCompletion(update.view), 0);
2313
+ });
2314
+
2315
+ return [
2316
+ jsonLanguage.data.of({ autocomplete: jsonSource }),
2317
+ autoTrigger,
2318
+ fhirDiagnosticsField,
2319
+ fhirLinterTheme,
2320
+ buildFhirValidationPlugin(getSDs, resourceTypeHint),
2321
+ ];
2322
+ }