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
@@ -1,15 +1,8 @@
1
1
  import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
2
2
  import type { RefObject } from 'preact';
3
3
 
4
- import {
5
- codeWindow,
6
- firstWindow,
7
- resolveWindow,
8
- sameWindow,
9
- type CodeWindow,
10
- type LineGeometry,
11
- type LineWindow,
12
- } from '@/shared/lib/code/lazy-window';
4
+ import { codeWindow, firstWindow, resolveWindow, sameWindow, type CodeWindow, type LineGeometry, type LineWindow } from '@/shared/lib/code/lazy-window';
5
+ import { findScrollContainer } from '@/client/hooks/editor/scroll-container';
13
6
 
14
7
  export interface UseCodeWindowOptions {
15
8
  /** The surface's root box: the frame every line offset in `geometry` is measured from. */
@@ -22,17 +15,6 @@ export interface UseCodeWindowOptions {
22
15
  enabled: boolean;
23
16
  }
24
17
 
25
- /** Nearest ancestor the document scrolls inside, or null when the page itself scrolls. */
26
- function findScrollContainer(element: HTMLElement): HTMLElement | null {
27
- for (let node = element.parentElement; node; node = node.parentElement) {
28
- const overflowY = getComputedStyle(node).overflowY;
29
- if ((overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay') && node.scrollHeight > node.clientHeight) {
30
- return node;
31
- }
32
- }
33
- return null;
34
- }
35
-
36
18
  /**
37
19
  * The lines visible inside the scroll container `root` lives in.
38
20
  *
@@ -0,0 +1,170 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * `useEditorFind` — writing.
8
+ *
9
+ * The rules asserted here are the ones a user notices when they break: a
10
+ * replacement is ONE undoable write that re-aims at the match after the text it
11
+ * just inserted, "replace all" reharness.writes every match, and a replacement that
12
+ * changes nothing harness.writes nothing at all.
13
+ */
14
+
15
+ import { describe, expect, test } from 'bun:test';
16
+
17
+ import { createFindHarness } from '@/client/hooks/editor/find-harness.test-util';
18
+
19
+ const harness = await createFindHarness();
20
+ harness.install();
21
+
22
+ describe('useEditorFind replacing', () => {
23
+ test('replace harness.writes one undoable edit and re-aims past the inserted text', async () => {
24
+ await harness.act(async () => {
25
+ harness.find?.openReplace();
26
+ });
27
+ await harness.act(async () => {
28
+ harness.find?.setReplacement('OMEGA');
29
+ });
30
+ await harness.act(async () => {
31
+ harness.find?.replaceCurrent();
32
+ });
33
+
34
+ expect(harness.writes[0].value).toBe('OMEGA beta alpha');
35
+ expect(harness.writes[0].caretStart).toBe(5);
36
+ });
37
+
38
+ test('replace all reharness.writes every match', async () => {
39
+ await harness.act(async () => {
40
+ harness.find?.openReplace();
41
+ });
42
+ await harness.act(async () => {
43
+ harness.find?.setReplacement('X');
44
+ });
45
+ await harness.act(async () => {
46
+ harness.find?.replaceAll();
47
+ });
48
+
49
+ expect(harness.writes[0].value).toBe('X beta X');
50
+ });
51
+
52
+ test('replace all on an unchanged buffer harness.writes nothing', async () => {
53
+ await harness.act(async () => {
54
+ harness.find?.openReplace();
55
+ });
56
+ await harness.act(async () => {
57
+ harness.find?.setQuery('beta');
58
+ });
59
+ await harness.act(async () => {
60
+ harness.find?.setReplacement('beta');
61
+ });
62
+ await harness.act(async () => {
63
+ harness.find?.replaceAll();
64
+ });
65
+
66
+ expect(harness.writes).toEqual([]);
67
+ });
68
+
69
+ test('replace with no current match harness.writes nothing', async () => {
70
+ await harness.act(async () => {
71
+ harness.find?.openReplace();
72
+ });
73
+ await harness.act(async () => {
74
+ harness.find?.setQuery('zzz');
75
+ });
76
+ await harness.act(async () => {
77
+ harness.find?.replaceCurrent();
78
+ });
79
+
80
+ expect(harness.writes).toEqual([]);
81
+ });
82
+ });
83
+
84
+ describe('useEditorFind options', () => {
85
+ test('toggling match case narrows the matches', async () => {
86
+ await harness.act(async () => {
87
+ harness.find?.openFind();
88
+ });
89
+ await harness.act(async () => {
90
+ harness.find?.setQuery('ALPHA');
91
+ });
92
+ expect(harness.find?.matches.length).toBe(2);
93
+
94
+ await harness.act(async () => {
95
+ harness.find?.toggleOption('matchCase');
96
+ });
97
+
98
+ expect(harness.find?.matches.length).toBe(0);
99
+ });
100
+
101
+ test('an invalid pattern is reported as such, not as an empty result', async () => {
102
+ await harness.act(async () => {
103
+ harness.find?.openFind();
104
+ });
105
+ await harness.act(async () => {
106
+ harness.find?.toggleOption('isRegex');
107
+ });
108
+ await harness.act(async () => {
109
+ harness.find?.setQuery('a(');
110
+ });
111
+
112
+ expect(harness.find?.invalid).toBe(true);
113
+ expect(harness.find?.matches).toEqual([]);
114
+ });
115
+ });
116
+
117
+ describe('useEditorFind commands', () => {
118
+ test('runCommand routes a find-bar chord to the bar’s own action', async () => {
119
+ await harness.act(async () => {
120
+ harness.find?.runCommand('find');
121
+ });
122
+ expect(harness.find?.open).toBe(true);
123
+
124
+ await harness.act(async () => {
125
+ harness.find?.runCommand('replace');
126
+ });
127
+ expect(harness.find?.replaceOpen).toBe(true);
128
+ });
129
+
130
+ test('an editor command the bar does not own is ignored, not misrouted', async () => {
131
+ await harness.act(async () => {
132
+ harness.find?.openFind();
133
+ });
134
+ await harness.act(async () => {
135
+ harness.find?.runCommand('deleteLine');
136
+ });
137
+
138
+ expect(harness.writes).toEqual([]);
139
+ expect(harness.find?.open).toBe(true);
140
+ });
141
+
142
+ test('closing puts focus back on the current match', async () => {
143
+ await harness.act(async () => {
144
+ harness.find?.openFind();
145
+ });
146
+ harness.selections.length = 0;
147
+
148
+ await harness.act(async () => {
149
+ harness.find?.close();
150
+ });
151
+
152
+ expect(harness.find?.open).toBe(false);
153
+ expect(harness.selections).toEqual([[0, 5, true]]);
154
+ });
155
+
156
+ test('a new document re-aims at the top instead of keeping the old offset', async () => {
157
+ await harness.act(async () => {
158
+ harness.find?.openFind();
159
+ });
160
+ await harness.act(async () => {
161
+ harness.find?.step(1);
162
+ });
163
+ expect(harness.find?.currentIndex).toBe(1);
164
+
165
+ harness.input.documentKey = 'file-2';
166
+ await harness.rerender();
167
+
168
+ expect(harness.find?.currentIndex).toBe(0);
169
+ });
170
+ });
@@ -0,0 +1,118 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * `useEditorFind` — opening and stepping.
8
+ *
9
+ * The rules asserted here are the ones a user notices when they break: the query
10
+ * seeds from the selection (VS Code's behaviour, and the reason ⌘F on a word is
11
+ * one keystroke), ⌘F on an open bar refocuses the field instead of overwriting
12
+ * what is in it, and a new document re-aims at the top rather than keeping an
13
+ * offset that means something else in this file.
14
+ */
15
+
16
+ import { describe, expect, test } from 'bun:test';
17
+
18
+ import { createFindHarness } from '@/client/hooks/editor/find-harness.test-util';
19
+
20
+ const harness = await createFindHarness();
21
+ harness.install();
22
+
23
+ describe('useEditorFind opening', () => {
24
+ test('⌘F seeds the query from the selection and jumps to the first match', async () => {
25
+ await harness.act(async () => {
26
+ harness.find?.openFind();
27
+ });
28
+
29
+ expect(harness.find?.open).toBe(true);
30
+ expect(harness.find?.query).toBe('alpha');
31
+ expect(harness.find?.matches.length).toBe(2);
32
+ expect(harness.find?.currentIndex).toBe(0);
33
+ expect(harness.reveals).toEqual([0]);
34
+ });
35
+
36
+ test('an empty or multi-line selection does not seed a query', async () => {
37
+ harness.input.selection = { start: 0, end: 0 };
38
+ await harness.act(async () => {
39
+ harness.find?.openFind();
40
+ });
41
+
42
+ expect(harness.find?.query).toBe('');
43
+ expect(harness.find?.open).toBe(true);
44
+ });
45
+
46
+ test('⌘F on an open bar keeps the query rather than re-seeding it', async () => {
47
+ // The bar keeps the current match selected in the document, so re-seeding
48
+ // would overwrite what the user typed with that match's text.
49
+ await harness.act(async () => {
50
+ harness.find?.openFind();
51
+ });
52
+ await harness.act(async () => {
53
+ harness.find?.setQuery('beta');
54
+ });
55
+ harness.input.selection = { start: 0, end: 5 };
56
+
57
+ await harness.act(async () => {
58
+ harness.find?.openFind();
59
+ });
60
+
61
+ expect(harness.find?.query).toBe('beta');
62
+ });
63
+
64
+ test('opening replace shows the second row and keeps the same match', async () => {
65
+ await harness.act(async () => {
66
+ harness.find?.openReplace();
67
+ });
68
+
69
+ expect(harness.find?.open).toBe(true);
70
+ expect(harness.find?.replaceOpen).toBe(true);
71
+ expect(harness.find?.currentIndex).toBe(0);
72
+ });
73
+ });
74
+
75
+ describe('useEditorFind stepping', () => {
76
+ test('F3 moves to the next match and wraps', async () => {
77
+ await harness.act(async () => {
78
+ harness.find?.openFind();
79
+ });
80
+ await harness.act(async () => {
81
+ harness.find?.step(1);
82
+ });
83
+ expect(harness.find?.currentIndex).toBe(1);
84
+
85
+ await harness.act(async () => {
86
+ harness.find?.step(1);
87
+ });
88
+ expect(harness.find?.currentIndex).toBe(0);
89
+ });
90
+
91
+ test('⇧F3 moves back, wrapping to the last match', async () => {
92
+ await harness.act(async () => {
93
+ harness.find?.openFind();
94
+ });
95
+ await harness.act(async () => {
96
+ harness.find?.step(-1);
97
+ });
98
+
99
+ expect(harness.find?.currentIndex).toBe(1);
100
+ });
101
+
102
+ test('stepping with no matches does nothing', async () => {
103
+ await harness.act(async () => {
104
+ harness.find?.openFind();
105
+ });
106
+ await harness.act(async () => {
107
+ harness.find?.setQuery('zzz');
108
+ });
109
+ const before = harness.find?.currentIndex;
110
+
111
+ await harness.act(async () => {
112
+ harness.find?.step(1);
113
+ });
114
+
115
+ expect(harness.find?.currentIndex).toBe(before);
116
+ expect(harness.find?.matches.length).toBe(0);
117
+ });
118
+ });
@@ -0,0 +1,321 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The editor's find/replace state machine.
8
+ *
9
+ * Owns the query, the option toggles, which match is "current", and the two
10
+ * write paths (replace one, replace all). Every rule it applies lives in
11
+ * `shared/lib/code/editor/find`, so the count in the widget, the marks painted
12
+ * on the surface and the bytes written back are the same list — the failure
13
+ * this shape exists to prevent is a widget that says "3 of 7" over highlights
14
+ * that disagree.
15
+ *
16
+ * The query and the toggles persist per session; whether the bar is OPEN does
17
+ * not. A reload that reopened a find bar over the file would be noise the user
18
+ * never asked for, and the same reasoning keeps the btw panel unpersisted.
19
+ */
20
+
21
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
22
+
23
+ import { useSessionState } from '@/client/hooks/workspace/session-state';
24
+ import {
25
+ DEFAULT_FIND_OPTIONS,
26
+ findMatches,
27
+ matchIndexAtOrAfter,
28
+ replaceAllMatches,
29
+ replaceMatch,
30
+ stepMatchIndex,
31
+ type FindOptions,
32
+ } from '@/shared/lib/code/editor/find';
33
+ import type { EditorCommand } from '@/shared/lib/code/editor/keymap';
34
+
35
+ export interface UseEditorFindOptions {
36
+ /** The buffer on screen. */
37
+ text: string;
38
+ /** Identity of the open document — a switch re-seeds the current match. */
39
+ documentKey: string;
40
+ /** Current selection, used to seed the query the way VS Code does. */
41
+ readSelection: () => { start: number; end: number } | null;
42
+ select: (start: number, end: number, focus?: boolean) => void;
43
+ reveal: (offset: number) => void;
44
+ /** Replace the buffer as ONE undoable edit; focus stays where the caller wants it. */
45
+ applyDocument: (value: string, caretStart: number, caretEnd: number) => void;
46
+ /** ⌥Z / Alt+Z — word wrap belongs to the editor panel, not to this hook. */
47
+ toggleWordWrap?: () => void;
48
+ }
49
+
50
+ export interface EditorFindState {
51
+ open: boolean;
52
+ replaceOpen: boolean;
53
+ query: string;
54
+ replacement: string;
55
+ options: FindOptions;
56
+ matches: readonly { start: number; end: number }[];
57
+ /** The document holds more matches than the cap. */
58
+ truncated: boolean;
59
+ /** The query does not compile as a regex — a different answer from "no matches". */
60
+ invalid: boolean;
61
+ currentIndex: number;
62
+ setQuery: (value: string) => void;
63
+ setReplacement: (value: string) => void;
64
+ openFind: () => void;
65
+ openReplace: () => void;
66
+ /** Expand/collapse the replace row without changing which match is current. */
67
+ toggleReplace: () => void;
68
+ /** Bumped on every "open find" so the widget can refocus its field. */
69
+ focusRequest: number;
70
+ close: () => void;
71
+ /** Dispatch a keymap command (⌘F, F3, ⌥⌘C, …). */
72
+ runCommand: (command: EditorCommand) => void;
73
+ toggleOption: (key: keyof FindOptions) => void;
74
+ step: (direction: 1 | -1) => void;
75
+ replaceCurrent: () => void;
76
+ replaceAll: () => void;
77
+ }
78
+
79
+ /** A seeded query must be one line of real text, like VS Code's. */
80
+ function seedQuery(text: string, selection: { start: number; end: number } | null): string | null {
81
+ if (!selection || selection.start === selection.end) return null;
82
+ const selected = text.slice(selection.start, selection.end);
83
+ if (!selected || selected.includes('\n') || selected.length > 200) return null;
84
+ return selected;
85
+ }
86
+
87
+ export function useEditorFind({
88
+ text,
89
+ documentKey,
90
+ readSelection,
91
+ select,
92
+ reveal,
93
+ applyDocument,
94
+ toggleWordWrap,
95
+ }: UseEditorFindOptions): EditorFindState {
96
+ const [query, setQueryState] = useSessionState<string>('editor.findQuery', '');
97
+ const [replacement, setReplacement] = useSessionState<string>('editor.findReplace', '');
98
+ const [storedOptions, setOptions] = useSessionState<FindOptions>('editor.findOptions', DEFAULT_FIND_OPTIONS);
99
+ // A blob written before an option existed carries only the keys it had, so
100
+ // the read is merged over the defaults rather than trusted wholesale.
101
+ const options = useMemo<FindOptions>(() => ({ ...DEFAULT_FIND_OPTIONS, ...storedOptions }), [storedOptions]);
102
+ const [open, setOpen] = useState(false);
103
+ const [replaceOpen, setReplaceOpen] = useState(false);
104
+ const [currentIndex, setCurrentIndex] = useState(-1);
105
+ /** Bumped by every open request so the widget can refocus its field. */
106
+ const [focusRequest, setFocusRequest] = useState(0);
107
+ /**
108
+ * Bumped whenever the current match must be re-resolved even though the match
109
+ * LIST has not changed — opening the bar on an unchanged query, or switching
110
+ * documents (where the offsets mean something else entirely). Without it the
111
+ * resolve effect never ran and the bar kept pointing at the previous file's
112
+ * index.
113
+ */
114
+ const [resolveRequest, setResolveRequest] = useState(0);
115
+ /** Read outside the callbacks: "already open" must not re-seed the query. */
116
+ const openRef = useRef(false);
117
+ openRef.current = open;
118
+
119
+ const result = useMemo(() => findMatches(text, query, options), [text, query, options]);
120
+
121
+ /**
122
+ * Where the current match is resolved FROM: the caret/anchor a reseed uses.
123
+ * A step and a replacement both move it, which is what keeps "next match"
124
+ * meaning the one after the one just replaced.
125
+ */
126
+ const anchorRef = useRef(0);
127
+ /** The next match-list change re-resolves the current match from the anchor. */
128
+ const reseedRef = useRef(true);
129
+
130
+ // Live mirrors: `reveal`/`select` close over the surface's own layout state,
131
+ // and the effects below must not re-run because a callback identity moved.
132
+ const revealRef = useRef(reveal);
133
+ revealRef.current = reveal;
134
+ const selectRef = useRef(select);
135
+ selectRef.current = select;
136
+
137
+ const reseed = useCallback((anchor: number) => {
138
+ anchorRef.current = anchor;
139
+ reseedRef.current = true;
140
+ }, []);
141
+
142
+ // A new document starts from the top: the offsets of the previous file's
143
+ // match say nothing about this one. The token is what makes the resolve below
144
+ // run when the match list itself is unchanged.
145
+ useEffect(() => {
146
+ reseed(0);
147
+ setResolveRequest((previous) => previous + 1);
148
+ }, [documentKey, reseed]);
149
+
150
+ // Resolve the current match whenever the match list moves under it — a
151
+ // keystroke in the query, a toggle, an edit, a replacement. Only a reseed
152
+ // scrolls: an edit in the document must not yank the view back to a match
153
+ // the user is no longer looking at.
154
+ useEffect(() => {
155
+ const matches = result.matches;
156
+ if (reseedRef.current) {
157
+ reseedRef.current = false;
158
+ const index = matchIndexAtOrAfter(matches, anchorRef.current);
159
+ setCurrentIndex(index);
160
+ if (index >= 0) {
161
+ selectRef.current(matches[index].start, matches[index].end, false);
162
+ revealRef.current(matches[index].start);
163
+ }
164
+ return;
165
+ }
166
+ setCurrentIndex((previous) => {
167
+ if (matches.length === 0) return -1;
168
+ if (previous < 0) return matchIndexAtOrAfter(matches, anchorRef.current);
169
+ return Math.min(previous, matches.length - 1);
170
+ });
171
+ // eslint-disable-next-line react-hooks/exhaustive-deps
172
+ }, [result, resolveRequest]);
173
+
174
+ const setQuery = useCallback(
175
+ (value: string) => {
176
+ setQueryState(value);
177
+ // Typing in the field is an explicit re-aim: the first match at or after
178
+ // the caret the widget opened on is the one the user means.
179
+ reseed(anchorRef.current);
180
+ },
181
+ [setQueryState, reseed],
182
+ );
183
+
184
+ const openFind = useCallback(() => {
185
+ setFocusRequest((previous) => previous + 1);
186
+ // Already open: ⌘F is a request for the field, not a new search. Re-seeding
187
+ // here would overwrite what the user typed with the current match's text,
188
+ // since the widget keeps that match selected in the document.
189
+ if (openRef.current) return;
190
+ const selection = readSelection();
191
+ const seeded = seedQuery(text, selection);
192
+ if (seeded) setQueryState(seeded);
193
+ reseed(selection ? selection.start : 0);
194
+ setOpen(true);
195
+ // The replace row is NOT collapsed here: ⌥⌘F opens the bar with it already
196
+ // showing, and this function is the path it takes.
197
+ setResolveRequest((previous) => previous + 1);
198
+ // eslint-disable-next-line react-hooks/exhaustive-deps
199
+ }, [text, readSelection, reseed, setQueryState]);
200
+
201
+ const openReplace = useCallback(() => {
202
+ setReplaceOpen(true);
203
+ openFind();
204
+ }, [openFind]);
205
+
206
+ const toggleReplace = useCallback(() => setReplaceOpen((prev) => !prev), []);
207
+
208
+ const close = useCallback(() => {
209
+ setOpen(false);
210
+ setReplaceOpen(false);
211
+ // Focus returns to the match, which is where the user was looking.
212
+ const match = result.matches[currentIndex];
213
+ if (match) select(match.start, match.end, true);
214
+ }, [result.matches, currentIndex, select]);
215
+
216
+ const step = useCallback(
217
+ (direction: 1 | -1) => {
218
+ const matches = result.matches;
219
+ if (matches.length === 0) return;
220
+ const index = stepMatchIndex(currentIndex, matches.length, direction);
221
+ setCurrentIndex(index);
222
+ anchorRef.current = matches[index].end;
223
+ // Selection is set without stealing focus: the user may be typing in the
224
+ // find field, and the current-match mark is what shows where it landed.
225
+ select(matches[index].start, matches[index].end, false);
226
+ reveal(matches[index].start);
227
+ },
228
+ [result.matches, currentIndex, select, reveal],
229
+ );
230
+
231
+ const toggleOption = useCallback(
232
+ (key: keyof FindOptions) => {
233
+ setOptions({ ...options, [key]: !options[key] });
234
+ reseed(anchorRef.current);
235
+ },
236
+ [options, setOptions, reseed],
237
+ );
238
+
239
+ const replaceCurrent = useCallback(() => {
240
+ const match = result.matches[currentIndex];
241
+ if (!match) return;
242
+ const next = replaceMatch(text, match, query, replacement, options);
243
+ if (!next) return;
244
+ // Focus stays in the widget: "Replace" is a button the user is about to
245
+ // press again, and pulling focus into the document would eat the next ⌘⌥F.
246
+ applyDocument(next.text, next.caret, next.caret);
247
+ reseed(next.caret);
248
+ }, [result.matches, currentIndex, text, query, replacement, options, applyDocument, reseed]);
249
+
250
+ const replaceAll = useCallback(() => {
251
+ const next = replaceAllMatches(text, query, replacement, options);
252
+ if (next === null || next === text) return;
253
+ const caret = Math.min(anchorRef.current, next.length);
254
+ applyDocument(next, caret, caret);
255
+ reseed(caret);
256
+ }, [text, query, replacement, options, applyDocument, reseed]);
257
+
258
+ const runCommand = useCallback(
259
+ (command: EditorCommand) => {
260
+ switch (command) {
261
+ case 'find':
262
+ openFind();
263
+ return;
264
+ case 'replace':
265
+ openReplace();
266
+ return;
267
+ case 'nextMatch':
268
+ step(1);
269
+ return;
270
+ case 'previousMatch':
271
+ step(-1);
272
+ return;
273
+ case 'replaceOne':
274
+ replaceCurrent();
275
+ return;
276
+ case 'replaceAll':
277
+ replaceAll();
278
+ return;
279
+ case 'toggleCaseSensitive':
280
+ toggleOption('matchCase');
281
+ return;
282
+ case 'toggleWholeWord':
283
+ toggleOption('wholeWord');
284
+ return;
285
+ case 'toggleRegex':
286
+ toggleOption('isRegex');
287
+ return;
288
+ case 'toggleWordWrap':
289
+ toggleWordWrap?.();
290
+ return;
291
+ default:
292
+ return;
293
+ }
294
+ },
295
+ [openFind, openReplace, step, replaceCurrent, replaceAll, toggleOption, toggleWordWrap],
296
+ );
297
+
298
+ return {
299
+ open,
300
+ replaceOpen,
301
+ query,
302
+ replacement,
303
+ options,
304
+ matches: result.matches,
305
+ truncated: result.truncated,
306
+ invalid: result.invalid,
307
+ currentIndex,
308
+ setQuery,
309
+ setReplacement,
310
+ openFind,
311
+ openReplace,
312
+ toggleReplace,
313
+ focusRequest,
314
+ close,
315
+ runCommand,
316
+ toggleOption,
317
+ step,
318
+ replaceCurrent,
319
+ replaceAll,
320
+ };
321
+ }