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,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The editor's reset stylesheet.
|
|
8
|
+
*
|
|
9
|
+
* The rule is what makes the editor work at all: the `<textarea>` must paint the
|
|
10
|
+
* caret and the selection while its glyphs stay invisible. A regression here is
|
|
11
|
+
* silent in review (it is a string) and immediately visible in the app, so it is
|
|
12
|
+
* asserted directly — the two selectors and the one deliberate exception.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, expect, test } from 'bun:test';
|
|
16
|
+
|
|
17
|
+
import { CODE_EDITOR_RESET_CSS } from '@/shared/lib/code/editor/reset-css';
|
|
18
|
+
|
|
19
|
+
describe('CODE_EDITOR_RESET_CSS', () => {
|
|
20
|
+
test('hides the textarea’s own glyphs', () => {
|
|
21
|
+
expect(CODE_EDITOR_RESET_CSS).toContain('-webkit-text-fill-color: transparent');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('keeps the placeholder visible, since no <pre> sits behind it', () => {
|
|
25
|
+
// Without this exception an empty editor's hint inherits the transparent
|
|
26
|
+
// fill and the field looks blank rather than empty.
|
|
27
|
+
expect(CODE_EDITOR_RESET_CSS).toContain('.code-editor-native-textarea::placeholder');
|
|
28
|
+
expect(CODE_EDITOR_RESET_CSS).toContain('var(--theme-ink)');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('is scoped to the editor’s own class, not to every textarea', () => {
|
|
32
|
+
expect(CODE_EDITOR_RESET_CSS).not.toMatch(/^\s*textarea\s*\{/m);
|
|
33
|
+
expect(CODE_EDITOR_RESET_CSS).toContain('.code-editor-native-textarea {');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The stylesheet the editor's two layers depend on.
|
|
8
|
+
*
|
|
9
|
+
* `CODE_EDITOR_RESET_CSS` is a rule, not a component: the transparent
|
|
10
|
+
* `<textarea>` paints the caret and the selection while its glyphs must stay
|
|
11
|
+
* invisible (the highlighted `<pre>` underneath supplies them), and the
|
|
12
|
+
* placeholder is the one thing that must remain visible. Keeping the rule in a
|
|
13
|
+
* `.ts` module rather than beside a JSX `<style>` element means a test can
|
|
14
|
+
* assert it without a DOM — and there is nothing to render but this string.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The reset rule, scoped to the textarea's own class.
|
|
19
|
+
*
|
|
20
|
+
* `::placeholder` is deliberately excluded: an empty editor's hint has no
|
|
21
|
+
* `<pre>` behind it, so it keeps the theme's ink and a readable opacity
|
|
22
|
+
* instead of inheriting the transparent fill.
|
|
23
|
+
*/
|
|
24
|
+
export const CODE_EDITOR_RESET_CSS = `
|
|
25
|
+
.code-editor-native-textarea {
|
|
26
|
+
-webkit-text-fill-color: transparent;
|
|
27
|
+
}
|
|
28
|
+
.code-editor-native-textarea::placeholder {
|
|
29
|
+
-webkit-text-fill-color: var(--theme-ink);
|
|
30
|
+
opacity: 0.45;
|
|
31
|
+
}
|
|
32
|
+
`;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The text-command dispatch.
|
|
8
|
+
*
|
|
9
|
+
* `runTextCommand` is the single entry point both the keyboard and the command
|
|
10
|
+
* palette go through, so what it asserts is the ROUTING: which commands it
|
|
11
|
+
* claims, which it leaves to the component (Escape, undo, Tab) and which it
|
|
12
|
+
* hands back to the panel. A command that returns `false` silently does nothing
|
|
13
|
+
* when a chord reaches it, which is why the membership table is asserted
|
|
14
|
+
* directly rather than only through the commands themselves.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { describe, expect, test } from 'bun:test';
|
|
18
|
+
|
|
19
|
+
import { isTextCommand, runTextCommand, type CommandContext } from '@/shared/lib/code/editor/text-commands';
|
|
20
|
+
import type { EditorCommand } from '@/shared/lib/code/editor/keymap';
|
|
21
|
+
|
|
22
|
+
interface Harness {
|
|
23
|
+
ctx: CommandContext;
|
|
24
|
+
committed: Array<{ value: string; selectionStart: number; selectionEnd: number }>;
|
|
25
|
+
carets: number[];
|
|
26
|
+
selections: Array<[number, number]>;
|
|
27
|
+
edges: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function harness(options: { value: string; start: number; end?: number; extend?: boolean; language?: string }): Harness {
|
|
31
|
+
const committed: Harness['committed'] = [];
|
|
32
|
+
const carets: number[] = [];
|
|
33
|
+
const selections: Array<[number, number]> = [];
|
|
34
|
+
const edges: string[] = [];
|
|
35
|
+
return {
|
|
36
|
+
committed,
|
|
37
|
+
carets,
|
|
38
|
+
selections,
|
|
39
|
+
edges,
|
|
40
|
+
ctx: {
|
|
41
|
+
value: options.value,
|
|
42
|
+
selectionStart: options.start,
|
|
43
|
+
selectionEnd: options.end ?? options.start,
|
|
44
|
+
extend: options.extend ?? false,
|
|
45
|
+
language: options.language ?? 'typescript',
|
|
46
|
+
commit: (edit) => committed.push(edit),
|
|
47
|
+
setCaret: (offset) => carets.push(offset),
|
|
48
|
+
selectRange: (start, end) => selections.push([start, end]),
|
|
49
|
+
setDocumentEdge: (edge) => edges.push(edge),
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe('isTextCommand', () => {
|
|
55
|
+
test('claims the line and caret commands', () => {
|
|
56
|
+
for (const command of ['moveLineUp', 'copyLineDown', 'deleteLine', 'toggleComment', 'indent', 'lineStart'] as const) {
|
|
57
|
+
expect(isTextCommand(command)).toBe(true);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('leaves the DOM-owned and panel-owned commands alone', () => {
|
|
62
|
+
// Undo/redo step the history, Escape is about focus, and the find-bar chords
|
|
63
|
+
// belong to the panel — none of them is buffer arithmetic.
|
|
64
|
+
for (const command of ['undo', 'redo', 'escape', 'find', 'selectNextOccurrence', 'toggleWordWrap'] as const) {
|
|
65
|
+
expect(isTextCommand(command)).toBe(false);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('runTextCommand', () => {
|
|
71
|
+
test('reports an unowned command rather than pretending to run it', () => {
|
|
72
|
+
const h = harness({ value: 'one', start: 0 });
|
|
73
|
+
expect(runTextCommand('find', h.ctx)).toBe(false);
|
|
74
|
+
expect(h.committed).toEqual([]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('moves a line through the commit path, not by writing the field', () => {
|
|
78
|
+
const h = harness({ value: 'one\ntwo', start: 4, end: 7 });
|
|
79
|
+
expect(runTextCommand('moveLineUp', h.ctx)).toBe(true);
|
|
80
|
+
|
|
81
|
+
expect(h.committed[0].value).toBe('two\none');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('indents the caret’s whole line, not the text at the caret', () => {
|
|
85
|
+
// ⌘] is a line command: on a bare caret it widens to that line first, which
|
|
86
|
+
// is what keeps it from inserting spaces mid-line.
|
|
87
|
+
const h = harness({ value: 'const a = 1;', start: 10 });
|
|
88
|
+
runTextCommand('indent', h.ctx);
|
|
89
|
+
|
|
90
|
+
expect(h.committed[0].value).toBe(' const a = 1;');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('indents every line of a selection', () => {
|
|
94
|
+
const h = harness({ value: 'one\ntwo', start: 0, end: 7 });
|
|
95
|
+
runTextCommand('indent', h.ctx);
|
|
96
|
+
|
|
97
|
+
expect(h.committed[0].value).toBe(' one\n two');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('outdenting a line that has no indent commits nothing', () => {
|
|
101
|
+
const h = harness({ value: 'one', start: 1 });
|
|
102
|
+
runTextCommand('outdent', h.ctx);
|
|
103
|
+
|
|
104
|
+
expect(h.committed).toEqual([]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('comments with the language’s own token', () => {
|
|
108
|
+
const h = harness({ value: 'key: value', start: 0, language: 'yaml' });
|
|
109
|
+
runTextCommand('toggleComment', h.ctx);
|
|
110
|
+
|
|
111
|
+
expect(h.committed[0].value).toBe('# key: value');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('caret moves collapse the selection', () => {
|
|
115
|
+
const h = harness({ value: 'one\ntwo', start: 5 });
|
|
116
|
+
runTextCommand('lineStart', h.ctx);
|
|
117
|
+
|
|
118
|
+
expect(h.carets).toEqual([4]);
|
|
119
|
+
expect(h.selections).toEqual([]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('shifted caret moves extend instead', () => {
|
|
123
|
+
const h = harness({ value: 'one\ntwo', start: 5, extend: true });
|
|
124
|
+
runTextCommand('lineEnd', h.ctx);
|
|
125
|
+
|
|
126
|
+
expect(h.selections).toEqual([[5, 7]]);
|
|
127
|
+
expect(h.carets).toEqual([]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('file edges go through the document-edge path', () => {
|
|
131
|
+
const h = harness({ value: 'one\ntwo', start: 5 });
|
|
132
|
+
runTextCommand('documentStart', h.ctx);
|
|
133
|
+
runTextCommand('documentEnd', h.ctx);
|
|
134
|
+
|
|
135
|
+
expect(h.edges).toEqual(['start', 'end']);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('a shifted document move selects instead of jumping', () => {
|
|
139
|
+
const h = harness({ value: 'one\ntwo', start: 5, extend: true });
|
|
140
|
+
runTextCommand('documentStart', h.ctx);
|
|
141
|
+
|
|
142
|
+
expect(h.selections).toEqual([[0, 5]]);
|
|
143
|
+
expect(h.edges).toEqual([]);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('the palette reaches the same commands with no event at all', () => {
|
|
147
|
+
// This is the path the command palette uses; a command reachable by chord
|
|
148
|
+
// must be reachable this way too.
|
|
149
|
+
const h = harness({ value: 'one\ntwo', start: 0, end: 3 });
|
|
150
|
+
runTextCommand('copyLineDown', h.ctx);
|
|
151
|
+
|
|
152
|
+
expect(h.committed[0].value).toBe('one\none\ntwo');
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('command coverage', () => {
|
|
157
|
+
test('every text command is one the editor keymap can produce', () => {
|
|
158
|
+
// A command in the table that no key or palette entry names would be dead
|
|
159
|
+
// code; this is the check that keeps the table honest.
|
|
160
|
+
const reachable: EditorCommand[] = [
|
|
161
|
+
'moveLineUp',
|
|
162
|
+
'moveLineDown',
|
|
163
|
+
'copyLineUp',
|
|
164
|
+
'copyLineDown',
|
|
165
|
+
'deleteLine',
|
|
166
|
+
'toggleComment',
|
|
167
|
+
'insertLineAbove',
|
|
168
|
+
'insertLineBelow',
|
|
169
|
+
'indent',
|
|
170
|
+
'outdent',
|
|
171
|
+
'lineStart',
|
|
172
|
+
'lineEnd',
|
|
173
|
+
'documentStart',
|
|
174
|
+
'documentEnd',
|
|
175
|
+
];
|
|
176
|
+
for (const command of reachable) expect(isTextCommand(command)).toBe(true);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The editor's text commands, as a dispatch table.
|
|
8
|
+
*
|
|
9
|
+
* Extracted from the component because it is dispatch rather than rendering,
|
|
10
|
+
* and because it keeps the component under the repository's line ceiling. The
|
|
11
|
+
* component still owns the keys whose behaviour is about the DOM node rather
|
|
12
|
+
* than the buffer — Escape, Tab, Backspace, Enter, undo/redo — and calls in
|
|
13
|
+
* here for everything that is pure string arithmetic.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
commentSyntaxFor,
|
|
18
|
+
deleteLines,
|
|
19
|
+
duplicateLines,
|
|
20
|
+
insertLine,
|
|
21
|
+
lineEdges,
|
|
22
|
+
moveLines,
|
|
23
|
+
toggleLineComment,
|
|
24
|
+
type TextRange,
|
|
25
|
+
} from '@/shared/lib/code/editor/commands';
|
|
26
|
+
import { editForTab } from '@/shared/lib/code/editor/edits';
|
|
27
|
+
import { lineRangeAt } from '@/shared/lib/code/editor/lines';
|
|
28
|
+
import type { EditorCommand } from '@/shared/lib/code/editor/keymap';
|
|
29
|
+
|
|
30
|
+
/** A whole-buffer edit plus where the caret lands. */
|
|
31
|
+
export interface CommittedEdit {
|
|
32
|
+
value: string;
|
|
33
|
+
selectionStart: number;
|
|
34
|
+
selectionEnd: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CommandContext {
|
|
38
|
+
/** Current buffer and selection, read from the live textarea. */
|
|
39
|
+
value: string;
|
|
40
|
+
selectionStart: number;
|
|
41
|
+
selectionEnd: number;
|
|
42
|
+
/** True while the event carried Shift (⌘↑ with Shift extends instead of moving). */
|
|
43
|
+
extend: boolean;
|
|
44
|
+
/** Shiki language id, which picks the comment syntax. */
|
|
45
|
+
language: string;
|
|
46
|
+
/** Commit a whole-buffer edit as ONE undoable step. */
|
|
47
|
+
commit: (edit: CommittedEdit) => void;
|
|
48
|
+
/** Move the caret, collapsing any selection. */
|
|
49
|
+
setCaret: (offset: number) => void;
|
|
50
|
+
/** Move the caret while keeping the selection's other end anchored. */
|
|
51
|
+
selectRange: (start: number, end: number) => void;
|
|
52
|
+
/** Jump to the buffer's first or last offset. */
|
|
53
|
+
setDocumentEdge: (edge: 'start' | 'end') => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Commands that edit or move within the buffer. Everything else (the find bar's
|
|
58
|
+
* chords, word wrap, save) belongs to the panel and is delegated by the caller.
|
|
59
|
+
*/
|
|
60
|
+
const TEXT_COMMANDS: Record<string, true> = {
|
|
61
|
+
moveLineUp: true,
|
|
62
|
+
moveLineDown: true,
|
|
63
|
+
copyLineUp: true,
|
|
64
|
+
copyLineDown: true,
|
|
65
|
+
deleteLine: true,
|
|
66
|
+
toggleComment: true,
|
|
67
|
+
insertLineAbove: true,
|
|
68
|
+
insertLineBelow: true,
|
|
69
|
+
indent: true,
|
|
70
|
+
outdent: true,
|
|
71
|
+
lineStart: true,
|
|
72
|
+
lineEnd: true,
|
|
73
|
+
documentStart: true,
|
|
74
|
+
documentEnd: true,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export function isTextCommand(command: EditorCommand): boolean {
|
|
78
|
+
return TEXT_COMMANDS[command] === true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Run one text command. Returns true when the key was consumed.
|
|
83
|
+
*
|
|
84
|
+
* The buffer-changing branches all commit through `ctx.commit`, which is the
|
|
85
|
+
* component's single write path: one undo entry per command, and the highlight
|
|
86
|
+
* layer follows because the commit goes through the controlled value.
|
|
87
|
+
*/
|
|
88
|
+
export function runTextCommand(command: EditorCommand, ctx: CommandContext): boolean {
|
|
89
|
+
const selection: TextRange = { start: ctx.selectionStart, end: ctx.selectionEnd };
|
|
90
|
+
|
|
91
|
+
switch (command) {
|
|
92
|
+
case 'moveLineUp':
|
|
93
|
+
ctx.commit(moveLines(ctx.value, selection, -1));
|
|
94
|
+
return true;
|
|
95
|
+
case 'moveLineDown':
|
|
96
|
+
ctx.commit(moveLines(ctx.value, selection, 1));
|
|
97
|
+
return true;
|
|
98
|
+
case 'copyLineUp':
|
|
99
|
+
ctx.commit(duplicateLines(ctx.value, selection, -1));
|
|
100
|
+
return true;
|
|
101
|
+
case 'copyLineDown':
|
|
102
|
+
ctx.commit(duplicateLines(ctx.value, selection, 1));
|
|
103
|
+
return true;
|
|
104
|
+
case 'deleteLine':
|
|
105
|
+
ctx.commit(deleteLines(ctx.value, selection));
|
|
106
|
+
return true;
|
|
107
|
+
case 'toggleComment':
|
|
108
|
+
ctx.commit(toggleLineComment(ctx.value, selection, commentSyntaxFor(ctx.language)));
|
|
109
|
+
return true;
|
|
110
|
+
case 'insertLineAbove':
|
|
111
|
+
ctx.commit(insertLine(ctx.value, ctx.selectionStart, 'above'));
|
|
112
|
+
return true;
|
|
113
|
+
case 'insertLineBelow':
|
|
114
|
+
ctx.commit(insertLine(ctx.value, ctx.selectionStart, 'below'));
|
|
115
|
+
return true;
|
|
116
|
+
|
|
117
|
+
case 'indent':
|
|
118
|
+
case 'outdent': {
|
|
119
|
+
// ⌘] / ⌘[ act on the LINES the selection touches — on a bare caret that
|
|
120
|
+
// is the caret's own line, which is why the range is widened first. Tab
|
|
121
|
+
// itself still indents at the caret (that is what Tab means in an
|
|
122
|
+
// editor), but a line command that shifted text mid-line would not be
|
|
123
|
+
// indenting anything.
|
|
124
|
+
const range = ctx.selectionStart === ctx.selectionEnd ? lineRangeAt(ctx.value, ctx.selectionStart) : selection;
|
|
125
|
+
const edit = editForTab(ctx.value, range.start, range.end, command === 'outdent');
|
|
126
|
+
if (edit.value !== ctx.value) ctx.commit(edit);
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
case 'lineStart': {
|
|
131
|
+
const edges = lineEdges(ctx.value, ctx.selectionStart);
|
|
132
|
+
if (ctx.extend) ctx.selectRange(edges.start, ctx.selectionEnd);
|
|
133
|
+
else ctx.setCaret(edges.start);
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
case 'lineEnd': {
|
|
137
|
+
const edges = lineEdges(ctx.value, ctx.selectionStart);
|
|
138
|
+
if (ctx.extend) ctx.selectRange(ctx.selectionStart, edges.end);
|
|
139
|
+
else ctx.setCaret(edges.end);
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
case 'documentStart':
|
|
143
|
+
if (ctx.extend) ctx.selectRange(0, ctx.selectionEnd);
|
|
144
|
+
else ctx.setDocumentEdge('start');
|
|
145
|
+
return true;
|
|
146
|
+
case 'documentEnd':
|
|
147
|
+
if (ctx.extend) ctx.selectRange(ctx.selectionStart, ctx.value.length);
|
|
148
|
+
else ctx.setDocumentEdge('end');
|
|
149
|
+
return true;
|
|
150
|
+
|
|
151
|
+
default:
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { beforeAll, describe, expect, test } from 'bun:test';
|
|
7
7
|
|
|
8
8
|
import { bootSyntax, onLanguageReady, requestLanguage } from '@/shared/lib/code/highlighter';
|
|
9
|
+
import { findMatches } from '@/shared/lib/code/editor/find';
|
|
9
10
|
import { getLanguageFromPath, highlightCode, highlightLines } from '@/shared/lib/code/syntax-highlight';
|
|
10
11
|
import { highlightCodeWindow } from '@/shared/lib/code/windowed-highlight';
|
|
11
12
|
|
|
@@ -161,6 +162,80 @@ describe('highlightCodeWindow', () => {
|
|
|
161
162
|
});
|
|
162
163
|
});
|
|
163
164
|
|
|
165
|
+
describe('find marks', () => {
|
|
166
|
+
const code = 'const alpha = 1;\nconst beta = alpha + 2;\n';
|
|
167
|
+
const PLAIN = { matchCase: false, wholeWord: false, isRegex: false };
|
|
168
|
+
/** The text of every mark, in order. A match spanning tokens is several
|
|
169
|
+
* fragments, so the assertion is on the fragments JOINED. */
|
|
170
|
+
const marked = (html: string): string[] =>
|
|
171
|
+
Array.from(html.matchAll(/<mark class="find-match"[^>]*>(.*?)<\/mark>/g), (match) => match[1]);
|
|
172
|
+
|
|
173
|
+
test('splits a match across the tokens it spans, and marks every fragment current', () => {
|
|
174
|
+
// `const alpha` is three tokens (`const`, ` `, `alpha`): each carries its
|
|
175
|
+
// own fragment of the one match, which is why the CSS may not draw a
|
|
176
|
+
// vertical edge per fragment.
|
|
177
|
+
const line = highlightLines(code, 'typescript', { marks: [{ start: 0, end: 11 }], currentMark: 0 })[0];
|
|
178
|
+
|
|
179
|
+
expect(marked(line).join('')).toBe('const alpha');
|
|
180
|
+
expect(line.match(/data-find-current/g)?.length).toBe(3);
|
|
181
|
+
expect(line.match(/<\/mark>/g)?.length).toBe(3);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('marks every occurrence, and only the current one as current', () => {
|
|
185
|
+
const marks = findMatches(code, 'alpha', PLAIN).matches;
|
|
186
|
+
const lines = highlightLines(code, 'typescript', { marks, currentMark: 1 });
|
|
187
|
+
|
|
188
|
+
expect(marked(lines[0]).join('')).toBe('alpha');
|
|
189
|
+
expect(marked(lines[1]).join('')).toBe('alpha');
|
|
190
|
+
expect(lines[0]).not.toContain('data-find-current');
|
|
191
|
+
expect(lines[1]).toContain('data-find-current');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('escapes the matched text it wraps', () => {
|
|
195
|
+
const line = highlightLines('a < b', 'text', { marks: [{ start: 2, end: 3 }], currentMark: 0 })[0];
|
|
196
|
+
|
|
197
|
+
expect(line).toContain('<mark class="find-match" data-find-current=""><</mark>');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('leaves the markup untouched when there are no marks', () => {
|
|
201
|
+
const plain = highlightLines(code, 'typescript')[0];
|
|
202
|
+
const empty = highlightLines(code, 'typescript', { marks: [], currentMark: -1 })[0];
|
|
203
|
+
|
|
204
|
+
expect(empty).toBe(plain);
|
|
205
|
+
expect(plain).not.toContain('<mark');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('keeps a windowed match on its own line, re-based to the slice', () => {
|
|
209
|
+
const marks = findMatches(code, 'alpha', PLAIN).matches;
|
|
210
|
+
// The window renders line 1 only. The match on line 0 must not reappear on
|
|
211
|
+
// line 1's text, and the current index (line 0's match) must not mark
|
|
212
|
+
// anything here.
|
|
213
|
+
const window = highlightCodeWindow(code, 'typescript', { start: 1, end: 2 }, { marks, currentMark: 0 });
|
|
214
|
+
|
|
215
|
+
expect(marked(window).join('')).toBe('alpha');
|
|
216
|
+
expect(window).not.toContain('data-find-current');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('re-bases the current index onto the marks inside the window', () => {
|
|
220
|
+
const marks = findMatches(code, 'alpha', PLAIN).matches;
|
|
221
|
+
const window = highlightCodeWindow(code, 'typescript', { start: 1, end: 2 }, { marks, currentMark: 1 });
|
|
222
|
+
|
|
223
|
+
expect(window.match(/data-find-current/g)?.length).toBe(1);
|
|
224
|
+
expect(marked(window).join('')).toBe('alpha');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('a mark reaching past the window is clipped to it', () => {
|
|
228
|
+
// The last line has no trailing newline, so a mark that runs to the end of
|
|
229
|
+
// the slice would be clamped by the rebase.
|
|
230
|
+
const window = highlightCodeWindow(code, 'typescript', { start: 1, end: 3 }, {
|
|
231
|
+
marks: [{ start: 30, end: 999 }],
|
|
232
|
+
currentMark: 0,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
expect(marked(window).join('')).toBe('alpha + 2;');
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
164
239
|
describe('getLanguageFromPath', () => {
|
|
165
240
|
test.each([
|
|
166
241
|
['a.ts', 'typescript'],
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import type { HighlighterCore } from 'shiki/core';
|
|
2
2
|
|
|
3
3
|
import { escapeCode, getHighlighterSync, requestLanguage } from '@/shared/lib/code/highlighter';
|
|
4
|
+
import { tokenMarkup, type HighlightLinesOptions } from '@/shared/lib/code/editor/marks';
|
|
5
|
+
import { lineStartOffset } from '@/shared/lib/code/editor/lines';
|
|
4
6
|
import { SHIKI_THEMES } from '@/shared/lib/code/shiki-themes';
|
|
5
7
|
|
|
8
|
+
export type { HighlightLinesOptions };
|
|
9
|
+
|
|
6
10
|
/** Map file extension to Shiki language id */
|
|
7
11
|
export function getLanguageFromPath(filePath?: string): string {
|
|
8
12
|
if (!filePath) return 'javascript';
|
|
@@ -139,8 +143,13 @@ function resolveReadyLang(hl: HighlighterCore, language: string): string {
|
|
|
139
143
|
}
|
|
140
144
|
|
|
141
145
|
/** Syntax highlight code string safely with fallback */
|
|
142
|
-
export function highlightCode(code: string, language = 'javascript'): string {
|
|
146
|
+
export function highlightCode(code: string, language = 'javascript', options: HighlightLinesOptions = {}): string {
|
|
143
147
|
if (!code) return '';
|
|
148
|
+
// A find in progress needs the match markup, which only the line-based path
|
|
149
|
+
// emits; without it the fast `codeToHtml` path is left exactly as it was.
|
|
150
|
+
if (options.marks && options.marks.length > 0) {
|
|
151
|
+
return `<span class="shiki">${highlightLines(code, language, options).join('\n')}</span>`;
|
|
152
|
+
}
|
|
144
153
|
const hl = getHighlighterSync();
|
|
145
154
|
if (!hl) return escapeCode(code);
|
|
146
155
|
const lang = resolveReadyLang(hl, language);
|
|
@@ -268,14 +277,6 @@ export function highlightJson(code: string): string {
|
|
|
268
277
|
return highlightCode(code, 'json');
|
|
269
278
|
}
|
|
270
279
|
|
|
271
|
-
export interface HighlightLinesOptions {
|
|
272
|
-
/**
|
|
273
|
-
* Lines at or above this length are emitted as plain text instead of being
|
|
274
|
-
* tokenized — the guard a minified bundle (one line of hundreds of KB) needs.
|
|
275
|
-
*/
|
|
276
|
-
maxLineLength?: number;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
280
|
/**
|
|
280
281
|
* Highlight code and return one HTML fragment per line, aligned 1:1 with
|
|
281
282
|
* `code.split('\n')`. Tokenizes the whole input at once so multi-line grammar
|
|
@@ -287,9 +288,18 @@ export function highlightLines(
|
|
|
287
288
|
options: HighlightLinesOptions = {},
|
|
288
289
|
): string[] {
|
|
289
290
|
const rawLines = code.split('\n');
|
|
291
|
+
const marks = options.marks;
|
|
292
|
+
const currentMark = options.currentMark ?? -1;
|
|
293
|
+
const occurrenceFrom = options.occurrenceFrom ?? -1;
|
|
294
|
+
// A grammar that has not arrived yet, or a highlighter that failed to boot,
|
|
295
|
+
// still gets the find marks: the widget's feedback must not depend on Shiki.
|
|
296
|
+
const plain = (lineIndex: number): string => {
|
|
297
|
+
if (!marks || marks.length === 0) return escapeCode(rawLines[lineIndex]);
|
|
298
|
+
return tokenMarkup(rawLines[lineIndex], lineStartOffset(code, lineIndex), marks, currentMark, occurrenceFrom);
|
|
299
|
+
};
|
|
290
300
|
if (!code) return rawLines;
|
|
291
301
|
const hl = getHighlighterSync();
|
|
292
|
-
if (!hl) return rawLines.map(
|
|
302
|
+
if (!hl) return rawLines.map((_line, index) => plain(index));
|
|
293
303
|
const lang = resolveReadyLang(hl, language);
|
|
294
304
|
try {
|
|
295
305
|
const { tokens } = hl.codeToTokens(code, {
|
|
@@ -298,20 +308,22 @@ export function highlightLines(
|
|
|
298
308
|
defaultColor: false,
|
|
299
309
|
tokenizeMaxLineLength: options.maxLineLength ?? 0,
|
|
300
310
|
});
|
|
301
|
-
if (tokens.length !== rawLines.length) return rawLines.map(
|
|
302
|
-
return tokens.map((lineTokens) =>
|
|
303
|
-
|
|
311
|
+
if (tokens.length !== rawLines.length) return rawLines.map((_line, index) => plain(index));
|
|
312
|
+
return tokens.map((lineTokens, lineIndex) => {
|
|
313
|
+
let offset = lineStartOffset(code, lineIndex);
|
|
314
|
+
return lineTokens
|
|
304
315
|
.map((tok) => {
|
|
305
|
-
const content =
|
|
316
|
+
const content = tokenMarkup(tok.content, offset, marks, currentMark, occurrenceFrom);
|
|
317
|
+
offset += tok.content.length;
|
|
306
318
|
if (!tok.htmlStyle) return content;
|
|
307
319
|
const style = Object.entries(tok.htmlStyle)
|
|
308
320
|
.map(([key, value]) => `${key}:${value}`)
|
|
309
321
|
.join(';');
|
|
310
322
|
return style ? `<span style="${style}">${content}</span>` : content;
|
|
311
323
|
})
|
|
312
|
-
.join('')
|
|
313
|
-
);
|
|
324
|
+
.join('');
|
|
325
|
+
});
|
|
314
326
|
} catch {
|
|
315
|
-
return rawLines.map(
|
|
327
|
+
return rawLines.map((_line, index) => plain(index));
|
|
316
328
|
}
|
|
317
329
|
}
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { LineWindow } from '@/shared/lib/code/lazy-window';
|
|
7
|
+
import type { FindMatch } from '@/shared/lib/code/editor/find';
|
|
8
|
+
import { lineStartOffset } from '@/shared/lib/code/editor/lines';
|
|
7
9
|
import { highlightLines } from '@/shared/lib/code/syntax-highlight';
|
|
8
10
|
|
|
9
11
|
/**
|
|
@@ -28,6 +30,15 @@ export interface HighlightCodeWindowOptions {
|
|
|
28
30
|
contextLines?: number;
|
|
29
31
|
/** Length at which a line falls back to plain text. */
|
|
30
32
|
maxLineLength?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Find matches in DOCUMENT offsets. Only those inside the rendered window are
|
|
35
|
+
* passed on, re-based to the sliced source the tokenizer sees.
|
|
36
|
+
*/
|
|
37
|
+
marks?: readonly FindMatch[];
|
|
38
|
+
/** Index of the current match within `marks`. */
|
|
39
|
+
currentMark?: number;
|
|
40
|
+
/** Marks from this index on are extra ⌘D selections rather than find matches. */
|
|
41
|
+
occurrenceFrom?: number;
|
|
31
42
|
}
|
|
32
43
|
|
|
33
44
|
/**
|
|
@@ -57,7 +68,47 @@ export function highlightCodeWindow(
|
|
|
57
68
|
if (start === end) return '';
|
|
58
69
|
|
|
59
70
|
const contextStart = Math.max(0, start - contextLines);
|
|
60
|
-
const
|
|
71
|
+
const slice = lines.slice(contextStart, end).join('\n');
|
|
72
|
+
const base = lineStartOffset(code, contextStart);
|
|
73
|
+
const sliceEnd = base + slice.length;
|
|
74
|
+
const { marks, currentMark, occurrenceFrom } = rebaseMarks(
|
|
75
|
+
options.marks,
|
|
76
|
+
options.currentMark ?? -1,
|
|
77
|
+
options.occurrenceFrom ?? -1,
|
|
78
|
+
base,
|
|
79
|
+
sliceEnd,
|
|
80
|
+
);
|
|
81
|
+
const rows = highlightLines(slice, language, { maxLineLength, marks, currentMark, occurrenceFrom });
|
|
61
82
|
const offset = start - contextStart;
|
|
62
83
|
return `<span class="shiki">${rows.slice(offset, offset + (end - start)).join('\n')}</span>`;
|
|
63
84
|
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The marks inside `[base, sliceEnd)`, shifted to slice coordinates, with the
|
|
88
|
+
* current-match index remapped — the index points into the full list, so the
|
|
89
|
+
* rows below the window cannot simply reuse it.
|
|
90
|
+
*/
|
|
91
|
+
function rebaseMarks(
|
|
92
|
+
marks: readonly FindMatch[] | undefined,
|
|
93
|
+
currentMark: number,
|
|
94
|
+
occurrenceFrom: number,
|
|
95
|
+
base: number,
|
|
96
|
+
sliceEnd: number,
|
|
97
|
+
): { marks: FindMatch[]; currentMark: number; occurrenceFrom: number } {
|
|
98
|
+
if (!marks || marks.length === 0) return { marks: [], currentMark: -1, occurrenceFrom: -1 };
|
|
99
|
+
const kept: FindMatch[] = [];
|
|
100
|
+
let remapped = -1;
|
|
101
|
+
let remappedOccurrenceFrom = -1;
|
|
102
|
+
for (let index = 0; index < marks.length; index++) {
|
|
103
|
+
const mark = marks[index];
|
|
104
|
+
if (mark.end <= base) continue;
|
|
105
|
+
if (mark.start >= sliceEnd) break;
|
|
106
|
+
if (index === currentMark) remapped = kept.length;
|
|
107
|
+
// The first surviving occurrence keeps the split, wherever it landed: a
|
|
108
|
+
// window whose opening lines held only matches still has to paint the
|
|
109
|
+
// selections below them differently.
|
|
110
|
+
if (remappedOccurrenceFrom === -1 && occurrenceFrom >= 0 && index >= occurrenceFrom) remappedOccurrenceFrom = kept.length;
|
|
111
|
+
kept.push({ start: mark.start - base, end: Math.min(mark.end, sliceEnd) - base });
|
|
112
|
+
}
|
|
113
|
+
return { marks: kept, currentMark: remapped, occurrenceFrom: remappedOccurrenceFrom };
|
|
114
|
+
}
|