@qoretechnologies/reqraft 0.10.53 → 0.10.54-pr.115.g9bc96ed

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 (283) hide show
  1. package/.claude/CLAUDE.md +25 -0
  2. package/.tasks/INDEX.md +1 -0
  3. package/.tasks/STORY_NETWORK_ISOLATION.md +98 -0
  4. package/README.md +1 -1
  5. package/design/FORM_ENGINE_EXPRESSIONS.md +110 -0
  6. package/design/FORM_ENGINE_FIELD_MIGRATION.md +56 -1
  7. package/design/RENDER_EXPRESSION_TRANSPORT.md +76 -10
  8. package/design/SMART_EDITOR_UX.md +21 -0
  9. package/dist/components/dpqlEditor/DpqlEditor.d.ts.map +1 -1
  10. package/dist/components/dpqlEditor/DpqlEditor.js +3 -3
  11. package/dist/components/dpqlEditor/DpqlEditor.js.map +1 -1
  12. package/dist/components/dpqlEditor/dpqlHelpers.d.ts +6 -0
  13. package/dist/components/dpqlEditor/dpqlHelpers.d.ts.map +1 -1
  14. package/dist/components/dpqlEditor/dpqlHelpers.js +21 -2
  15. package/dist/components/dpqlEditor/dpqlHelpers.js.map +1 -1
  16. package/dist/components/dpqlEditor/dpqlProbe.d.ts +37 -0
  17. package/dist/components/dpqlEditor/dpqlProbe.d.ts.map +1 -0
  18. package/dist/components/dpqlEditor/dpqlProbe.js +203 -0
  19. package/dist/components/dpqlEditor/dpqlProbe.js.map +1 -0
  20. package/dist/components/dpqlEditor/dpqlTags.d.ts +8 -0
  21. package/dist/components/dpqlEditor/dpqlTags.d.ts.map +1 -1
  22. package/dist/components/dpqlEditor/dpqlTags.js +5 -1
  23. package/dist/components/dpqlEditor/dpqlTags.js.map +1 -1
  24. package/dist/components/dpqlEditor/index.d.ts +4 -0
  25. package/dist/components/dpqlEditor/index.d.ts.map +1 -1
  26. package/dist/components/dpqlEditor/index.js +5 -1
  27. package/dist/components/dpqlEditor/index.js.map +1 -1
  28. package/dist/components/dpqlEditor/types.d.ts +42 -2
  29. package/dist/components/dpqlEditor/types.d.ts.map +1 -1
  30. package/dist/components/dpqlEditor/types.js +0 -4
  31. package/dist/components/dpqlEditor/types.js.map +1 -1
  32. package/dist/components/dpqlEditor/useDpqlProbe.d.ts +4 -0
  33. package/dist/components/dpqlEditor/useDpqlProbe.d.ts.map +1 -0
  34. package/dist/components/dpqlEditor/useDpqlProbe.js +29 -0
  35. package/dist/components/dpqlEditor/useDpqlProbe.js.map +1 -0
  36. package/dist/components/dpqlEditor/useDpqlSession.d.ts +5 -2
  37. package/dist/components/dpqlEditor/useDpqlSession.d.ts.map +1 -1
  38. package/dist/components/dpqlEditor/useDpqlSession.js +8 -2
  39. package/dist/components/dpqlEditor/useDpqlSession.js.map +1 -1
  40. package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
  41. package/dist/components/form/engine/CompactRow.js +192 -45
  42. package/dist/components/form/engine/CompactRow.js.map +1 -1
  43. package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
  44. package/dist/components/form/engine/CompactToolbar.js +5 -2
  45. package/dist/components/form/engine/CompactToolbar.js.map +1 -1
  46. package/dist/components/form/engine/FormEngine.d.ts +160 -12
  47. package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
  48. package/dist/components/form/engine/FormEngine.js +376 -75
  49. package/dist/components/form/engine/FormEngine.js.map +1 -1
  50. package/dist/components/form/engine/FormFieldsSkeleton.d.ts +48 -0
  51. package/dist/components/form/engine/FormFieldsSkeleton.d.ts.map +1 -0
  52. package/dist/components/form/engine/FormFieldsSkeleton.js +38 -0
  53. package/dist/components/form/engine/FormFieldsSkeleton.js.map +1 -0
  54. package/dist/components/form/engine/OptionFieldMessages.d.ts +17 -2
  55. package/dist/components/form/engine/OptionFieldMessages.d.ts.map +1 -1
  56. package/dist/components/form/engine/OptionFieldMessages.js +61 -11
  57. package/dist/components/form/engine/OptionFieldMessages.js.map +1 -1
  58. package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -1
  59. package/dist/components/form/engine/_structuredData/SchemaDataView.js +7 -1
  60. package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -1
  61. package/dist/components/form/engine/compactToolbarContext.d.ts +1 -0
  62. package/dist/components/form/engine/compactToolbarContext.d.ts.map +1 -1
  63. package/dist/components/form/engine/readFirst.d.ts +42 -1
  64. package/dist/components/form/engine/readFirst.d.ts.map +1 -1
  65. package/dist/components/form/engine/readFirst.js +74 -7
  66. package/dist/components/form/engine/readFirst.js.map +1 -1
  67. package/dist/components/form/engine/rendererTypes.d.ts +13 -0
  68. package/dist/components/form/engine/rendererTypes.d.ts.map +1 -1
  69. package/dist/components/form/engine/rendererTypes.js.map +1 -1
  70. package/dist/components/form/engine/rowMenuContext.d.ts +78 -0
  71. package/dist/components/form/engine/rowMenuContext.d.ts.map +1 -0
  72. package/dist/components/form/engine/rowMenuContext.js +103 -0
  73. package/dist/components/form/engine/rowMenuContext.js.map +1 -0
  74. package/dist/components/form/engine/typeChoices.d.ts +37 -0
  75. package/dist/components/form/engine/typeChoices.d.ts.map +1 -0
  76. package/dist/components/form/engine/typeChoices.js +25 -0
  77. package/dist/components/form/engine/typeChoices.js.map +1 -0
  78. package/dist/components/form/expressions/DpqlRendering.d.ts +26 -0
  79. package/dist/components/form/expressions/DpqlRendering.d.ts.map +1 -0
  80. package/dist/components/form/expressions/DpqlRendering.js +53 -0
  81. package/dist/components/form/expressions/DpqlRendering.js.map +1 -0
  82. package/dist/components/form/expressions/ExpressionField.d.ts +7 -1
  83. package/dist/components/form/expressions/ExpressionField.d.ts.map +1 -1
  84. package/dist/components/form/expressions/ExpressionField.js +202 -45
  85. package/dist/components/form/expressions/ExpressionField.js.map +1 -1
  86. package/dist/components/form/expressions/argumentPresence.d.ts +97 -0
  87. package/dist/components/form/expressions/argumentPresence.d.ts.map +1 -0
  88. package/dist/components/form/expressions/argumentPresence.js +203 -0
  89. package/dist/components/form/expressions/argumentPresence.js.map +1 -0
  90. package/dist/components/form/expressions/builder/argumentLabel.d.ts.map +1 -1
  91. package/dist/components/form/expressions/builder/argumentLabel.js +9 -4
  92. package/dist/components/form/expressions/builder/argumentLabel.js.map +1 -1
  93. package/dist/components/form/expressions/builder/index.d.ts +15 -2
  94. package/dist/components/form/expressions/builder/index.d.ts.map +1 -1
  95. package/dist/components/form/expressions/builder/index.js +20 -58
  96. package/dist/components/form/expressions/builder/index.js.map +1 -1
  97. package/dist/components/form/expressions/builder/renderTemplate.d.ts +4 -1
  98. package/dist/components/form/expressions/builder/renderTemplate.d.ts.map +1 -1
  99. package/dist/components/form/expressions/builder/renderTemplate.js +10 -67
  100. package/dist/components/form/expressions/builder/renderTemplate.js.map +1 -1
  101. package/dist/components/form/expressions/dpqlMockLanguage.d.ts +76 -0
  102. package/dist/components/form/expressions/dpqlMockLanguage.d.ts.map +1 -0
  103. package/dist/components/form/expressions/dpqlMockLanguage.js +525 -0
  104. package/dist/components/form/expressions/dpqlMockLanguage.js.map +1 -0
  105. package/dist/components/form/expressions/dpqlMockLsp.d.ts +31 -2
  106. package/dist/components/form/expressions/dpqlMockLsp.d.ts.map +1 -1
  107. package/dist/components/form/expressions/dpqlMockLsp.js +86 -80
  108. package/dist/components/form/expressions/dpqlMockLsp.js.map +1 -1
  109. package/dist/components/form/expressions/mockExpressions.d.ts.map +1 -1
  110. package/dist/components/form/expressions/mockExpressions.js +8 -4
  111. package/dist/components/form/expressions/mockExpressions.js.map +1 -1
  112. package/dist/components/form/expressions/renderExpressionToText.d.ts +1 -1
  113. package/dist/components/form/expressions/renderExpressionToText.d.ts.map +1 -1
  114. package/dist/components/form/expressions/renderExpressionToText.js +95 -19
  115. package/dist/components/form/expressions/renderExpressionToText.js.map +1 -1
  116. package/dist/components/form/expressions/types.d.ts +7 -0
  117. package/dist/components/form/expressions/types.d.ts.map +1 -1
  118. package/dist/components/form/expressions/useRenderExpression.d.ts +7 -16
  119. package/dist/components/form/expressions/useRenderExpression.d.ts.map +1 -1
  120. package/dist/components/form/expressions/useRenderExpression.js +50 -126
  121. package/dist/components/form/expressions/useRenderExpression.js.map +1 -1
  122. package/dist/components/form/fields/Field.d.ts.map +1 -1
  123. package/dist/components/form/fields/Field.js +3 -1
  124. package/dist/components/form/fields/Field.js.map +1 -1
  125. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts +11 -1
  126. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
  127. package/dist/components/form/fields/allowed-values/AllowedValues.js +14 -2
  128. package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
  129. package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
  130. package/dist/components/form/fields/auto/AutoFormField.js +32 -8
  131. package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
  132. package/dist/components/form/fields/date/Date.d.ts +1 -1
  133. package/dist/components/form/fields/date/Date.d.ts.map +1 -1
  134. package/dist/components/form/fields/date/Date.js +16 -2
  135. package/dist/components/form/fields/date/Date.js.map +1 -1
  136. package/dist/components/form/fields/rich-text/RichText.d.ts +21 -1
  137. package/dist/components/form/fields/rich-text/RichText.d.ts.map +1 -1
  138. package/dist/components/form/fields/rich-text/RichText.js +140 -8
  139. package/dist/components/form/fields/rich-text/RichText.js.map +1 -1
  140. package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts +2 -1
  141. package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts.map +1 -1
  142. package/dist/components/form/fields/template/ReadOnlyTemplateTag.js +3 -2
  143. package/dist/components/form/fields/template/ReadOnlyTemplateTag.js.map +1 -1
  144. package/dist/components/form/fields/template/TemplateField.d.ts +31 -1
  145. package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
  146. package/dist/components/form/fields/template/TemplateField.js +528 -50
  147. package/dist/components/form/fields/template/TemplateField.js.map +1 -1
  148. package/dist/components/form/index.d.ts +1 -0
  149. package/dist/components/form/index.d.ts.map +1 -1
  150. package/dist/components/form/index.js +1 -0
  151. package/dist/components/form/index.js.map +1 -1
  152. package/dist/components/smartEditor/SmartEditor.d.ts.map +1 -1
  153. package/dist/components/smartEditor/SmartEditor.js +29 -8
  154. package/dist/components/smartEditor/SmartEditor.js.map +1 -1
  155. package/dist/components/smartEditor/__fixtures__/mockLspServer.d.ts +8 -0
  156. package/dist/components/smartEditor/__fixtures__/mockLspServer.d.ts.map +1 -1
  157. package/dist/components/smartEditor/__fixtures__/mockLspServer.js +53 -10
  158. package/dist/components/smartEditor/__fixtures__/mockLspServer.js.map +1 -1
  159. package/dist/components/smartEditor/useLspSession.d.ts +5 -1
  160. package/dist/components/smartEditor/useLspSession.d.ts.map +1 -1
  161. package/dist/components/smartEditor/useLspSession.js +16 -2
  162. package/dist/components/smartEditor/useLspSession.js.map +1 -1
  163. package/dist/helpers/dpqlDetection.d.ts +58 -0
  164. package/dist/helpers/dpqlDetection.d.ts.map +1 -0
  165. package/dist/helpers/dpqlDetection.js +164 -0
  166. package/dist/helpers/dpqlDetection.js.map +1 -0
  167. package/dist/helpers/optionUiTypes.d.ts +3 -3
  168. package/dist/helpers/options.d.ts +37 -0
  169. package/dist/helpers/options.d.ts.map +1 -1
  170. package/dist/helpers/options.js +78 -1
  171. package/dist/helpers/options.js.map +1 -1
  172. package/dist/helpers/templateItems.d.ts +47 -0
  173. package/dist/helpers/templateItems.d.ts.map +1 -0
  174. package/dist/helpers/templateItems.js +88 -0
  175. package/dist/helpers/templateItems.js.map +1 -0
  176. package/dist/helpers/templateText.d.ts +37 -0
  177. package/dist/helpers/templateText.d.ts.map +1 -0
  178. package/dist/helpers/templateText.js +64 -0
  179. package/dist/helpers/templateText.js.map +1 -0
  180. package/dist/helpers/templates.d.ts +19 -11
  181. package/dist/helpers/templates.d.ts.map +1 -1
  182. package/dist/helpers/templates.js +46 -29
  183. package/dist/helpers/templates.js.map +1 -1
  184. package/dist/helpers/validations.d.ts.map +1 -1
  185. package/dist/helpers/validations.js +56 -9
  186. package/dist/helpers/validations.js.map +1 -1
  187. package/dist/hooks/useArgSchema.d.ts +2 -1
  188. package/dist/hooks/useArgSchema.d.ts.map +1 -1
  189. package/dist/hooks/useFetch/useFetch.d.ts +3 -1
  190. package/dist/hooks/useFetch/useFetch.d.ts.map +1 -1
  191. package/dist/hooks/useFetch/useFetch.js +47 -9
  192. package/dist/hooks/useFetch/useFetch.js.map +1 -1
  193. package/dist/hooks/useTemplates.d.ts +20 -0
  194. package/dist/hooks/useTemplates.d.ts.map +1 -1
  195. package/dist/hooks/useTemplates.js +34 -2
  196. package/dist/hooks/useTemplates.js.map +1 -1
  197. package/dist/index.d.ts +2 -1
  198. package/dist/index.d.ts.map +1 -1
  199. package/dist/index.js +5 -1
  200. package/dist/index.js.map +1 -1
  201. package/dist/stories/storyNetwork.d.ts +36 -0
  202. package/dist/stories/storyNetwork.d.ts.map +1 -0
  203. package/dist/stories/storyNetwork.js +82 -0
  204. package/dist/stories/storyNetwork.js.map +1 -0
  205. package/dist/utils/fetch.d.ts +151 -1
  206. package/dist/utils/fetch.d.ts.map +1 -1
  207. package/dist/utils/fetch.js +256 -44
  208. package/dist/utils/fetch.js.map +1 -1
  209. package/dist/utils/lspClient.d.ts +6 -0
  210. package/dist/utils/lspClient.d.ts.map +1 -1
  211. package/dist/utils/lspClient.js +62 -11
  212. package/dist/utils/lspClient.js.map +1 -1
  213. package/dist/utils/websocket.d.ts +6 -0
  214. package/dist/utils/websocket.d.ts.map +1 -1
  215. package/dist/utils/websocket.js +15 -7
  216. package/dist/utils/websocket.js.map +1 -1
  217. package/package.json +2 -2
  218. package/src/components/dpqlEditor/DpqlEditor.stories.tsx +3 -119
  219. package/src/components/dpqlEditor/DpqlEditor.tsx +5 -2
  220. package/src/components/dpqlEditor/dpqlHelpers.ts +20 -1
  221. package/src/components/dpqlEditor/dpqlProbe.ts +165 -0
  222. package/src/components/dpqlEditor/dpqlTags.ts +13 -0
  223. package/src/components/dpqlEditor/index.ts +4 -0
  224. package/src/components/dpqlEditor/types.ts +38 -2
  225. package/src/components/dpqlEditor/useDpqlProbe.ts +33 -0
  226. package/src/components/dpqlEditor/useDpqlSession.ts +20 -4
  227. package/src/components/form/engine/CompactRow.tsx +240 -51
  228. package/src/components/form/engine/CompactToolbar.tsx +10 -3
  229. package/src/components/form/engine/FormEngine.stories.tsx +423 -120
  230. package/src/components/form/engine/FormEngine.tsx +504 -87
  231. package/src/components/form/engine/FormFieldsSkeleton.tsx +76 -0
  232. package/src/components/form/engine/OptionFieldMessages.tsx +89 -11
  233. package/src/components/form/engine/_structuredData/SchemaDataView.tsx +11 -0
  234. package/src/components/form/engine/compactToolbarContext.ts +1 -1
  235. package/src/components/form/engine/readFirst.ts +86 -7
  236. package/src/components/form/engine/rendererTypes.ts +12 -0
  237. package/src/components/form/engine/rowMenuContext.ts +159 -0
  238. package/src/components/form/engine/typeChoices.ts +55 -0
  239. package/src/components/form/expressions/DpqlRendering.tsx +85 -0
  240. package/src/components/form/expressions/ExpressionField.stories.tsx +826 -48
  241. package/src/components/form/expressions/ExpressionField.tsx +265 -54
  242. package/src/components/form/expressions/argumentPresence.ts +200 -0
  243. package/src/components/form/expressions/builder/ExpressionBuilder.stories.tsx +20 -4
  244. package/src/components/form/expressions/builder/argumentLabel.tsx +10 -4
  245. package/src/components/form/expressions/builder/index.tsx +39 -77
  246. package/src/components/form/expressions/builder/renderTemplate.tsx +23 -56
  247. package/src/components/form/expressions/dpqlMockLanguage.ts +565 -0
  248. package/src/components/form/expressions/dpqlMockLsp.ts +109 -83
  249. package/src/components/form/expressions/mockExpressions.ts +8 -4
  250. package/src/components/form/expressions/renderExpressionToText.ts +112 -22
  251. package/src/components/form/expressions/types.ts +7 -0
  252. package/src/components/form/expressions/useRenderExpression.ts +43 -127
  253. package/src/components/form/fields/Field.tsx +3 -2
  254. package/src/components/form/fields/allowed-values/AllowedValues.tsx +13 -2
  255. package/src/components/form/fields/auto/AutoFormField.stories.tsx +27 -20
  256. package/src/components/form/fields/auto/AutoFormField.tsx +33 -9
  257. package/src/components/form/fields/date/Date.tsx +17 -1
  258. package/src/components/form/fields/rich-text/RichText.tsx +199 -22
  259. package/src/components/form/fields/template/ReadOnlyTemplateTag.tsx +4 -2
  260. package/src/components/form/fields/template/TemplateField.stories.tsx +31 -9
  261. package/src/components/form/fields/template/TemplateField.tsx +714 -122
  262. package/src/components/form/index.tsx +1 -0
  263. package/src/components/log/Log.stories.tsx +2 -1
  264. package/src/components/smartEditor/SmartEditor.tsx +41 -9
  265. package/src/components/smartEditor/__fixtures__/mockLspServer.ts +52 -3
  266. package/src/components/smartEditor/useLspSession.ts +21 -3
  267. package/src/helpers/dpqlDetection.ts +209 -0
  268. package/src/helpers/options.ts +70 -0
  269. package/src/helpers/templateItems.ts +81 -0
  270. package/src/helpers/templateText.ts +92 -0
  271. package/src/helpers/templates.ts +47 -37
  272. package/src/helpers/validations.ts +59 -9
  273. package/src/hooks/useFetch/useFetch.stories.tsx +6 -5
  274. package/src/hooks/useFetch/useFetch.tsx +53 -7
  275. package/src/hooks/useStorage/useStorage.stories.tsx +2 -1
  276. package/src/hooks/useTemplates.ts +41 -0
  277. package/src/hooks/useWebSocket/useWebsocket.stories.tsx +2 -1
  278. package/src/index.tsx +14 -1
  279. package/src/stores/currentUser/currentUser.stories.tsx +2 -1
  280. package/src/stories/storyNetwork.ts +92 -0
  281. package/src/utils/fetch.ts +311 -21
  282. package/src/utils/lspClient.ts +68 -11
  283. package/src/utils/websocket.ts +20 -9
