ompchamber 2.0.2 → 3.1.0

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 (419) hide show
  1. package/README.md +31 -11
  2. package/bunfig.toml +11 -0
  3. package/dist/client/index.html +16 -5
  4. package/dist/client/index.js +1393 -0
  5. package/dist/client/static/js/index-8t70jhyj.js +5149 -0
  6. package/dist/client/static/js/index-n4wkqj7t.css +1 -0
  7. package/index.html +54 -0
  8. package/package.json +9 -10
  9. package/public/apple-touch-icon.png +0 -0
  10. package/public/icon-192x192.png +0 -0
  11. package/public/icon.svg +60 -0
  12. package/public/manifest.webmanifest +38 -0
  13. package/src/cli/lib/commands/serve.js +9 -5
  14. package/src/cli/lib/runtime.js +18 -7
  15. package/src/client/components/common/code-editor/handler.test.ts +274 -0
  16. package/src/client/components/common/code-editor/handler.ts +175 -0
  17. package/src/client/components/common/code-editor/index.tsx +185 -220
  18. package/src/client/components/common/code-surface/index.tsx +109 -12
  19. package/src/client/components/common/lazy-panels.tsx +5 -1
  20. package/src/client/components/common/provider-icon/glyphs.ts +118 -0
  21. package/src/client/components/common/provider-icon/index.tsx +85 -0
  22. package/src/client/components/common/viewport.ts +5 -3
  23. package/src/client/components/layout/RightActivityBar.tsx +2 -1
  24. package/src/client/components/layout/desktop-layout/TopNavbar.tsx +21 -4
  25. package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +4 -0
  26. package/src/client/components/layout/desktop-layout/index.tsx +26 -4
  27. package/src/client/components/mobile/LayoutWrapper.tsx +16 -0
  28. package/src/client/components/mobile/RightSidebar.tsx +11 -4
  29. package/src/client/components/mobile/SessionSidebar.tsx +10 -0
  30. package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +0 -1
  31. package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +16 -1
  32. package/src/client/components/mobile/mobile-session-sidebar/List.tsx +3 -0
  33. package/src/client/components/settings/categories/ProviderSettings.tsx +45 -4
  34. package/src/client/components/settings/categories/UsageSettings.tsx +22 -7
  35. package/src/client/components/settings/categories/provider-settings/AddModelModal.tsx +322 -0
  36. package/src/client/components/settings/categories/provider-settings/AddProviderModal.tsx +152 -56
  37. package/src/client/components/settings/categories/provider-settings/AuthSection.tsx +52 -1
  38. package/src/client/components/settings/categories/provider-settings/DeleteProviderModal.tsx +133 -0
  39. package/src/client/components/settings/categories/provider-settings/DialectFields.tsx +136 -0
  40. package/src/client/components/settings/categories/provider-settings/EffortLadder.tsx +89 -0
  41. package/src/client/components/settings/categories/provider-settings/Header.tsx +2 -2
  42. package/src/client/components/settings/categories/provider-settings/LoginModal.tsx +2 -2
  43. package/src/client/components/settings/categories/provider-settings/ModelsList.tsx +22 -2
  44. package/src/client/components/settings/categories/provider-settings/PresetPicker.tsx +82 -0
  45. package/src/client/components/settings/categories/provider-settings/PriceFields.tsx +107 -0
  46. package/src/client/components/settings/categories/provider-settings/ReconnectModal.tsx +43 -22
  47. package/src/client/components/settings/categories/provider-settings/SidebarList.tsx +2 -2
  48. package/src/client/components/settings/categories/usage-settings/LimitsSection.tsx +115 -0
  49. package/src/client/components/settings/categories/usage-settings/ProviderCard.tsx +19 -26
  50. package/src/client/components/settings/categories/usage-settings/ProviderDetail.tsx +29 -22
  51. package/src/client/components/settings/categories/usage-settings/SidebarList.tsx +60 -48
  52. package/src/client/components/settings/categories/usage-settings/format.ts +36 -0
  53. package/src/client/components/settings/categories/usage-settings/providers.ts +52 -28
  54. package/src/client/components/workspace/browser-panel/AddressBar.tsx +1 -10
  55. package/src/client/components/workspace/browser-panel/Screencast.tsx +4 -5
  56. package/src/client/components/workspace/browser-panel/index.tsx +6 -6
  57. package/src/client/components/workspace/browser-toolbar/index.tsx +22 -12
  58. package/src/client/components/workspace/chat-timeline/AiMessageFooter.tsx +1 -1
  59. package/src/client/components/workspace/chat-timeline/GeneratingIndicator.tsx +1 -1
  60. package/src/client/components/workspace/chat-timeline/ToolCallCard.tsx +1 -1
  61. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Todo.tsx +1 -1
  62. package/src/client/components/workspace/diff-panel/Toolbar.tsx +71 -38
  63. package/src/client/components/workspace/diff-panel/index.tsx +139 -53
  64. package/src/client/components/workspace/editor/CommandPalette.test.ts +242 -0
  65. package/src/client/components/workspace/editor/CommandPalette.tsx +138 -0
  66. package/src/client/components/workspace/editor/FindWidget.test.ts +261 -0
  67. package/src/client/components/workspace/editor/FindWidget.tsx +219 -0
  68. package/src/client/components/workspace/editor/Tabs.tsx +2 -2
  69. package/src/client/components/workspace/editor/Toolbar.tsx +129 -26
  70. package/src/client/components/workspace/editor/index.tsx +180 -62
  71. package/src/client/components/workspace/file-explorer/Modals.tsx +79 -2
  72. package/src/client/components/workspace/file-explorer/TreeItem.tsx +43 -113
  73. package/src/client/components/workspace/model-dropdown/Item.tsx +13 -23
  74. package/src/client/components/workspace/model-dropdown/Panel.tsx +1 -1
  75. package/src/client/components/workspace/model-dropdown/Section.tsx +13 -4
  76. package/src/client/components/workspace/model-dropdown/index.tsx +7 -3
  77. package/src/client/components/workspace/todo-panel/PhaseGroup.tsx +76 -0
  78. package/src/client/components/workspace/todo-panel/Row.tsx +69 -0
  79. package/src/client/components/workspace/todo-panel/index.tsx +161 -0
  80. package/src/client/components/workspace/usage-panel/ProviderSelect.tsx +10 -5
  81. package/src/client/components/workspace/usage-panel/index.tsx +36 -18
  82. package/src/client/data/samples/dialogue-session.ts +1 -1
  83. package/src/client/data/samples/sample-tools/other.ts +1 -1
  84. package/src/client/data/settings/provider.ts +141 -9
  85. package/src/client/hooks/editor/find-harness.test-util.ts +170 -0
  86. package/src/client/hooks/editor/scroll-container.test.ts +102 -0
  87. package/src/client/hooks/editor/scroll-container.ts +26 -0
  88. package/src/client/hooks/editor/use-code-window.ts +2 -20
  89. package/src/client/hooks/editor/use-editor-find-write.test.ts +170 -0
  90. package/src/client/hooks/editor/use-editor-find.test.ts +118 -0
  91. package/src/client/hooks/editor/use-editor-find.ts +321 -0
  92. package/src/client/hooks/editor/use-editor-history.test.ts +216 -0
  93. package/src/client/hooks/editor/use-editor-history.ts +134 -0
  94. package/src/client/hooks/editor/use-file-editor.ts +24 -27
  95. package/src/client/hooks/editor/use-occurrences.test.ts +236 -0
  96. package/src/client/hooks/editor/use-occurrences.ts +98 -0
  97. package/src/client/hooks/models/use-provider-names.ts +1 -1
  98. package/src/client/hooks/settings/providers/actions.ts +337 -0
  99. package/src/client/hooks/settings/providers/api.ts +69 -0
  100. package/src/client/hooks/settings/providers/index.ts +138 -0
  101. package/src/client/hooks/settings/providers/messages.ts +77 -0
  102. package/src/client/hooks/settings/useUsageReport.ts +8 -3
  103. package/src/client/hooks/ui/theme.ts +3 -3
  104. package/src/client/hooks/workspace/file-tabs.ts +62 -11
  105. package/src/client/hooks/workspace/file-tree-actions.test.ts +348 -0
  106. package/src/client/hooks/workspace/file-tree-actions.ts +250 -0
  107. package/src/client/hooks/workspace/session-todos.ts +102 -0
  108. package/src/client/stubs/elkjs.ts +6 -1
  109. package/src/client/tailwind.css +30 -0
  110. package/src/server/index.ts +20 -2
  111. package/src/server/lib/assets/font-css.server.ts +86 -0
  112. package/src/server/lib/assets/fonts.server.ts +117 -0
  113. package/src/server/lib/bundler/css.ts +116 -0
  114. package/src/server/lib/lifecycle/shell-route.ts +17 -0
  115. package/src/server/lib/models/provider-listing.server.ts +257 -0
  116. package/src/server/lib/models/provider-registry.server.test.ts +122 -0
  117. package/src/server/lib/models/provider-registry.server.ts +76 -25
  118. package/src/server/lib/omp/config/models-config.ts +11 -0
  119. package/src/server/lib/omp/config/models-validation.test.ts +137 -0
  120. package/src/server/lib/omp/config/models-validation.ts +111 -2
  121. package/src/server/lib/omp/config/provider-seeds.test.ts +167 -0
  122. package/src/server/lib/omp/config/provider-seeds.ts +176 -0
  123. package/src/server/lib/omp/config/providers.ts +94 -100
  124. package/src/server/lib/omp/core/auth-credentials.ts +45 -0
  125. package/src/server/lib/omp/session/todos.ts +100 -0
  126. package/src/server/lib/omp/session/{usage.ts → usage/aggregate.ts} +1 -118
  127. package/src/server/lib/omp/session/usage/shaping.ts +130 -0
  128. package/src/server/lib/usage/deepseek.ts +4 -5
  129. package/src/server/lib/usage/kenari.ts +7 -6
  130. package/src/server/lib/usage/omp-usage.server.test.ts +126 -0
  131. package/src/server/lib/usage/omp-usage.server.ts +194 -0
  132. package/src/server/lib/usage/provider-key.ts +81 -3
  133. package/src/server/lib/usage/providers.server.test.ts +47 -0
  134. package/src/server/lib/usage/providers.server.ts +208 -0
  135. package/src/server/lib/usage/vendor-balance.server.test.ts +104 -0
  136. package/src/server/lib/usage/vendor-balance.server.ts +284 -0
  137. package/src/server/plugins/shell.server.ts +56 -0
  138. package/src/server/plugins/ssr.ts +24 -36
  139. package/src/server/plugins/static.test.ts +26 -29
  140. package/src/server/plugins/static.ts +40 -19
  141. package/src/server/routes/fs/action-create.test.ts +90 -0
  142. package/src/server/routes/fs/action.ts +23 -0
  143. package/src/server/routes/omp/index.ts +2 -0
  144. package/src/server/routes/omp/session-todos.ts +80 -0
  145. package/src/server/routes/settings/index.ts +2 -0
  146. package/src/server/routes/settings/provider-model.ts +107 -0
  147. package/src/server/routes/settings/provider-models.ts +65 -202
  148. package/src/server/routes/settings/providers.ts +22 -7
  149. package/src/server/routes/settings/usage.ts +125 -45
  150. package/src/server/routes/telemetry/tokens.ts +2 -1
  151. package/src/shared/lib/chat/{todo-parser.ts → todo/parser.ts} +17 -0
  152. package/src/shared/lib/chat/todo/progress.ts +97 -0
  153. package/src/shared/lib/chat/todo/snapshot.test.ts +156 -0
  154. package/src/shared/lib/chat/todo/snapshot.ts +215 -0
  155. package/src/shared/lib/code/editor/commands.test.ts +155 -0
  156. package/src/shared/lib/code/editor/commands.ts +260 -0
  157. package/src/shared/lib/code/editor/edits.test.ts +100 -0
  158. package/src/shared/lib/code/editor/edits.ts +122 -0
  159. package/src/shared/lib/code/editor/find-status.test.ts +50 -0
  160. package/src/shared/lib/code/editor/find-status.ts +36 -0
  161. package/src/shared/lib/code/editor/find.test.ts +203 -0
  162. package/src/shared/lib/code/editor/find.ts +224 -0
  163. package/src/{client/components/common/code-editor → shared/lib/code/editor}/history.test.ts +1 -1
  164. package/src/shared/lib/code/editor/keymap.test.ts +109 -0
  165. package/src/shared/lib/code/editor/keymap.ts +151 -0
  166. package/src/shared/lib/code/editor/layers.test.ts +77 -0
  167. package/src/shared/lib/code/editor/layers.ts +67 -0
  168. package/src/shared/lib/code/editor/line-edits.test.ts +131 -0
  169. package/src/shared/lib/code/editor/line-edits.ts +159 -0
  170. package/src/shared/lib/code/editor/lines.test.ts +81 -0
  171. package/src/shared/lib/code/editor/lines.ts +83 -0
  172. package/src/shared/lib/code/editor/marks.test.ts +86 -0
  173. package/src/shared/lib/code/editor/marks.ts +106 -0
  174. package/src/shared/lib/code/editor/occurrences.test.ts +117 -0
  175. package/src/shared/lib/code/editor/occurrences.ts +111 -0
  176. package/src/shared/lib/code/editor/palette.test.ts +91 -0
  177. package/src/shared/lib/code/editor/palette.ts +92 -0
  178. package/src/shared/lib/code/editor/reset-css.test.ts +35 -0
  179. package/src/shared/lib/code/editor/reset-css.ts +32 -0
  180. package/src/shared/lib/code/editor/text-commands.test.ts +178 -0
  181. package/src/shared/lib/code/editor/text-commands.ts +154 -0
  182. package/src/shared/lib/code/syntax-highlight.test.ts +75 -0
  183. package/src/shared/lib/code/syntax-highlight.ts +29 -17
  184. package/src/shared/lib/code/windowed-highlight.ts +52 -1
  185. package/src/shared/lib/fs/diff-parser.test.ts +116 -0
  186. package/src/shared/lib/fs/diff-parser.ts +86 -33
  187. package/src/shared/lib/models/{provider-connection.ts → provider/connection.ts} +37 -0
  188. package/src/shared/lib/models/provider/dialect.ts +179 -0
  189. package/src/shared/lib/models/provider/glyph.test.ts +116 -0
  190. package/src/shared/lib/models/provider/glyph.ts +200 -0
  191. package/src/shared/lib/models/{provider-models.ts → provider/models.ts} +91 -6
  192. package/src/shared/lib/models/provider/presets.test.ts +172 -0
  193. package/src/shared/lib/models/provider/presets.ts +83 -0
  194. package/src/shared/lib/ui/key-binding.test.ts +115 -0
  195. package/src/shared/lib/ui/key-binding.ts +143 -0
  196. package/src/shared/lib/util/platform.test.ts +87 -0
  197. package/src/shared/lib/util/platform.ts +30 -0
  198. package/src/shared/lib/workspace/file-tab-id.ts +29 -0
  199. package/src/shared/lib/workspace/keymap.test.ts +82 -0
  200. package/src/shared/lib/workspace/keymap.ts +30 -0
  201. package/src/shared/lib/workspace/refresh-cadence.ts +9 -0
  202. package/src/shared/lib/workspace/right-panels.ts +8 -0
  203. package/src/shared/lib/workspace/session-state/store.ts +0 -1
  204. package/src/shared/types/index.ts +1 -0
  205. package/src/shared/types/settings/provider.ts +81 -0
  206. package/src/shared/types/settings/usage.ts +88 -14
  207. package/src/shared/types/todo.ts +108 -0
  208. package/tsconfig.json +3 -1
  209. package/dist/client/static/css/index.5f630421b7.css +0 -2
  210. package/dist/client/static/font/KaTeX_AMS-Regular.a79f1c3119.woff2 +0 -0
  211. package/dist/client/static/font/KaTeX_Caligraphic-Bold.ec17d13264.woff2 +0 -0
  212. package/dist/client/static/font/KaTeX_Caligraphic-Regular.55fac25845.woff2 +0 -0
  213. package/dist/client/static/font/KaTeX_Fraktur-Bold.d42a5579b0.woff2 +0 -0
  214. package/dist/client/static/font/KaTeX_Fraktur-Regular.d3c882a649.woff2 +0 -0
  215. package/dist/client/static/font/KaTeX_Main-Bold.c3fb5ac22f.woff2 +0 -0
  216. package/dist/client/static/font/KaTeX_Main-BoldItalic.6f2bb1dff2.woff2 +0 -0
  217. package/dist/client/static/font/KaTeX_Main-Italic.8916142bec.woff2 +0 -0
  218. package/dist/client/static/font/KaTeX_Main-Regular.0462f03bdf.woff2 +0 -0
  219. package/dist/client/static/font/KaTeX_Math-BoldItalic.572d331f69.woff2 +0 -0
  220. package/dist/client/static/font/KaTeX_Math-Italic.f28c23acad.woff2 +0 -0
  221. package/dist/client/static/font/KaTeX_SansSerif-Bold.8c5b5494b6.woff2 +0 -0
  222. package/dist/client/static/font/KaTeX_SansSerif-Italic.3b1e59b3ba.woff2 +0 -0
  223. package/dist/client/static/font/KaTeX_SansSerif-Regular.ba21ed5f84.woff2 +0 -0
  224. package/dist/client/static/font/KaTeX_Script-Regular.03e9641d6f.woff2 +0 -0
  225. package/dist/client/static/font/KaTeX_Size1-Regular.eae34984b3.woff2 +0 -0
  226. package/dist/client/static/font/KaTeX_Size2-Regular.5916a24fa3.woff2 +0 -0
  227. package/dist/client/static/font/KaTeX_Size4-Regular.10d95fd3a2.woff2 +0 -0
  228. package/dist/client/static/font/KaTeX_Typewriter-Regular.a8709e3622.woff2 +0 -0
  229. package/dist/client/static/font/fira-code-latin-400-normal.2655b10c55.woff2 +0 -0
  230. package/dist/client/static/font/fira-code-latin-500-normal.92a991f204.woff2 +0 -0
  231. package/dist/client/static/font/fira-code-latin-600-normal.43788eb7f1.woff2 +0 -0
  232. package/dist/client/static/font/fira-code-latin-700-normal.2667215d33.woff2 +0 -0
  233. package/dist/client/static/js/77.0d81c74cf0.js +0 -139
  234. package/dist/client/static/js/77.0d81c74cf0.js.LICENSE.txt +0 -8
  235. package/dist/client/static/js/async/0.8e59f2eb5b.js +0 -1
  236. package/dist/client/static/js/async/0k.5852e6b4fa.js +0 -1
  237. package/dist/client/static/js/async/0w.9ed36d5860.js +0 -1
  238. package/dist/client/static/js/async/1.f387c12b63.js +0 -1
  239. package/dist/client/static/js/async/12.d091f91c99.js +0 -1
  240. package/dist/client/static/js/async/13.8f810a8a6c.js +0 -1
  241. package/dist/client/static/js/async/19.6fe909d55a.js +0 -160
  242. package/dist/client/static/js/async/1b.23f1141a38.js +0 -1
  243. package/dist/client/static/js/async/1k.973e8e21fc.js +0 -2
  244. package/dist/client/static/js/async/1k.973e8e21fc.js.LICENSE.txt +0 -11
  245. package/dist/client/static/js/async/1p.0d5cddda96.js +0 -1
  246. package/dist/client/static/js/async/2.b589f1733b.js +0 -1
  247. package/dist/client/static/js/async/2v.96165101de.js +0 -1
  248. package/dist/client/static/js/async/3.0cacc08423.js +0 -1
  249. package/dist/client/static/js/async/37.b28b2f61cd.js +0 -1
  250. package/dist/client/static/js/async/3d.5085fb913f.js +0 -1
  251. package/dist/client/static/js/async/3j.357c80ff38.js +0 -2
  252. package/dist/client/static/js/async/3j.357c80ff38.js.LICENSE.txt +0 -11
  253. package/dist/client/static/js/async/3p.bcf32a0983.js +0 -1
  254. package/dist/client/static/js/async/3r.3a69e98ee0.js +0 -64
  255. package/dist/client/static/js/async/3u.1228460e2a.js +0 -1
  256. package/dist/client/static/js/async/3y.3530b824ad.js +0 -198
  257. package/dist/client/static/js/async/3y.3530b824ad.js.LICENSE.txt +0 -5
  258. package/dist/client/static/js/async/4.9442eeb472.js +0 -1
  259. package/dist/client/static/js/async/4e.678285ca55.js +0 -1
  260. package/dist/client/static/js/async/4k.33a0c7eeb4.js +0 -1
  261. package/dist/client/static/js/async/4m.928a1dde85.js +0 -31
  262. package/dist/client/static/js/async/4m.928a1dde85.js.LICENSE.txt +0 -11
  263. package/dist/client/static/js/async/4q.aad20dab25.js +0 -1
  264. package/dist/client/static/js/async/5.ebaf8b89cc.js +0 -36
  265. package/dist/client/static/js/async/51.95bcae98af.js +0 -2
  266. package/dist/client/static/js/async/51.95bcae98af.js.LICENSE.txt +0 -6
  267. package/dist/client/static/js/async/5b.d88304bc13.js +0 -1
  268. package/dist/client/static/js/async/5j.d6407c4248.js +0 -287
  269. package/dist/client/static/js/async/5o.88316d4460.js +0 -62
  270. package/dist/client/static/js/async/6.f034c9b528.js +0 -1
  271. package/dist/client/static/js/async/66.5dfa29f957.js +0 -93
  272. package/dist/client/static/js/async/6o.5b1a06a382.js +0 -1
  273. package/dist/client/static/js/async/7.e4a9288dd0.js +0 -1
  274. package/dist/client/static/js/async/73.308dff4da8.js +0 -1
  275. package/dist/client/static/js/async/7e.a1cc9bfd88.js +0 -1
  276. package/dist/client/static/js/async/7g.a7814350c3.js +0 -1
  277. package/dist/client/static/js/async/7l.fadbbc0583.js +0 -1
  278. package/dist/client/static/js/async/8.5067439d8c.js +0 -1
  279. package/dist/client/static/js/async/8j.ea110663fb.js +0 -1
  280. package/dist/client/static/js/async/a.bce4542392.js +0 -1
  281. package/dist/client/static/js/async/aa.65d3852576.js +0 -1
  282. package/dist/client/static/js/async/ar.9303e5f2e8.js +0 -2
  283. package/dist/client/static/js/async/ar.9303e5f2e8.js.LICENSE.txt +0 -11
  284. package/dist/client/static/js/async/az.d656062b0f.js +0 -1
  285. package/dist/client/static/js/async/b.b4f6f06515.js +0 -1
  286. package/dist/client/static/js/async/bi.09688ba75c.js +0 -1
  287. package/dist/client/static/js/async/bq.586bc738d1.js +0 -1
  288. package/dist/client/static/js/async/c.797d933882.js +0 -1
  289. package/dist/client/static/js/async/c0.0af1e640d3.js +0 -24
  290. package/dist/client/static/js/async/ca.8a52cfd447.js +0 -1
  291. package/dist/client/static/js/async/cf.d34fff5d67.js +0 -1
  292. package/dist/client/static/js/async/ct.b74928f7a3.js +0 -1
  293. package/dist/client/static/js/async/cx.3cb0242a3a.js +0 -226
  294. package/dist/client/static/js/async/d.a5fc8fe1b3.js +0 -1
  295. package/dist/client/static/js/async/da.f62c70f6c8.js +0 -1
  296. package/dist/client/static/js/async/dp.1725d6432c.js +0 -1
  297. package/dist/client/static/js/async/e.93214d622b.js +0 -79
  298. package/dist/client/static/js/async/f.ab50467719.js +0 -19
  299. package/dist/client/static/js/async/f.ab50467719.js.LICENSE.txt +0 -12
  300. package/dist/client/static/js/async/f7.46971e9d6d.js +0 -2
  301. package/dist/client/static/js/async/f7.46971e9d6d.js.LICENSE.txt +0 -6
  302. package/dist/client/static/js/async/f8.bbf0dbae70.js +0 -1
  303. package/dist/client/static/js/async/g.cb81980498.js +0 -2
  304. package/dist/client/static/js/async/g.cb81980498.js.LICENSE.txt +0 -11
  305. package/dist/client/static/js/async/h.4e790471e4.js +0 -27
  306. package/dist/client/static/js/async/h1.393f066d09.js +0 -1
  307. package/dist/client/static/js/async/h3.216e9798ab.js +0 -148
  308. package/dist/client/static/js/async/hd.0da931b622.js +0 -1
  309. package/dist/client/static/js/async/hk.ba7fb04d41.js +0 -1
  310. package/dist/client/static/js/async/hn.b645041e00.js +0 -1
  311. package/dist/client/static/js/async/ho.475a5e1ae4.js +0 -1
  312. package/dist/client/static/js/async/hp.e9fe2e395a.js +0 -2
  313. package/dist/client/static/js/async/hp.e9fe2e395a.js.LICENSE.txt +0 -17
  314. package/dist/client/static/js/async/hs.e7c59d8090.js +0 -41
  315. package/dist/client/static/js/async/i.7c148e8440.js +0 -1
  316. package/dist/client/static/js/async/id.573df20f29.js +0 -1
  317. package/dist/client/static/js/async/j.ab925882d8.js +0 -1
  318. package/dist/client/static/js/async/jd.63886a1f09.js +0 -1
  319. package/dist/client/static/js/async/jg.d86ebd4013.js +0 -86
  320. package/dist/client/static/js/async/jl.874d352278.js +0 -1
  321. package/dist/client/static/js/async/jll.11313b455a.js +0 -1
  322. package/dist/client/static/js/async/js.67182759ef.js +0 -1
  323. package/dist/client/static/js/async/jsc.23b70d6f40.js +0 -1
  324. package/dist/client/static/js/async/jse.334588ff5d.js +0 -1
  325. package/dist/client/static/js/async/jz.72eb929106.js +0 -77
  326. package/dist/client/static/js/async/k.3210286401.js +0 -217
  327. package/dist/client/static/js/async/ka.c1990ecaa3.js +0 -115
  328. package/dist/client/static/js/async/katex-vendor.de6a87c308.js +0 -1
  329. package/dist/client/static/js/async/ku.29c8a59d0c.js +0 -1
  330. package/dist/client/static/js/async/ky.8dab1dade0.js +0 -1
  331. package/dist/client/static/js/async/l.12dd34e1be.js +0 -1
  332. package/dist/client/static/js/async/l9.949710adaf.js +0 -83
  333. package/dist/client/static/js/async/l9.949710adaf.js.LICENSE.txt +0 -13
  334. package/dist/client/static/js/async/ls.9d9a01026c.js +0 -104
  335. package/dist/client/static/js/async/m.eb2e6752e2.js +0 -28
  336. package/dist/client/static/js/async/m.eb2e6752e2.js.LICENSE.txt +0 -11
  337. package/dist/client/static/js/async/m3.d2b05f9604.js +0 -2
  338. package/dist/client/static/js/async/m3.d2b05f9604.js.LICENSE.txt +0 -6
  339. package/dist/client/static/js/async/ma.2c261b04dd.js +0 -2
  340. package/dist/client/static/js/async/ma.2c261b04dd.js.LICENSE.txt +0 -6
  341. package/dist/client/static/js/async/me.8ace414f80.js +0 -23
  342. package/dist/client/static/js/async/ms.98960af4eb.js +0 -67
  343. package/dist/client/static/js/async/ms.98960af4eb.js.LICENSE.txt +0 -12
  344. package/dist/client/static/js/async/mt.0352e976d8.js +0 -1
  345. package/dist/client/static/js/async/n.a9f1c51701.js +0 -1
  346. package/dist/client/static/js/async/na.a36cd532d6.js +0 -1
  347. package/dist/client/static/js/async/nk.fc7e58325e.js +0 -1
  348. package/dist/client/static/js/async/o.36353dfb88.js +0 -1
  349. package/dist/client/static/js/async/oc.b73042e8bd.js +0 -8
  350. package/dist/client/static/js/async/og.8eb577893a.js +0 -193
  351. package/dist/client/static/js/async/p.48ecfcd14e.js +0 -1
  352. package/dist/client/static/js/async/px.fc6877924b.js +0 -1
  353. package/dist/client/static/js/async/q.22588845a9.js +0 -21
  354. package/dist/client/static/js/async/q.22588845a9.js.LICENSE.txt +0 -6
  355. package/dist/client/static/js/async/q5.97dc1481b6.js +0 -286
  356. package/dist/client/static/js/async/qs.2a9ddb3c72.js +0 -1
  357. package/dist/client/static/js/async/r.51010ad87c.js +0 -1
  358. package/dist/client/static/js/async/rd.cb6bf495bb.js +0 -2
  359. package/dist/client/static/js/async/rd.cb6bf495bb.js.LICENSE.txt +0 -4
  360. package/dist/client/static/js/async/s.0f395cdda5.js +0 -94
  361. package/dist/client/static/js/async/t.4dca0e2ba8.js +0 -1
  362. package/dist/client/static/js/async/t9.d44f57d518.js +0 -1
  363. package/dist/client/static/js/async/ta.973534bbec.js +0 -1
  364. package/dist/client/static/js/async/tr.e878658140.js +0 -309
  365. package/dist/client/static/js/async/u.0871ac5b22.js +0 -1
  366. package/dist/client/static/js/async/ud.3065bfee31.js +0 -2
  367. package/dist/client/static/js/async/ud.3065bfee31.js.LICENSE.txt +0 -4
  368. package/dist/client/static/js/async/up.ea990da0ff.js +0 -32
  369. package/dist/client/static/js/async/uw.8890abbabc.js +0 -1
  370. package/dist/client/static/js/async/v.7d28d3c241.js +0 -34
  371. package/dist/client/static/js/async/vp.9b13bf8610.js +0 -1
  372. package/dist/client/static/js/async/vt.42416b707d.js +0 -1
  373. package/dist/client/static/js/async/w.08ac210f60.js +0 -1
  374. package/dist/client/static/js/async/wt.ad92393a90.js +0 -1
  375. package/dist/client/static/js/async/x.563f72ca6b.js +0 -1
  376. package/dist/client/static/js/async/x0.475516418f.js +0 -1
  377. package/dist/client/static/js/async/x1.da8687159b.js +0 -90
  378. package/dist/client/static/js/async/xe.da8dcc2015.js +0 -2
  379. package/dist/client/static/js/async/xe.da8dcc2015.js.LICENSE.txt +0 -11
  380. package/dist/client/static/js/async/xl.a3c562a58c.js +0 -1
  381. package/dist/client/static/js/async/xn.6510c0abe2.js +0 -2
  382. package/dist/client/static/js/async/xn.6510c0abe2.js.LICENSE.txt +0 -6
  383. package/dist/client/static/js/async/xn1.83a7784fe1.js +0 -10
  384. package/dist/client/static/js/async/xn1.83a7784fe1.js.LICENSE.txt +0 -11
  385. package/dist/client/static/js/async/xnl.feb0021e50.js +0 -2
  386. package/dist/client/static/js/async/xnl.feb0021e50.js.LICENSE.txt +0 -12
  387. package/dist/client/static/js/async/y.1a9ed7a5d8.js +0 -1
  388. package/dist/client/static/js/async/y6.43c57defe4.js +0 -1
  389. package/dist/client/static/js/async/yb.8c1342401d.js +0 -1
  390. package/dist/client/static/js/async/ybq.4491f2f2dc.js +0 -1
  391. package/dist/client/static/js/async/yg.35bebd6d0c.js +0 -149
  392. package/dist/client/static/js/async/yq.917ddd7ba2.js +0 -1
  393. package/dist/client/static/js/async/ys.00687cf82d.js +0 -38
  394. package/dist/client/static/js/async/yv.d99d829136.js +0 -1
  395. package/dist/client/static/js/async/z.56f61a4374.js +0 -1
  396. package/dist/client/static/js/async/zi.5fdb07b86a.js +0 -24
  397. package/dist/client/static/js/async/zj.654f37e8ff.js +0 -1
  398. package/dist/client/static/js/async/zn.966d6f1cc6.js +0 -1
  399. package/dist/client/static/js/async/zo.26213b40c0.js +0 -1
  400. package/dist/client/static/js/async/zx.e9f4105e00.js +0 -1
  401. package/dist/client/static/js/index.e6a257402a.js +0 -38
  402. package/dist/client/static/js/index.e6a257402a.js.LICENSE.txt +0 -4
  403. package/dist/client/static/js/markdown-vendor.71e43278b3.js +0 -144
  404. package/dist/client/static/js/shiki-vendor.cb8452ddcd.js +0 -1
  405. package/src/client/components/settings/categories/provider-settings/Icons.tsx +0 -35
  406. package/src/client/hooks/settings/providers.ts +0 -326
  407. package/src/server/plugins/dev-assets.ts +0 -49
  408. package/src/shared/lib/code/editor-utils.ts +0 -16
  409. package/src/shared/lib/models/provider-presets.ts +0 -29
  410. /package/dist/client/{apple-touch-icon.png → static/apple-touch-icon-dskx5sr3.png} +0 -0
  411. /package/dist/client/{icon-192x192.png → static/icon-192x192-6gn5cr2f.png} +0 -0
  412. /package/dist/client/{icon.svg → static/icon-d07z9dtz.svg} +0 -0
  413. /package/dist/client/{manifest.webmanifest → static/manifest-bnyy75kt.webmanifest} +0 -0
  414. /package/{dist/client → public}/icon-512x512.png +0 -0
  415. /package/{dist/client → public}/icon-maskable-512x512.png +0 -0
  416. /package/{dist/client → public}/sw.js +0 -0
  417. /package/src/{client/components/common/code-editor → shared/lib/code/editor}/history.ts +0 -0
  418. /package/src/shared/lib/models/{provider-cleanup.ts → provider/cleanup.ts} +0 -0
  419. /package/src/shared/lib/models/{provider-label.ts → provider/label.ts} +0 -0
