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
|
@@ -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
|
-
|
|
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
|
+
}
|