@@ -1,100 +1,126 @@
1
1
  // Copyright 2026 Qore Technologies, s.r.o.
2
- // A compact mock LSP for ExpressionField Text-mode stories. Registers just
3
- // enough of the DPQL contract on the shared harness plumbing
4
- // (`smartEditor/__fixtures__/mockLspServer`) to exercise the parse/serialize
5
- // boundary glue: `dpql/parse` echoes the typed text into an expression AST,
6
- // `dpql/serialize` reconstructs a string. (The DpqlEditor's own stories
7
- // carry the full mock; this is the minimal subset.)
2
+ // The DPQL language server every story talks to unless it brings its own.
3
+ //
4
+ // `.storybook/preview.tsx` starts it before each story, so an expression field,
5
+ // a Preview or an Explain panel finds a server on the endpoint whatever story it
6
+ // sits in — the same way `GLOBAL_STORY_MOCK_DATA` answers the REST requests
7
+ // components make on their own. A story that needs different handlers starts
8
+ // its own server on the URL and replaces this one (see `createMockLspServer`).
9
+ // The answers come from `dpqlMockLanguage`, which follows the real server.
8
10
  import {
9
11
  createMockLspServer,
12
+ DEFAULT_LSP_CAPABILITIES,
13
+ IMockLspServer,
10
14
  MOCK_LSP_URL,
15
+ TMockLspHandler,
11
16
  } from '../../smartEditor/__fixtures__/mockLspServer';
