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