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