17
+ import {
18
+ IDpqlMockNode,
19
+ mockParseDpql,
20
+ mockRenderDpql,
21
+ mockSerializeDpql,
22
+ mockTokenizeDpql,
23
+ } from './dpqlMockLanguage';
12
24
 
13
- // Loose AST node shape the serialize/render handlers poke at — the mock
14
- // doesn't validate structure.
15
- interface IMockExprNode {
16
- exp?: string;
17
- args?: IMockExprNode[];
18
- value?: IMockExprNode;
19
- is_expression?: boolean;
25
+ /**
26
+ * Every `dpql/parse` the mock answered, in order, for diagnosis.
27
+ *
28
+ * The type-analysis stories depend on the front end SENDING `target_type` —
29
+ * the mock returns analysis only when it is present, exactly as the server
30
+ * behaves. When one of those stories fails there are two very different
31
+ * causes, and the rendered DOM cannot tell them apart: the request never
32
+ * carried a target, or it did and the answer was not rendered. This records
33
+ * which, and the story reports it in its assertion message so the answer
34
+ * survives into a CI log.
35
+ */
36
+ export interface IDpqlMockParseCall {
37
+ text: string;
38
+ target?: string;
39
+ analysed: boolean;
20
40
  }
41
+ export const dpqlMockParseCalls: IDpqlMockParseCall[] = [];
21
42
 
22
- /** Start the mock LSP; returns a teardown function. */
23
- export const startDpqlMockLsp = (): (() => void) => {
24
- const lsp = createMockLspServer(MOCK_LSP_URL, {
25
- capabilities: {
26
- textDocumentSync: { openClose: true, change: 2 },
27
- completionProvider: { triggerCharacters: ['@', '$', '.', ':'] },
28
- },
29
- // Unknown requests get an empty success so nothing hangs.
30
- defaultResult: {},
31
- handlers: {
32
- 'textDocument/completion': () => ({ isIncomplete: false, items: [] }),
43
+ /** The template namespaces the server offers after a `$`. */
44
+ const DPQL_MOCK_TEMPLATE_ITEMS = [
45
+ { label: '$data:', insertText: '$data:', kind: 1, detail: 'template' },
46
+ { label: '$config:', insertText: '$config:', kind: 1, detail: 'template' },
47
+ { label: '$static:', insertText: '$static:', kind: 1, detail: 'template' },
48
+ { label: '$timestamp:', insertText: '$timestamp:', kind: 1, detail: 'template' },
49
+ ];
50
+
51
+ /** The record fields the server offers after an `@`. */
52
+ const DPQL_MOCK_FIELD_ITEMS = [
53
+ { label: '@name', insertText: '@name', kind: 5, detail: 'string' },
54
+ { label: '@status', insertText: '@status', kind: 5, detail: 'string' },
55
+ ];
56
+
57
+ const DPQL_MOCK_HANDLERS: Record<string, TMockLspHandler> = {
58
+ /* Position-aware, as the real server is: `$` opens the template
59
+ namespaces, `@` the record fields. Answering the same list whatever
60
+ precedes the cursor would make a story asserting the `$` menu pass
61
+ for a shell that asked in the wrong context — which is the only thing
62
+ such a story is there to catch. (`DpqlEditor`'s own stories carry the
63
+ full catalogue; this is the minimal subset.) */
64
+ 'textDocument/completion': (msg, server) => {
65
+ const position = msg.params?.position ?? { line: 0, character: 0 };
66
+ const line = server.textOf(msg.params?.textDocument?.uri).split('\n')[position.line] ?? '';
67
+ const head = line.slice(0, position.character);
68
+ const sigil = head.match(/[@$][\w:.{}]*$/)?.[0]?.[0] ?? '';
69
+ if (sigil === '$') {
70
+ return { isIncomplete: false, items: DPQL_MOCK_TEMPLATE_ITEMS };
71
+ }
72
+ if (sigil === '@') {
73
+ return { isIncomplete: false, items: DPQL_MOCK_FIELD_ITEMS };
74
+ }
75
+ return { isIncomplete: false, items: [] };
76
+ },
77
+
78
+ 'textDocument/semanticTokens/full': (msg, server) => ({
79
+ data: mockTokenizeDpql(server.textOf(msg.params?.textDocument?.uri)),
80
+ }),
33
81
 
34
- 'textDocument/semanticTokens/full': () => ({ data: [] }),
82
+ 'dpql/setContext': () => ({ fields: {} }),
35
83
 
36
- 'dpql/setContext': () => ({ fields: {} }),
84
+ 'dpql/parse': (msg) => {
85
+ const text = String(msg.params?.text ?? '');
86
+ const target = msg.params?.target_type as string | undefined;
87
+ dpqlMockParseCalls.push({ text: text.trim(), target, analysed: !!target });
88
+ return mockParseDpql(text, target);
89
+ },
37
90
 
38
- // Echo the typed text into an `==` expression so the AST is
39
- // deterministic and verifiable.
40
- 'dpql/parse': (msg) => {
41
- const text = (msg.params?.text ?? '').trim();
42
- return {
43
- success: true,
44
- expression: {
45
- is_expression: true,
46
- value: {
47
- exp: '==',
48
- args: [
49
- { type: 'string', value: text || 'x' },
50
- { type: 'string', value: '' },
51
- ],
52
- },
53
- },
54
- diagnostics: [],
55
- };
56
- },
91
+ 'dpql/serialize': (msg) => mockSerializeDpql(msg.params?.expression as IDpqlMockNode),
57
92
 
58
- 'dpql/serialize': (msg) => {
59
- const expr = msg.params?.expression as IMockExprNode | undefined;
60
- const args = expr?.args ?? expr?.value?.args ?? [];
61
- const left = args[0]?.value ?? '';
62
- const right = args[1]?.value ?? '';
63
- return { dpql: `${left} == ${right}`.trim() };
64
- },
93
+ 'dpql/validate': () => ({ diagnostics: [] }),
65
94
 
66
- 'dpql/validate': () => ({ diagnostics: [] }),
95
+ 'dpql/renderExpression': (msg) => mockRenderDpql(msg.params?.expression as IDpqlMockNode),
96
+ };
97
+
98
+ export interface IStartDpqlMockLspOptions {
99
+ /** Handlers to use instead of, or beside, the default ones. */
100
+ handlers?: Record<string, TMockLspHandler>;
101
+ /** Per-method response delays in ms. */
102
+ delays?: Record<string, number>;
103
+ }
67
104
 
68
- // Mirrors the server contract: accepts the bare `{exp, args}` AST
69
- // or the `{is_expression, value}` wrapper; returns `{ rendered,
70
- // richtext }` where `rendered` is the human-readable form.
71
- 'dpql/renderExpression': (msg) => {
72
- const input = msg.params?.expression as IMockExprNode | undefined;
73
- const root = input?.value && !input?.exp ? input.value : input;
74
- const renderNode = (node: IMockExprNode): string => {
75
- if (!node?.exp) return '';
76
- const args = (node.args ?? []).map((arg) =>
77
- arg?.is_expression && arg.value?.exp
78
- ? `(${renderNode(arg.value)})`
79
- : typeof arg?.value === 'string'
80
- ? JSON.stringify(arg.value)
81
- : String(arg?.value)
82
- );
83
- return /^[^\w\s]+$/.test(node.exp)
84
- ? args.join(` ${node.exp} `)
85
- : `${node.exp}(${args.join(', ')})`;
86
- };
87
- const rendered = renderNode(root);
88
- return {
89
- rendered,
90
- richtext: {
91
- type: 'richtext',
92
- value: [{ type: 'paragraph', children: [{ text: rendered }] }],
93
- },
94
- };
95
- },
105
+ /** Start the mock DPQL language server; returns the server and a teardown. */
106
+ export const startDpqlMockLspServer = (
107
+ options: IStartDpqlMockLspOptions = {}
108
+ ): { lsp: IMockLspServer; stop: () => void } => {
109
+ dpqlMockParseCalls.length = 0;
110
+ const lsp = createMockLspServer(MOCK_LSP_URL, {
111
+ capabilities: {
112
+ ...DEFAULT_LSP_CAPABILITIES,
113
+ textDocumentSync: { openClose: true, change: 2 },
114
+ completionProvider: { triggerCharacters: ['@', '$', '.', ':'] },
96
115
  },
116
+ // Unknown requests get an empty success so nothing hangs.
117
+ defaultResult: {},
118
+ delays: options.delays,
119
+ handlers: { ...DPQL_MOCK_HANDLERS, ...options.handlers },
97
120
  });
98
-
99
- return () => lsp.close();
121
+ return { lsp, stop: () => lsp.close() };
100
122
  };
123
+
124
+ /** Start the mock DPQL language server; returns a teardown function. */
125
+ export const startDpqlMockLsp = (options?: IStartDpqlMockLspOptions): (() => void) =>
126
+ startDpqlMockLspServer(options).stop;
@@ -119,7 +119,8 @@ export const mockExpressions: IExpressionSchema[] = [
119
119
  return_type: 'bool',
120
120
  ui_return_type: 'bool',
121
121
  varargs: false,
122
- groups: ['String'],
122
+ groups: ['Comparison'],
123
+ render_template: '$arg[0] contains $arg[1]${arg[2] ? (ignore case):}',
123
124
  args: stringArgs('Substring', 'contains'),
124
125
  },
125
126
  {
@@ -133,7 +134,8 @@ export const mockExpressions: IExpressionSchema[] = [
133
134
  return_type: 'bool',
134
135
  ui_return_type: 'bool',
135
136
  varargs: false,
136
- groups: ['String'],
137
+ groups: ['Comparison'],
138
+ render_template: '$arg[0].startsWith($arg[1], $arg[2])',
137
139
  args: stringArgs('Start String', 'starts with'),
138
140
  },
139
141
  {
@@ -147,7 +149,8 @@ export const mockExpressions: IExpressionSchema[] = [
147
149
  return_type: 'bool',
148
150
  ui_return_type: 'bool',
149
151
  varargs: false,
150
- groups: ['String'],
152
+ groups: ['Comparison'],
153
+ render_template: '$arg[0].endsWith($arg[1], $arg[2])',
151
154
  args: stringArgs('End String', 'ends with'),
152
155
  },
153
156
  // --- Varargs (int) -------------------------------------------------------
@@ -242,7 +245,8 @@ export const mockExpressions: IExpressionSchema[] = [
242
245
  return_type: 'bool',
243
246
  ui_return_type: 'bool',
244
247
  varargs: false,
245
- groups: ['String'],
248
+ groups: ['Comparison'],
249
+ render_template: '$arg[0] =~ /$arg[1]/$arg[2]',
246
250
  args: [
247
251
  { name: 'softstring', display_name: 'String Value', ui_type: 'richtext', required: true },
248
252
  { name: 'string', display_name: 'Regular Expression', ui_type: 'string', required: true },
@@ -1,15 +1,77 @@
1
1
  // Copyright 2026 Qore Technologies, s.r.o.
2
- // Client-side expression renderer — the offline approximation of an expression
3
- // AST. Used as the fallback when the LSP "Explain" path
4
- // (`DataProvider::renderExpression`) is unreachable, and by read-first
5
- // summaries that must render synchronously. Pure: no transport/socket import,
6
- // so it stays out of the LSP/nanoid dependency graph.
2
+ // Client-side expression renderer — an approximation of an expression AST for
3
+ // the read-first row summaries, which must render synchronously. Nothing that
4
+ // can wait uses it: Explain and the Text view's Preview wait for the server's
5
+ // `dpql/renderExpression` (see `useRenderExpression`). Pure: no
6
+ // transport/socket import, so it stays out of the LSP/nanoid dependency graph.
7
7
  import { IExpression, IExpressionSchema, IExpressionValue } from './types';
8
8
 
9
9
  /** A symbol made only of non-word characters renders infix (`a == b`). */
10
10
  const isOperatorSymbol = (symbol?: string): boolean =>
11
11
  !!symbol && /^[^\w\s]+$/.test(symbol);
12
12
 
13
+ /**
14
+ * Operator precedence, lower binds tighter.
15
+ *
16
+ * Mirrors `DpqlOperatorPrecedence` in the Qore DataProvider module, which is
17
+ * the source of truth — the server renders the same ASTs through
18
+ * `DataProvider::renderExpression`, and a row summary that parenthesised
19
+ * differently would make the same expression read two ways depending on
20
+ * whether it was collapsed or open. (It does not model the catalogue's
21
+ * `render_template`s, so `contains` still reads as a call here.)
22
+ */
23
+ const PRECEDENCE: Record<string, number> = {
24
+ '*': 1,
25
+ '/': 1,
26
+ '%': 1,
27
+ '+': 2,
28
+ '-': 2,
29
+ '!': 3,
30
+ '==': 4,
31
+ '!=': 4,
32
+ '<': 4,
33
+ '<=': 4,
34
+ '>': 4,
35
+ '>=': 4,
36
+ '&&': 5,
37
+ '||': 6,
38
+ };
39
+
40
+ /** Anything that is not an operator; nothing binds tighter. */
41
+ const PREC_PRIMARY = 0;
42
+ /** Looser than every operator, so a value rendered in it is never wrapped. */
43
+ const PREC_LOOSEST = 7;
44
+
45
+ /**
46
+ * Operators whose grouping cannot change the result.
47
+ *
48
+ * `+` is deliberately absent: it concatenates when an operand is a string, so
49
+ * `1 + (2 + "a")` is `"12a"` where `1 + 2 + "a"` is `"3a"`. Dropping its
50
+ * parentheses would not be a cosmetic simplification.
51
+ */
52
+ const ASSOCIATIVE = new Set(['&&', '||']);
53
+
54
+ const precedenceOf = (exp?: string): number => PRECEDENCE[exp ?? ''] ?? PREC_PRIMARY;
55
+
56
+ /**
57
+ * The context an operator's argument is rendered in — the same rule as
58
+ * `DpqlSerializer::getArgumentContext()`.
59
+ *
60
+ * The first argument is where left-associative parsing puts a same-precedence
61
+ * child back, so it needs no parentheses; a later one does, or `1 - (2 - 3)`
62
+ * reads as `1 - 2 - 3`, which is a different expression.
63
+ */
64
+ const argumentContext = (exp: string, argn: number): number => {
65
+ const prec = precedenceOf(exp);
66
+ return !argn || ASSOCIATIVE.has(exp) ? prec : prec - 1;
67
+ };
68
+
69
+ /** An argument reaches this as a bare value or as a `{type, value}` envelope. */
70
+ const readArgValue = (arg: unknown): unknown =>
71
+ arg !== null && typeof arg === 'object' && 'value' in (arg as object)
72
+ ? (arg as { value: unknown }).value
73
+ : arg;
74
+
13
75
  const renderLiteral = (value: unknown): string => {
14
76
  if (value === undefined || value === null) return 'null';
15
77
  if (typeof value === 'string') return JSON.stringify(value);
@@ -17,31 +79,59 @@ const renderLiteral = (value: unknown): string => {
17
79
  return String(value);
18
80
  };
19
81
 
20
- /**
21
- * Recursively render an expression AST to readable text — the client-side
22
- * approximation, used as the fallback when the LSP is unreachable.
23
- */
24
- export const renderExpressionToText = (
82
+ /** Renders `value` and wraps it only if it binds looser than `contextPrec`. */
83
+ const renderInContext = (
25
84
  value: IExpressionValue | undefined,
26
- expressions: IExpressionSchema[] = []
85
+ expressions: IExpressionSchema[],
86
+ contextPrec: number
27
87
  ): string => {
28
88
  if (!value?.exp) return '';
89
+ /* A `value` expression IS its literal.
90
+
91
+ It is the wrapper the parser puts around a bare literal — typing `null`,
92
+ or `42`, into the text editor produces one — and it carries no symbol, so
93
+ rendering it through the generic paths printed the wrapper instead of the
94
+ value: `value(42)` with a catalogue that names it, `(42)` with one that
95
+ supplies its empty symbol. The server renders the literal alone, and a
96
+ summary that disagrees with the preview beside it is worse than either. */
97
+ if (value.exp === 'value' && (value.args ?? []).length === 1) {
98
+ const only = (value.args ?? [])[0] as IExpression;
99
+ if (!only?.is_expression) {
100
+ return renderLiteral(readArgValue(only));
101
+ }
102
+ }
29
103
  const schema = expressions.find((e) => e.name === value.exp);
30
104
  const symbol = schema?.symbol ?? value.exp;
31
- const args = (value.args ?? []).map((arg: IExpression) =>
105
+ const args = value.args ?? [];
106
+ // Logical group (subtype 2) → join with the symbol.
107
+ const isLogicalGroup = schema?.subtype === 2 || value.exp === '&&' || value.exp === '||';
108
+ // Binary operator → infix. Everything else renders as a call, whose own
109
+ // parentheses and commas already delimit its arguments.
110
+ const isInfix = isLogicalGroup || (args.length === 2 && isOperatorSymbol(symbol));
111
+
112
+ const parts = args.map((arg: IExpression, argn: number) =>
32
113
  arg?.is_expression && arg.value?.exp
33
- ? `(${renderExpressionToText(arg.value, expressions)})`
114
+ ? renderInContext(
115
+ arg.value,
116
+ expressions,
117
+ isInfix ? argumentContext(value.exp as string, argn) : PREC_LOOSEST
118
+ )
34
119
  : renderLiteral(arg?.value)
35
120
  );
36
121
 
37
- // Logical group (subtype 2) → join with the symbol.
38
- if (schema?.subtype === 2 || value.exp === '&&' || value.exp === '||') {
39
- return args.join(` ${symbol} `);
40
- }
41
- // Binary operator → infix.
42
- if (args.length === 2 && isOperatorSymbol(symbol)) {
43
- return `${args[0]} ${symbol} ${args[1]}`;
122
+ if (!isInfix) {
123
+ return `${symbol}(${parts.join(', ')})`;
44
124
  }
45
- // Otherwise function form.
46
- return `${symbol}(${args.join(', ')})`;
125
+
126
+ const text = isLogicalGroup ? parts.join(` ${symbol} `) : `${parts[0]} ${symbol} ${parts[1]}`;
127
+ return precedenceOf(value.exp) > contextPrec ? `(${text})` : text;
47
128
  };
129
+
130
+ /**
131
+ * Recursively render an expression AST to readable text — the client-side
132
+ * approximation a read-first row summary shows.
133
+ */
134
+ export const renderExpressionToText = (
135
+ value: IExpressionValue | undefined,
136
+ expressions: IExpressionSchema[] = []
137
+ ): string => renderInContext(value, expressions, PREC_LOOSEST);
@@ -68,6 +68,13 @@ export interface IExpressionSchema {
68
68
  return_type: TQorusType;
69
69
  ui_return_type: string;
70
70
  symbol: string;
71
+ /**
72
+ * How the server's readable rendering (`dpql/renderExpression`) spells this
73
+ * expression, when not by its symbol: `$arg[n]` is an argument, `$symbol` the
74
+ * symbol, `$args` all arguments, and `${arg[n] ? set : unset}` reads the
75
+ * argument or its `default_value` — e.g. `$arg[0].startsWith($arg[1], $arg[2])`.
76
+ */
77
+ render_template?: string;
71
78
  /** Server type code (operator/function); not a reliable infix discriminator. */
72
79
  type: number;
73
80
  /** `1` normal, `2` logical group (AND/OR). */