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,260 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The editor's text commands — everything a keybinding does to the buffer that
8
+ * is not typing: line comment toggle, line move/duplicate/delete, insert line,
9
+ * the ⌘D occurrence selection, and the edit replication that makes a
10
+ * multi-selection actually editable.
11
+ *
12
+ * All of it is pure string arithmetic, so the rules are testable without a DOM
13
+ * and the component keeps only the parts that need one (reading the textarea,
14
+ * committing through the undo history).
15
+ *
16
+ * MULTI-SELECTION MODEL: the textarea owns exactly one selection — the
17
+ * "primary" — and every other range lives in `occurrences`. Painting them is
18
+ * the highlight layer's job (they are marked like a find match); editing them
19
+ * works by REPLICATION: the browser performs the user's keystroke at the
20
+ * primary range, the resulting edit is diffed out of the new buffer, and the
21
+ * same replacement is applied to every other range in one pass. That is why a
22
+ * keystroke, a paste and an IME composition all need no special case here.
23
+ */
24
+
25
+ import { lineRangesInSelection, type TextRange } from '@/shared/lib/code/editor/lines';
26
+
27
+ export type { TextRange };
28
+ export { deleteLines, duplicateLines, insertLine, lineEdges, moveLines } from '@/shared/lib/code/editor/line-edits';
29
+
30
+ /** One edit the browser made, as the difference between two buffers. */
31
+ export interface EditDiff {
32
+ /** Range in the OLD buffer that was replaced. */
33
+ start: number;
34
+ end: number;
35
+ /** Text that took its place (empty for a deletion). */
36
+ inserted: string;
37
+ }
38
+
39
+ /** Cap on occurrences a single command may select — a one-character ⌘⇧L on a big file must not build 100k ranges. */
40
+ export const MAX_OCCURRENCES = 2_000;
41
+
42
+ /**
43
+ * The single edit that turns `before` into `after`, as a common-prefix /
44
+ * common-suffix diff. This is what recovers "what did the user just type" from
45
+ * a textarea `input` event, which reports a whole buffer rather than a keystroke.
46
+ *
47
+ * A prefix/suffix diff is ambiguous on repetitive text: deleting the first `a`
48
+ * of `aab` and deleting the second both yield `ab`, and the plain diff always
49
+ * reports the LAST one. `limit` is the caller's knowledge of where the edit
50
+ * happened — the primary selection before the keystroke — and the reported
51
+ * region is widened to contain it. The result is always a valid diff (applying
52
+ * it reproduces `after`) and never claims the edit happened away from the
53
+ * caret.
54
+ */
55
+ export function diffEdit(before: string, after: string, limit?: TextRange): EditDiff {
56
+ const max = Math.min(before.length, after.length);
57
+ let prefix = 0;
58
+ while (prefix < max && before[prefix] === after[prefix]) prefix++;
59
+
60
+ let suffix = 0;
61
+ while (suffix < max - prefix && before[before.length - 1 - suffix] === after[after.length - 1 - suffix]) {
62
+ suffix++;
63
+ }
64
+
65
+ let start = prefix;
66
+ let end = before.length - suffix;
67
+ if (limit) {
68
+ start = Math.min(start, limit.start);
69
+ end = Math.max(end, limit.end);
70
+ }
71
+ return { start, end, inserted: after.slice(start, after.length - (before.length - end)) };
72
+ }
73
+
74
+ /**
75
+ * Apply `inserted` at every range and return the new buffer with each range
76
+ * collapsed to a caret after its own insertion.
77
+ *
78
+ * Ranges are replaced back-to-front so an earlier replacement cannot move the
79
+ * offsets of a later one, while the carets are accumulated front-to-back —
80
+ * each one sits after every insertion that precedes it in the new buffer.
81
+ */
82
+ export function replicateEdit(
83
+ text: string,
84
+ ranges: readonly TextRange[],
85
+ inserted: string,
86
+ ): { value: string; carets: number[] } {
87
+ const ordered = [...ranges].sort((a, b) => a.start - b.start);
88
+ let value = text;
89
+ for (let index = ordered.length - 1; index >= 0; index--) {
90
+ const range = ordered[index];
91
+ value = value.slice(0, range.start) + inserted + value.slice(range.end);
92
+ }
93
+ const carets: number[] = new Array(ordered.length);
94
+ let shift = 0;
95
+ for (let index = 0; index < ordered.length; index++) {
96
+ const range = ordered[index];
97
+ const insertedLength = inserted.length - (range.end - range.start);
98
+ carets[index] = range.start + shift + inserted.length;
99
+ shift += insertedLength;
100
+ }
101
+ return { value, carets };
102
+ }
103
+
104
+ /**
105
+ * The word the caret sits on: an identifier-ish run of `[\w$]`, with the
106
+ * caret allowed at either edge (⌘D pressed just after a word selects it).
107
+ * Returns null when the caret is not on a word, which is the caller's cue to do
108
+ * nothing rather than to guess at a line.
109
+ */
110
+ export function wordRangeAt(text: string, offset: number): TextRange | null {
111
+ const isWord = (char: string | undefined) => char !== undefined && /[\w$]/.test(char);
112
+ let start = Math.min(offset, text.length);
113
+ let end = start;
114
+ if (!isWord(text[start]) && isWord(text[start - 1])) start--;
115
+ if (!isWord(text[start])) return null;
116
+ end = start;
117
+ while (isWord(text[start - 1])) start--;
118
+ while (isWord(text[end])) end++;
119
+ return { start, end };
120
+ }
121
+
122
+ /** Every occurrence of `query` in `text`, in document order. */
123
+ export function occurrenceRanges(text: string, query: string, limit: number = MAX_OCCURRENCES): TextRange[] {
124
+ if (!query) return [];
125
+ const ranges: TextRange[] = [];
126
+ for (let index = text.indexOf(query); index !== -1; index = text.indexOf(query, index + query.length)) {
127
+ ranges.push({ start: index, end: index + query.length });
128
+ if (ranges.length >= limit) break;
129
+ }
130
+ return ranges;
131
+ }
132
+
133
+ /**
134
+ * The next occurrence after the last selected range, wrapping to the first one
135
+ * that is not already selected. Null when the document holds no further
136
+ * occurrence — the caller then leaves the selection alone instead of cycling
137
+ * silently.
138
+ */
139
+ export function nextOccurrence(text: string, query: string, selected: readonly TextRange[]): TextRange | null {
140
+ const all = occurrenceRanges(text, query);
141
+ if (all.length === 0) return null;
142
+ const taken = new Set(selected.map((range) => `${range.start}:${range.end}`));
143
+ const after = selected.reduce((max, range) => Math.max(max, range.end), -1);
144
+ const found = all.findIndex((range) => range.start >= after);
145
+ const from = found === -1 ? 0 : found;
146
+ for (let step = 0; step < all.length; step++) {
147
+ const range = all[(from + step) % all.length];
148
+ if (!taken.has(`${range.start}:${range.end}`)) return range;
149
+ }
150
+ return null;
151
+ }
152
+
153
+ /** A line-comment syntax: a prefix per line, or a wrapper for languages that have only block comments. */
154
+ export type CommentSyntax = { line: string } | { block: readonly [string, string] };
155
+
156
+ /**
157
+ * How each language's line comment is written. The default is `//`, which is
158
+ * the right answer for every C-family grammar; the exceptions are the ones a
159
+ * wrong token would visibly break (`#` in YAML would be a syntax error, not a
160
+ * comment). Languages that only have block comments get the wrapper — a `//`
161
+ * inserted into CSS comments out nothing and breaks the rule it was added to.
162
+ */
163
+ const COMMENT_SYNTAX: Record<string, CommentSyntax> = {
164
+ python: { line: '#' },
165
+ bash: { line: '#' },
166
+ yaml: { line: '#' },
167
+ toml: { line: '#' },
168
+ ruby: { line: '#' },
169
+ perl: { line: '#' },
170
+ r: { line: '#' },
171
+ makefile: { line: '#' },
172
+ dockerfile: { line: '#' },
173
+ ini: { line: '#' },
174
+ properties: { line: '#' },
175
+ elixir: { line: '#' },
176
+ graphql: { line: '#' },
177
+ nix: { line: '#' },
178
+ sql: { line: '--' },
179
+ lua: { line: '--' },
180
+ haskell: { line: '--' },
181
+ css: { block: ['/*', '*/'] },
182
+ scss: { block: ['/*', '*/'] },
183
+ less: { block: ['/*', '*/'] },
184
+ sass: { block: ['/*', '*/'] },
185
+ stylus: { block: ['/*', '*/'] },
186
+ html: { block: ['<!--', '-->'] },
187
+ xml: { block: ['<!--', '-->'] },
188
+ markdown: { block: ['<!--', '-->'] },
189
+ };
190
+
191
+ export function commentSyntaxFor(language: string): CommentSyntax {
192
+ return COMMENT_SYNTAX[language] ?? { line: '//' };
193
+ }
194
+
195
+ /** The lines a selection touches, as ranges that INCLUDE their newline. */
196
+ function selectionLines(text: string, selection: TextRange): TextRange[] {
197
+ return lineRangesInSelection(text, selection);
198
+ }
199
+
200
+ /** Whether every line's content already begins with `token`. */
201
+ function allCommented(text: string, lines: readonly TextRange[], token: string): boolean {
202
+ return lines.every((line) => text.slice(line.start, line.end).trimStart().startsWith(token));
203
+ }
204
+
205
+ /**
206
+ * Toggle line comments over every line the selection touches.
207
+ *
208
+ * Adding puts the token at the first non-space character, so indentation
209
+ * survives; removing takes the token and the single space an add would have
210
+ * written, which is what makes the toggle idempotent. For a block-only language
211
+ * the whole selection is wrapped instead, once.
212
+ */
213
+ export function toggleLineComment(
214
+ text: string,
215
+ selection: TextRange,
216
+ syntax: CommentSyntax,
217
+ ): { value: string; selectionStart: number; selectionEnd: number } {
218
+ if ('block' in syntax) {
219
+ const [open, close] = syntax.block;
220
+ const before = text.slice(0, selection.start);
221
+ const body = text.slice(selection.start, selection.end);
222
+ const wrapped = `${open} ${body} ${close}`;
223
+ return {
224
+ value: before + wrapped + text.slice(selection.end),
225
+ selectionStart: selection.start,
226
+ selectionEnd: selection.start + wrapped.length,
227
+ };
228
+ }
229
+
230
+ const token = syntax.line;
231
+ const lines = selectionLines(text, selection);
232
+ const remove = allCommented(text, lines, token);
233
+ let value = text;
234
+ for (let index = lines.length - 1; index >= 0; index--) {
235
+ const line = lines[index];
236
+ const content = text.slice(line.start, line.end);
237
+ const indentLength = content.length - content.trimStart().length;
238
+ const at = line.start + indentLength;
239
+ if (remove) {
240
+ const after = value.slice(at + token.length);
241
+ const withSpace = after.startsWith(' ') ? after.slice(1) : after;
242
+ value = value.slice(0, at) + withSpace;
243
+ } else {
244
+ value = value.slice(0, at) + `${token} ` + value.slice(at);
245
+ }
246
+ }
247
+
248
+ // The selection keeps covering the same text: adding shifts both edges by the
249
+ // token (plus its space) per touched line, removing shifts them back.
250
+ const delta = token.length + 1;
251
+ const touched = lines.length;
252
+ const shift = remove ? -delta * touched : delta * touched;
253
+ const startShift = remove ? Math.min(0, shift) : shift;
254
+ return {
255
+ value,
256
+ selectionStart: Math.max(0, selection.start + (remove ? shift : startShift)),
257
+ selectionEnd: Math.max(0, selection.end + shift),
258
+ };
259
+ }
260
+
@@ -0,0 +1,100 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The editor's indent edits, which are the rules a reader notices only when
8
+ * they are wrong: Tab indents every line the selection touches, ⇧Tab removes
9
+ * one level, and the selection's own edges move with the text so a selection
10
+ * that does not start at a line start is not silently shortened.
11
+ */
12
+
13
+ import { describe, expect, test } from 'bun:test';
14
+
15
+ import { editForBackspace, editForEnter, editForTab, TAB_CHARACTER } from '@/shared/lib/code/editor/edits';
16
+
17
+ describe('editForTab', () => {
18
+ test('inserts one indent at the caret and lands after it', () => {
19
+ const edit = editForTab('const a = 1;', 6, 6, false);
20
+
21
+ expect(edit.value).toBe(`const ${TAB_CHARACTER}a = 1;`);
22
+ expect([edit.selectionStart, edit.selectionEnd]).toEqual([8, 8]);
23
+ });
24
+
25
+ test('indents every line the selection touches', () => {
26
+ const edit = editForTab('one\ntwo\nthree', 0, 7, false);
27
+
28
+ expect(edit.value).toBe(`${TAB_CHARACTER}one\n${TAB_CHARACTER}two\nthree`);
29
+ // The end moves by one indent per touched line, so the selection still
30
+ // covers the same text.
31
+ expect(edit.selectionEnd).toBe(7 + TAB_CHARACTER.length * 2);
32
+ });
33
+
34
+ test('an indent moves a selection that starts on a line with content', () => {
35
+ // The indent is inserted BEFORE the selection start, so the selection has
36
+ // to move with its own text or it would silently cover different bytes.
37
+ const midLine = editForTab('one\ntwo', 1, 5, false);
38
+ expect(midLine.value).toBe(`${TAB_CHARACTER}one\n${TAB_CHARACTER}two`);
39
+ expect(midLine.selectionStart).toBe(1 + TAB_CHARACTER.length);
40
+ expect(midLine.selectionEnd).toBe(5 + TAB_CHARACTER.length * 2);
41
+ });
42
+
43
+ test('unindents only the lines that carry an indent', () => {
44
+ const edit = editForTab(`${TAB_CHARACTER}one\ntwo`, 0, 9, true);
45
+
46
+ expect(edit.value).toBe('one\ntwo');
47
+ expect(edit.selectionStart).toBe(0);
48
+ expect(edit.selectionEnd).toBe(9 - TAB_CHARACTER.length);
49
+ });
50
+
51
+ test('unindenting a selection whose first line has no indent keeps the start', () => {
52
+ const edit = editForTab(`one\n${TAB_CHARACTER}two`, 0, 9, true);
53
+
54
+ expect(edit.value).toBe('one\ntwo');
55
+ expect(edit.selectionStart).toBe(0);
56
+ });
57
+
58
+ test('⇧Tab with nothing selected leaves the buffer alone', () => {
59
+ const edit = editForTab('one', 2, 2, true);
60
+
61
+ expect(edit.value).toBe('one');
62
+ expect([edit.selectionStart, edit.selectionEnd]).toEqual([2, 2]);
63
+ });
64
+ });
65
+
66
+ describe('editForBackspace', () => {
67
+ test('removes a whole indent unit', () => {
68
+ const edit = editForBackspace(`${TAB_CHARACTER}one`, TAB_CHARACTER.length, TAB_CHARACTER.length);
69
+
70
+ expect(edit?.value).toBe('one');
71
+ expect(edit?.selectionStart).toBe(0);
72
+ });
73
+
74
+ test('leaves a partial indent to the browser', () => {
75
+ // One space of an indent is an ordinary character deletion.
76
+ expect(editForBackspace(' one', 1, 1)).toBeNull();
77
+ expect(editForBackspace('one', 3, 3)).toBeNull();
78
+ });
79
+
80
+ test('never fires over a selection', () => {
81
+ expect(editForBackspace(' one', 0, 2)).toBeNull();
82
+ });
83
+ });
84
+
85
+ describe('editForEnter', () => {
86
+ test('keeps the current line’s leading whitespace', () => {
87
+ const edit = editForEnter(' one', 5, 5);
88
+
89
+ expect(edit?.value).toBe(' one\n ');
90
+ expect(edit?.selectionStart).toBe(8);
91
+ });
92
+
93
+ test('a line with no indent is left to the browser', () => {
94
+ expect(editForEnter('one', 3, 3)).toBeNull();
95
+ });
96
+
97
+ test('a selection is never auto-indented', () => {
98
+ expect(editForEnter(' one', 2, 5)).toBeNull();
99
+ });
100
+ });
@@ -0,0 +1,122 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The text edits the editor's own keydown handler performs — Tab/⇧Tab indent,
8
+ * backspace over an indent, and Enter's auto-indent.
9
+ *
10
+ * These are pure functions of the buffer and the selection, so they are
11
+ * testable without a DOM, and the component keeps only the part that needs one
12
+ * (reading the `<textarea>` and committing the result through the undo
13
+ * history). Each returns `null` when the key should be left to the browser.
14
+ */
15
+
16
+ /** What an edit produces: the new buffer and where the selection lands. */
17
+ export interface EditorEdit {
18
+ value: string;
19
+ selectionStart: number;
20
+ selectionEnd: number;
21
+ }
22
+
23
+ export const TAB_CHARACTER = ' ';
24
+
25
+ /** The whole line containing `offset`, newline included. */
26
+ function lineRangeAt(text: string, offset: number): { start: number; end: number } {
27
+ const before = text.slice(0, Math.min(offset, text.length));
28
+ const start = before.lastIndexOf('\n') + 1;
29
+ const nextBreak = text.indexOf('\n', start);
30
+ return { start, end: nextBreak === -1 ? text.length : nextBreak + 1 };
31
+ }
32
+
33
+ /** Lines `[startLine, endLine]` of `text`, as offsets of their first characters. */
34
+ function lineRange(text: string, selectionStart: number, selectionEnd: number): { startLine: number; endLine: number; lines: string[] } {
35
+ const lines = text.split('\n');
36
+ let startLine = 0;
37
+ for (let index = text.indexOf('\n'); index !== -1 && index < selectionStart; index = text.indexOf('\n', index + 1)) {
38
+ startLine++;
39
+ }
40
+ let endLine = startLine;
41
+ for (let index = text.indexOf('\n', selectionStart); index !== -1 && index < selectionEnd; index = text.indexOf('\n', index + 1)) {
42
+ endLine++;
43
+ }
44
+ return { startLine, endLine, lines };
45
+ }
46
+
47
+ /**
48
+ * Tab on a selection indents every line it touches; ⇧Tab removes one level.
49
+ *
50
+ * The selection's own edges move with the edit — an indent inserts before the
51
+ * caret and at each line start, so a selection that is not at a line start
52
+ * would otherwise shrink by one indent per line.
53
+ */
54
+ export function editForTab(text: string, selectionStart: number, selectionEnd: number, unindent: boolean): EditorEdit {
55
+ if (selectionStart === selectionEnd) {
56
+ if (unindent) {
57
+ // ⇧Tab on a bare caret outdents the line it sits on, which is what the
58
+ // key means in VS Code — treating it as a no-op left ⌘[ (the same
59
+ // command, reached from the keymap) doing nothing at all.
60
+ const line = lineRangeAt(text, selectionStart);
61
+ const content = text.slice(line.start, line.end);
62
+ if (!content.startsWith(TAB_CHARACTER)) return { value: text, selectionStart, selectionEnd };
63
+ const caret = Math.max(line.start, selectionStart - TAB_CHARACTER.length);
64
+ return {
65
+ value: text.slice(0, line.start) + content.slice(TAB_CHARACTER.length) + text.slice(line.end),
66
+ selectionStart: caret,
67
+ selectionEnd: caret,
68
+ };
69
+ }
70
+ const caret = selectionStart + TAB_CHARACTER.length;
71
+ return { value: text.slice(0, selectionStart) + TAB_CHARACTER + text.slice(selectionEnd), selectionStart: caret, selectionEnd: caret };
72
+ }
73
+
74
+ const { startLine, endLine, lines } = lineRange(text, selectionStart, selectionEnd);
75
+ if (unindent) {
76
+ const next = lines.map((line, index) =>
77
+ index >= startLine && index <= endLine && line.startsWith(TAB_CHARACTER) ? line.slice(TAB_CHARACTER.length) : line,
78
+ );
79
+ const nextValue = next.join('\n');
80
+ const firstHadIndent = lines[startLine]?.startsWith(TAB_CHARACTER) ?? false;
81
+ return {
82
+ value: nextValue,
83
+ // Clamped: a selection beginning INSIDE the indent it removes shifts
84
+ // before the buffer's own start (`0 - 2`), which only ever looked correct
85
+ // because assigning a negative offset to a textarea clamps to zero.
86
+ selectionStart: firstHadIndent ? Math.max(0, selectionStart - TAB_CHARACTER.length) : selectionStart,
87
+ selectionEnd: selectionEnd - (text.length - nextValue.length),
88
+ };
89
+ }
90
+
91
+ const next = lines.map((line, index) => (index >= startLine && index <= endLine ? TAB_CHARACTER + line : line));
92
+ const firstLineText = lines[startLine];
93
+ return {
94
+ value: next.join('\n'),
95
+ // The indent lands before the selection start, so a selection that begins
96
+ // on a line with content moves with its text.
97
+ selectionStart: firstLineText && /\S/.test(firstLineText) ? selectionStart + TAB_CHARACTER.length : selectionStart,
98
+ selectionEnd: selectionEnd + TAB_CHARACTER.length * (endLine - startLine + 1),
99
+ };
100
+ }
101
+
102
+ /** Backspace over a whole indent unit removes it; otherwise the browser handles the key. */
103
+ export function editForBackspace(text: string, selectionStart: number, selectionEnd: number): EditorEdit | null {
104
+ if (selectionStart !== selectionEnd) return null;
105
+ if (!text.slice(0, selectionStart).endsWith(TAB_CHARACTER)) return null;
106
+ const caret = selectionStart - TAB_CHARACTER.length;
107
+ return { value: text.slice(0, caret) + text.slice(selectionEnd), selectionStart: caret, selectionEnd: caret };
108
+ }
109
+
110
+ /** Enter keeps the current line's leading whitespace; the browser handles an unindented line. */
111
+ export function editForEnter(text: string, selectionStart: number, selectionEnd: number): EditorEdit | null {
112
+ if (selectionStart !== selectionEnd) return null;
113
+ const line = text.slice(0, selectionStart).split('\n').pop();
114
+ const indent = line?.match(/^\s+/)?.[0];
115
+ if (!indent) return null;
116
+ const caret = selectionStart + indent.length + 1;
117
+ return {
118
+ value: `${text.slice(0, selectionStart)}\n${indent}${text.slice(selectionEnd)}`,
119
+ selectionStart: caret,
120
+ selectionEnd: caret,
121
+ };
122
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The find widget's readouts.
8
+ *
9
+ * These are the two messages the widget exists to keep apart: "No results" and
10
+ * "your pattern does not compile" are different answers, and a match list that
11
+ * stopped at the cap has to say so rather than report a total it never counted.
12
+ */
13
+
14
+ import { describe, expect, test } from 'bun:test';
15
+
16
+ import { findCounterLabel, findStatusLabel } from '@/shared/lib/code/editor/find-status';
17
+
18
+ describe('findCounterLabel', () => {
19
+ test('counts from one, the way a reader does', () => {
20
+ expect(findCounterLabel({ total: 7, current: 2, truncated: false })).toBe('3 of 7');
21
+ });
22
+
23
+ test('a capped list is reported with a plus, never as a total', () => {
24
+ // The scan stopped at the cap; printing "1 of 5000" would claim the
25
+ // document holds exactly 5000 matches.
26
+ expect(findCounterLabel({ total: 5000, current: 0, truncated: true })).toBe('1 of 5000+');
27
+ });
28
+
29
+ test('says nothing when there is nothing to count', () => {
30
+ expect(findCounterLabel({ total: 0, current: -1, truncated: false })).toBe('');
31
+ });
32
+ });
33
+
34
+ describe('findStatusLabel', () => {
35
+ test('names an uncompilable pattern as such', () => {
36
+ expect(findStatusLabel('a(', true, { total: 0, current: -1, truncated: false })).toBe('Invalid pattern');
37
+ });
38
+
39
+ test('a query with no matches says so', () => {
40
+ expect(findStatusLabel('zzz', false, { total: 0, current: -1, truncated: false })).toBe('No results');
41
+ });
42
+
43
+ test('an empty query says nothing — an untouched field is not a failed search', () => {
44
+ expect(findStatusLabel('', false, { total: 0, current: -1, truncated: false })).toBe('');
45
+ });
46
+
47
+ test('otherwise it is the counter', () => {
48
+ expect(findStatusLabel('alpha', false, { total: 5, current: 0, truncated: false })).toBe('1 of 5');
49
+ });
50
+ });
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The find widget's readouts, as rules rather than JSX conditionals.
8
+ *
9
+ * Two of these are the widget's whole reason for being honest: "No results" and
10
+ * "your pattern does not compile" are different answers, and a match list that
11
+ * stopped at the cap has to say so rather than report a total it never counted.
12
+ */
13
+
14
+ /** Ceiling on collected matches; past it the count is reported with a `+`. */
15
+ export interface FindCounts {
16
+ total: number;
17
+ current: number;
18
+ truncated: boolean;
19
+ }
20
+
21
+ /** `3 of 7`, `1 of 5000+`, or an empty string when there is nothing to say. */
22
+ export function findCounterLabel({ total, current, truncated }: FindCounts): string {
23
+ if (total === 0) return '';
24
+ return `${current + 1} of ${total}${truncated ? '+' : ''}`;
25
+ }
26
+
27
+ /**
28
+ * The status line: an uncompilable pattern is named as such, a query with no
29
+ * matches says so, and an empty query says nothing at all (a "0 results" over
30
+ * an untouched field is noise).
31
+ */
32
+ export function findStatusLabel(query: string, invalid: boolean, counts: FindCounts): string {
33
+ if (invalid) return 'Invalid pattern';
34
+ if (counts.total === 0) return query ? 'No results' : '';
35
+ return findCounterLabel(counts);
36
+ }