@@ -0,0 +1,203 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The find widget's arithmetic: what a query matches, where the caret lands
8
+ * after a replacement, and how `$1` is expanded. These are the rules the widget
9
+ * count, the on-screen highlights and the written buffer all read, so each is
10
+ * asserted against the engine's own `String.replace` behaviour where one
11
+ * exists.
12
+ */
13
+
14
+ import { describe, expect, test } from 'bun:test';
15
+
16
+ import {
17
+ buildFindRegex,
18
+ expandReplacement,
19
+ findMatches,
20
+ lineIndexAt,
21
+ lineStartOffset,
22
+ matchIndexAtOrAfter,
23
+ MAX_FIND_MATCHES,
24
+ replaceAllMatches,
25
+ replaceMatch,
26
+ stepMatchIndex,
27
+ type FindOptions,
28
+ } from '@/shared/lib/code/editor/find';
29
+
30
+ const PLAIN: FindOptions = { matchCase: false, wholeWord: false, isRegex: false };
31
+
32
+ describe('findMatches', () => {
33
+ test('finds literal occurrences in document order, ignoring case by default', () => {
34
+ const text = 'Alpha beta ALPHA alpha';
35
+ expect(findMatches(text, 'alpha', PLAIN).matches).toEqual([
36
+ { start: 0, end: 5 },
37
+ { start: 11, end: 16 },
38
+ { start: 17, end: 22 },
39
+ ]);
40
+ });
41
+
42
+ test('matchCase narrows to the exact spelling', () => {
43
+ expect(findMatches('Alpha alpha', 'alpha', { ...PLAIN, matchCase: true }).matches).toEqual([
44
+ { start: 6, end: 11 },
45
+ ]);
46
+ });
47
+
48
+ test('wholeWord rejects a match inside a longer word', () => {
49
+ const text = 'cat category cat';
50
+ expect(findMatches(text, 'cat', { ...PLAIN, wholeWord: true }).matches).toEqual([
51
+ { start: 0, end: 3 },
52
+ { start: 13, end: 16 },
53
+ ]);
54
+ });
55
+
56
+ test('wholeWord keeps an alternation’s own precedence', () => {
57
+ // `\bfoo|bar\b` would match `bar` inside `barbaz`; the group prevents it.
58
+ expect(findMatches('barbaz bar', 'foo|bar', { ...PLAIN, wholeWord: true, isRegex: true }).matches).toEqual([
59
+ { start: 7, end: 10 },
60
+ ]);
61
+ });
62
+
63
+ test('regex mode compiles the query as written', () => {
64
+ expect(findMatches('a1 b2 c3', '[a-z]\\d', { ...PLAIN, isRegex: true }).matches).toEqual([
65
+ { start: 0, end: 2 },
66
+ { start: 3, end: 5 },
67
+ { start: 6, end: 8 },
68
+ ]);
69
+ });
70
+
71
+ test('an uncompilable regex is reported as invalid, not as no matches', () => {
72
+ const result = findMatches('abc', '(', { ...PLAIN, isRegex: true });
73
+ expect(result).toEqual({ matches: [], truncated: false, invalid: true });
74
+ });
75
+
76
+ test('an empty query matches nothing and is not an error', () => {
77
+ expect(findMatches('abc', '', PLAIN)).toEqual({ matches: [], truncated: false, invalid: false });
78
+ });
79
+
80
+ test('skips zero-width matches instead of looping forever', () => {
81
+ const result = findMatches('abc', 'x*', { ...PLAIN, isRegex: true });
82
+ expect(result.matches).toEqual([]);
83
+ expect(result.invalid).toBe(false);
84
+ });
85
+
86
+ test('caps the collected matches and says so', () => {
87
+ const text = 'a'.repeat(MAX_FIND_MATCHES + 10);
88
+ const result = findMatches(text, 'a', PLAIN);
89
+ expect(result.matches).toHaveLength(MAX_FIND_MATCHES);
90
+ expect(result.truncated).toBe(true);
91
+ });
92
+ });
93
+
94
+ describe('buildFindRegex', () => {
95
+ test('escapes a literal query so regex metacharacters are literal', () => {
96
+ const regex = buildFindRegex('a.c', PLAIN);
97
+ expect(regex?.test('a.c')).toBe(true);
98
+ expect(regex?.test('abc')).toBe(false);
99
+ });
100
+
101
+ test('returns null for an empty query and for a bad pattern', () => {
102
+ expect(buildFindRegex('', PLAIN)).toBeNull();
103
+ expect(buildFindRegex('[', { ...PLAIN, isRegex: true })).toBeNull();
104
+ });
105
+ });
106
+
107
+ describe('replaceMatch', () => {
108
+ test('literal replacement inserts the field verbatim, dollar signs included', () => {
109
+ const text = 'const a = 1;';
110
+ const result = replaceMatch(text, { start: 6, end: 7 }, 'a', '$&x', PLAIN);
111
+ expect(result?.text).toBe('const $&x = 1;');
112
+ expect(result?.caret).toBe(6 + 3);
113
+ });
114
+
115
+ test('regex replacement expands capture groups like String.replace', () => {
116
+ const text = 'key = value';
117
+ const result = replaceMatch(text, { start: 6, end: 11 }, '(\\w+)', '[$1]', { ...PLAIN, isRegex: true });
118
+ expect(result?.text).toBe('key = [value]');
119
+ expect(result?.caret).toBe(13);
120
+ });
121
+
122
+ test('refuses a match that is no longer at its offset', () => {
123
+ expect(replaceMatch('abc', { start: 5, end: 7 }, 'a', 'z', PLAIN)).toBeNull();
124
+ });
125
+ });
126
+
127
+ describe('replaceAllMatches', () => {
128
+ test('literal replace-all rewrites every occurrence', () => {
129
+ expect(replaceAllMatches('a a a', 'a', 'b', PLAIN)).toBe('b b b');
130
+ });
131
+
132
+ test('regex replace-all agrees with the engine', () => {
133
+ const text = 'foo1 bar2';
134
+ const options: FindOptions = { ...PLAIN, isRegex: true };
135
+ expect(replaceAllMatches(text, '([a-z]+)(\\d)', '$2-$1', options)).toBe(text.replace(/([a-z]+)(\d)/g, '$2-$1'));
136
+ });
137
+
138
+ test('whole-word replace-all leaves substrings alone', () => {
139
+ expect(replaceAllMatches('cat category', 'cat', 'dog', { ...PLAIN, wholeWord: true })).toBe('dog category');
140
+ });
141
+
142
+ test('returns null for an unusable query', () => {
143
+ expect(replaceAllMatches('abc', '(', 'x', { ...PLAIN, isRegex: true })).toBeNull();
144
+ expect(replaceAllMatches('abc', '', 'x', PLAIN)).toBeNull();
145
+ });
146
+ });
147
+
148
+ describe('expandReplacement', () => {
149
+ /** Indexed by group number, `[0]` unused — the shape `groupsOf` produces. */
150
+ const groups = { full: 'ab12', numbered: [undefined, 'a', 'b'] as const, named: { tail: '12' } };
151
+
152
+ test('expands $&, $1 and $<name>', () => {
153
+ expect(expandReplacement('$&|$1|$2|$<tail>', groups)).toBe('ab12|a|b|12');
154
+ });
155
+
156
+ test('$$ is a literal dollar and an unknown group is empty', () => {
157
+ expect(expandReplacement('$$$9', groups)).toBe('$');
158
+ });
159
+
160
+ test('a missing two-digit group falls back to the one-digit group plus the digit', () => {
161
+ // The engine's own reading of `$12` with two groups: `$1` then a literal `2`.
162
+ expect('x'.replace(/(x)/, '$12')).toBe('x2');
163
+ expect(expandReplacement('$12', groups)).toBe('a2');
164
+ });
165
+
166
+ test('a template without a dollar is returned as-is', () => {
167
+ expect(expandReplacement('plain', groups)).toBe('plain');
168
+ });
169
+ });
170
+
171
+ describe('caret anchoring', () => {
172
+ test('lineIndexAt and lineStartOffset are inverses for every line start', () => {
173
+ const text = 'one\ntwo\n\nfour';
174
+ // Line starts in `text`: 0, 4, 8 (the empty line) and 9.
175
+ for (const offset of [0, 4, 8, 9]) {
176
+ const line = lineIndexAt(text, offset);
177
+ expect(lineStartOffset(text, line)).toBe(offset);
178
+ }
179
+ expect(lineIndexAt(text, 13)).toBe(3);
180
+ });
181
+
182
+ test('lineIndexAt clamps an offset past the end to the last line', () => {
183
+ expect(lineIndexAt('one\ntwo', 999)).toBe(1);
184
+ });
185
+
186
+ test('a caret inside a match selects that match, and past the last wraps to the first', () => {
187
+ const matches = [
188
+ { start: 0, end: 3 },
189
+ { start: 10, end: 13 },
190
+ ];
191
+ expect(matchIndexAtOrAfter(matches, 1)).toBe(0);
192
+ expect(matchIndexAtOrAfter(matches, 3)).toBe(1);
193
+ expect(matchIndexAtOrAfter(matches, 13)).toBe(0);
194
+ expect(matchIndexAtOrAfter([], 0)).toBe(-1);
195
+ });
196
+
197
+ test('stepping wraps at both ends', () => {
198
+ expect(stepMatchIndex(2, 3, 1)).toBe(0);
199
+ expect(stepMatchIndex(0, 3, -1)).toBe(2);
200
+ expect(stepMatchIndex(-1, 3, -1)).toBe(2);
201
+ expect(stepMatchIndex(0, 0, 1)).toBe(-1);
202
+ });
203
+ });
@@ -0,0 +1,224 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ import { lineIndexAt, lineStartOffset, type TextRange } from '@/shared/lib/code/editor/lines';
7
+
8
+ export { lineIndexAt, lineStartOffset };
9
+
10
+ /**
11
+ * Find and replace over one document buffer — the pure half of the editor's
12
+ * find widget.
13
+ *
14
+ * Everything here is offset arithmetic on a string: what the query compiles
15
+ * to, which ranges match, where the caret lands after a replacement, and how
16
+ * `$1` is expanded. The widget, the hook that owns its state and the editor
17
+ * surface that paints the matches all read these results, so none of them
18
+ * re-implements a rule and the count in the widget cannot disagree with the
19
+ * ranges on screen.
20
+ */
21
+
22
+ export interface FindOptions {
23
+ matchCase: boolean;
24
+ wholeWord: boolean;
25
+ isRegex: boolean;
26
+ }
27
+
28
+ /** Half-open `[start, end)` offset range in the document. */
29
+ export type FindMatch = TextRange;
30
+
31
+ export interface FindResult {
32
+ matches: FindMatch[];
33
+ /** The document holds more matches than the cap; the list stops at it. */
34
+ truncated: boolean;
35
+ /** A regex query that does not compile — a different answer from "no matches". */
36
+ invalid: boolean;
37
+ }
38
+
39
+ /**
40
+ * Ceiling on collected matches. A one-character query over a large file matches
41
+ * tens of thousands of times and every match costs a Range in the highlight
42
+ * registry, so past this the list stops and the widget reports `N+`.
43
+ */
44
+ export const MAX_FIND_MATCHES = 5_000;
45
+
46
+ export const DEFAULT_FIND_OPTIONS: FindOptions = {
47
+ matchCase: false,
48
+ wholeWord: false,
49
+ isRegex: false,
50
+ };
51
+
52
+ /**
53
+ * The pattern a query compiles to, or null when the query is empty or does not
54
+ * compile. A literal query is escaped here rather than at the call site so the
55
+ * match scan and the replacement splice can never disagree about what the
56
+ * pattern was.
57
+ *
58
+ * Whole-word is a `\b`-anchored non-capturing group rather than a bare wrap so
59
+ * an alternation (`foo|bar`) keeps its own precedence.
60
+ */
61
+ export function buildFindRegex(query: string, options: FindOptions): RegExp | null {
62
+ if (!query) return null;
63
+ const source = options.isRegex ? query : query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
64
+ const wrapped = options.wholeWord ? `\\b(?:${source})\\b` : source;
65
+ try {
66
+ return new RegExp(wrapped, options.matchCase ? 'g' : 'gi');
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Every match of `query` in `text`, in document order.
74
+ *
75
+ * Zero-width matches (`^`, `\b`, `a*`) are skipped: they have nothing to
76
+ * highlight and nothing to replace, and stepping past them is what keeps the
77
+ * scan terminating.
78
+ */
79
+ export function findMatches(text: string, query: string, options: FindOptions): FindResult {
80
+ const regex = buildFindRegex(query, options);
81
+ if (!query) return { matches: [], truncated: false, invalid: false };
82
+ if (!regex) return { matches: [], truncated: false, invalid: true };
83
+
84
+ const matches: FindMatch[] = [];
85
+ let match: RegExpExecArray | null;
86
+ while ((match = regex.exec(text)) !== null) {
87
+ if (match[0].length === 0) {
88
+ regex.lastIndex += 1;
89
+ continue;
90
+ }
91
+ matches.push({ start: match.index, end: match.index + match[0].length });
92
+ if (matches.length >= MAX_FIND_MATCHES) return { matches, truncated: true, invalid: false };
93
+ }
94
+ return { matches, truncated: false, invalid: false };
95
+ }
96
+
97
+ /**
98
+ * The match to call "current" for a caret/anchor at `offset`: the first one
99
+ * that ends after it, so a caret sitting inside a match still counts as being
100
+ * on that match. Wraps to the first match when the anchor is past the last one,
101
+ * and reports -1 for an empty list.
102
+ */
103
+ export function matchIndexAtOrAfter(matches: readonly FindMatch[], offset: number): number {
104
+ if (matches.length === 0) return -1;
105
+ for (let index = 0; index < matches.length; index++) {
106
+ if (matches[index].end > offset) return index;
107
+ }
108
+ return 0;
109
+ }
110
+
111
+ /** Step one match forward (`1`) or back (`-1`), wrapping at either end. */
112
+ export function stepMatchIndex(index: number, count: number, direction: 1 | -1): number {
113
+ if (count <= 0) return -1;
114
+ if (index < 0) return direction === 1 ? 0 : count - 1;
115
+ return (index + direction + count) % count;
116
+ }
117
+
118
+ /** What a replacement template is expanded against. */
119
+ export interface MatchGroups {
120
+ /** `$&` — the whole match. */
121
+ full: string;
122
+ /** `$1`…`$99`, indexed by their own number: entry `n` is group `n` (`[0]` is unused). */
123
+ numbered: readonly (string | undefined)[];
124
+ /** `$<name>` — named capture groups. */
125
+ named?: Record<string, string | undefined>;
126
+ }
127
+
128
+ /** The engine's own argument order: `found[1]` is `$1`. */
129
+ function groupsOf(found: RegExpExecArray): MatchGroups {
130
+ return {
131
+ full: found[0],
132
+ numbered: [undefined, ...found.slice(1)],
133
+ named: found.groups,
134
+ };
135
+ }
136
+
137
+ const SUBSTITUTION = /\$(\$|&|\d{1,2}|<[^>]*>)/g;
138
+
139
+ /**
140
+ * Expand a replacement template the way `String.prototype.replace` does for a
141
+ * string replacement: `$$`, `$&`, `$1`–`$99` and `$<name>`. The prefix/suffix
142
+ * forms (`` $` `` / `$'`) are deliberately left literal — nothing in the UI
143
+ * offers them, and leaving the two characters alone is the honest reading.
144
+ *
145
+ * A two-digit group that does not exist falls back to the one-digit group plus
146
+ * the second digit, which is the engine's own behaviour for `$12` when the
147
+ * pattern has one group.
148
+ */
149
+ export function expandReplacement(template: string, groups: MatchGroups): string {
150
+ if (!template.includes('$')) return template;
151
+ return template.replace(SUBSTITUTION, (_token, body: string) => {
152
+ if (body === '$') return '$';
153
+ if (body === '&') return groups.full;
154
+ if (body.startsWith('<')) return groups.named?.[body.slice(1, -1)] ?? '';
155
+ if (body.length === 2) {
156
+ const direct = groups.numbered[Number(body)];
157
+ if (direct !== undefined) return direct;
158
+ const first = groups.numbered[Number(body[0])];
159
+ return first === undefined ? '' : first + body[1];
160
+ }
161
+ return groups.numbered[Number(body)] ?? '';
162
+ });
163
+ }
164
+
165
+ /**
166
+ * Replace one match. Returns the new buffer and where the caret should land
167
+ * (just after the inserted text), or null when the match is no longer there —
168
+ * the regex is re-run at the match's own offset rather than trusting the
169
+ * offsets handed in, so a stale list cannot splice the wrong range.
170
+ */
171
+ export function replaceMatch(
172
+ text: string,
173
+ match: FindMatch,
174
+ query: string,
175
+ replacement: string,
176
+ options: FindOptions,
177
+ ): { text: string; caret: number } | null {
178
+ const regex = buildFindRegex(query, options);
179
+ if (!regex) return null;
180
+ // The pattern is re-run at the match's own offset rather than trusting the
181
+ // offsets handed in: a list computed against an older buffer cannot splice
182
+ // the wrong range out of the current one.
183
+ regex.lastIndex = match.start;
184
+ const found = regex.exec(text);
185
+ if (!found || found.index !== match.start) return null;
186
+ if (!options.isRegex) {
187
+ // Literal mode inserts the field verbatim: `$&` in the replace box is the
188
+ // two characters the user typed, not the match.
189
+ return {
190
+ text: text.slice(0, match.start) + replacement + text.slice(match.end),
191
+ caret: match.start + replacement.length,
192
+ };
193
+ }
194
+ const inserted = expandReplacement(replacement, groupsOf(found));
195
+ return {
196
+ text: text.slice(0, found.index) + inserted + text.slice(found.index + found[0].length),
197
+ caret: found.index + inserted.length,
198
+ };
199
+ }
200
+
201
+ /** Replace every match. Null when the query is empty or does not compile. */
202
+ export function replaceAllMatches(
203
+ text: string,
204
+ query: string,
205
+ replacement: string,
206
+ options: FindOptions,
207
+ ): string | null {
208
+ const regex = buildFindRegex(query, options);
209
+ if (!regex) return null;
210
+ if (!options.isRegex) return text.replace(regex, () => replacement);
211
+ return text.replace(regex, (...args: unknown[]) => {
212
+ // A replacer's arguments are `(match, …groups, offset, string, groups?)`;
213
+ // rebuilding an exec result from them keeps one expansion rule for both
214
+ // the single and the bulk path.
215
+ const last = args[args.length - 1];
216
+ const hasNamed = typeof last === 'object' && last !== null;
217
+ const groups = args.slice(1, args.length - (hasNamed ? 3 : 2)) as string[];
218
+ return expandReplacement(replacement, {
219
+ full: String(args[0]),
220
+ numbered: [undefined, ...groups],
221
+ named: hasNamed ? (last as Record<string, string | undefined>) : undefined,
222
+ });
223
+ });
224
+ }
@@ -14,7 +14,7 @@ import {
14
14
  stepHistory,
15
15
  HISTORY_TIME_GAP,
16
16
  type HistoryRecord,
17
- } from '@/client/components/common/code-editor/history';
17
+ } from '@/shared/lib/code/editor/history';
18
18
 
19
19
  const NOW = 1_700_000_000_000;
20
20
 
@@ -0,0 +1,109 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The editor keymap's contract.
8
+ *
9
+ * These assertions exist because the failure mode is silent: a chord matched on
10
+ * `event.key` never fires on macOS (Option produces a different character), a
11
+ * platform whose binding was forgotten makes a documented shortcut dead, and a
12
+ * label that disagrees with the stroke it describes turns a tooltip into a
13
+ * lie. Each of those is a test here rather than a bug report.
14
+ */
15
+
16
+ import { describe, expect, test } from 'bun:test';
17
+
18
+ import { EDITOR_KEY_BINDINGS, editorStrokeFor, isFindBarCommand } from '@/shared/lib/code/editor/keymap';
19
+ import { describeBinding, resolveBinding, type KeyboardEventLike, type KeyBinding } from '@/shared/lib/ui/key-binding';
20
+ import type { EditorCommand } from '@/shared/lib/code/editor/keymap';
21
+
22
+ /** A keyboard event with only the fields the matcher reads. */
23
+ function key(code: string, keyName: string, modifiers: Partial<KeyboardEventLike> = {}): KeyboardEventLike {
24
+ return { code, key: keyName, metaKey: false, ctrlKey: false, altKey: false, shiftKey: false, ...modifiers };
25
+ }
26
+
27
+ function commandFor(event: KeyboardEventLike, isMac: boolean): EditorCommand | null {
28
+ return resolveBinding(EDITOR_KEY_BINDINGS, event, isMac);
29
+ }
30
+
31
+ describe('editor keymap', () => {
32
+ test('⌘D adds the next occurrence, ⇧⌘D inserts a cursor below', () => {
33
+ expect(commandFor(key('KeyD', 'd', { metaKey: true }), true)).toBe('selectNextOccurrence');
34
+ expect(commandFor(key('KeyD', 'd', { ctrlKey: true }), false)).toBe('selectNextOccurrence');
35
+ expect(commandFor(key('KeyD', 'd', { metaKey: true, shiftKey: true }), true)).toBe('insertCursorBelow');
36
+ });
37
+
38
+ test('a binding from the other platform never fires', () => {
39
+ // Ctrl+D on macOS is the emacs forward-delete; ⌘D on Windows belongs to nobody.
40
+ expect(commandFor(key('KeyD', 'd', { ctrlKey: true }), true)).toBeNull();
41
+ expect(commandFor(key('KeyD', 'd', { metaKey: true }), false)).toBeNull();
42
+ });
43
+
44
+ test('the line commands use the arrows with the modifiers each platform expects', () => {
45
+ expect(commandFor(key('ArrowUp', 'ArrowUp', { altKey: true }), true)).toBe('moveLineUp');
46
+ expect(commandFor(key('ArrowDown', 'ArrowDown', { altKey: true }), true)).toBe('moveLineDown');
47
+ expect(commandFor(key('ArrowDown', 'ArrowDown', { metaKey: true, altKey: true, shiftKey: true }), true)).toBe('copyLineDown');
48
+ expect(commandFor(key('KeyK', 'k', { metaKey: true, shiftKey: true }), true)).toBe('deleteLine');
49
+ });
50
+
51
+ test('toggle comment is read from the physical Slash key', () => {
52
+ expect(commandFor(key('Slash', '/', { metaKey: true }), true)).toBe('toggleComment');
53
+ expect(commandFor(key('Slash', '/', { ctrlKey: true }), false)).toBe('toggleComment');
54
+ });
55
+
56
+ test('every command carries both platforms and a label', () => {
57
+ for (const binding of EDITOR_KEY_BINDINGS) {
58
+ expect(binding.label.length).toBeGreaterThan(0);
59
+ expect(binding.mac.code.length).toBeGreaterThan(0);
60
+ expect(binding.other.code.length).toBeGreaterThan(0);
61
+ }
62
+ });
63
+
64
+ test('no two commands claim the same chord on a platform', () => {
65
+ for (const isMac of [true, false]) {
66
+ const seen = new Map<string, EditorCommand>();
67
+ for (const binding of EDITOR_KEY_BINDINGS) {
68
+ const stroke = editorStrokeFor(binding.command, isMac, false);
69
+ if (!stroke) continue;
70
+ const id = `${stroke.code}|${stroke.meta ? 'm' : ''}${stroke.ctrl ? 'c' : ''}${stroke.alt ? 'a' : ''}${stroke.shift ? 's' : ''}`;
71
+ const owner = seen.get(id);
72
+ expect(`${binding.command} vs ${owner}`).toBe(`${binding.command} vs undefined`);
73
+ seen.set(id, binding.command);
74
+ }
75
+ }
76
+ });
77
+
78
+ test('redo has a Windows-only chord without disturbing the others', () => {
79
+ expect(editorStrokeFor('redo', true, false)).toEqual({ code: 'KeyZ', meta: true, shift: true });
80
+ expect(editorStrokeFor('redo', false, true)).toEqual({ code: 'KeyY', ctrl: true });
81
+ expect(editorStrokeFor('redo', false, false)).toEqual({ code: 'KeyZ', ctrl: true, shift: true });
82
+ });
83
+
84
+ test('the find-bar commands are exactly the ones the bar owns', () => {
85
+ expect(isFindBarCommand('find')).toBe(true);
86
+ expect(isFindBarCommand('toggleComment')).toBe(false);
87
+ expect(isFindBarCommand('selectNextOccurrence')).toBe(false);
88
+ });
89
+ });
90
+
91
+ describe('binding labels', () => {
92
+ const binding = (command: EditorCommand): KeyBinding<EditorCommand> => {
93
+ const found = EDITOR_KEY_BINDINGS.find((entry) => entry.command === command);
94
+ if (!found) throw new Error(`no binding for ${command}`);
95
+ return found;
96
+ };
97
+
98
+ test('mac chords render without separators', () => {
99
+ expect(describeBinding(binding('selectNextOccurrence'), true)).toBe('⌘D');
100
+ expect(describeBinding(binding('copyLineDown'), true)).toBe('⌥⇧⌘ArrowDown');
101
+ expect(describeBinding(binding('toggleComment'), true)).toBe('⌘/');
102
+ });
103
+
104
+ test('other platforms render with + separators', () => {
105
+ expect(describeBinding(binding('selectNextOccurrence'), false)).toBe('Ctrl+D');
106
+ expect(describeBinding(binding('moveLineUp'), false)).toBe('Alt+ArrowUp');
107
+ expect(describeBinding(binding('lineStart'), false)).toBe('Home');
108
+ });
109
+ });