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,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The code editor's keymap, as data.
|
|
8
|
+
*
|
|
9
|
+
* One table is the whole truth about an editor shortcut: what it does, which
|
|
10
|
+
* chords reach it on each platform, and what the palette calls it. The
|
|
11
|
+
* textarea's handler, the find bar's buttons, the tooltips and the command
|
|
12
|
+
* palette all read this table, so a shortcut cannot be listed in the palette
|
|
13
|
+
* and unbound in the editor.
|
|
14
|
+
*
|
|
15
|
+
* VS Code's defaults are followed wherever this editor has an equivalent. Where
|
|
16
|
+
* it has none — folding, go-to-definition, the `⌘K` prefix chords — the command
|
|
17
|
+
* is absent rather than bound to something that would surprise.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { KeyBinding, KeyStroke } from '@/shared/lib/ui/key-binding';
|
|
21
|
+
|
|
22
|
+
export type EditorCommand =
|
|
23
|
+
| 'find'
|
|
24
|
+
| 'replace'
|
|
25
|
+
| 'nextMatch'
|
|
26
|
+
| 'previousMatch'
|
|
27
|
+
| 'replaceOne'
|
|
28
|
+
| 'replaceAll'
|
|
29
|
+
| 'toggleCaseSensitive'
|
|
30
|
+
| 'toggleWholeWord'
|
|
31
|
+
| 'toggleRegex'
|
|
32
|
+
| 'toggleWordWrap'
|
|
33
|
+
| 'selectNextOccurrence'
|
|
34
|
+
| 'selectAllOccurrences'
|
|
35
|
+
| 'insertCursorBelow'
|
|
36
|
+
| 'moveLineUp'
|
|
37
|
+
| 'moveLineDown'
|
|
38
|
+
| 'copyLineUp'
|
|
39
|
+
| 'copyLineDown'
|
|
40
|
+
| 'deleteLine'
|
|
41
|
+
| 'toggleComment'
|
|
42
|
+
| 'insertLineBelow'
|
|
43
|
+
| 'insertLineAbove'
|
|
44
|
+
| 'indent'
|
|
45
|
+
| 'outdent'
|
|
46
|
+
| 'undo'
|
|
47
|
+
| 'redo'
|
|
48
|
+
| 'lineStart'
|
|
49
|
+
| 'lineEnd'
|
|
50
|
+
| 'documentStart'
|
|
51
|
+
| 'documentEnd'
|
|
52
|
+
| 'selectToLineStart'
|
|
53
|
+
| 'selectToLineEnd'
|
|
54
|
+
| 'selectToDocumentStart'
|
|
55
|
+
| 'selectToDocumentEnd'
|
|
56
|
+
| 'selectWord'
|
|
57
|
+
| 'selectLine'
|
|
58
|
+
| 'escape';
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The editor keymap.
|
|
62
|
+
*
|
|
63
|
+
* Every entry carries both platforms even when the chords are identical,
|
|
64
|
+
* because the label is DERIVED from the stroke rather than written twice — a
|
|
65
|
+
* hand-written "Ctrl+F" beside a binding that listens for ⌘F is exactly the
|
|
66
|
+
* drift this table exists to prevent.
|
|
67
|
+
*/
|
|
68
|
+
export const EDITOR_KEY_BINDINGS: readonly KeyBinding<EditorCommand>[] = [
|
|
69
|
+
{ command: 'find', label: 'Find', mac: { code: 'KeyF', meta: true }, other: { code: 'KeyF', ctrl: true } },
|
|
70
|
+
{ command: 'replace', label: 'Replace', mac: { code: 'KeyF', meta: true, alt: true }, other: { code: 'KeyH', ctrl: true } },
|
|
71
|
+
{ command: 'nextMatch', label: 'Next match', mac: { code: 'F3' }, other: { code: 'F3' } },
|
|
72
|
+
{ command: 'previousMatch', label: 'Previous match', mac: { code: 'F3', shift: true }, other: { code: 'F3', shift: true } },
|
|
73
|
+
{ command: 'replaceOne', label: 'Replace current match', mac: { code: 'Digit1', meta: true, shift: true }, other: { code: 'Digit1', ctrl: true, shift: true } },
|
|
74
|
+
{ command: 'replaceAll', label: 'Replace all matches', mac: { code: 'Enter', meta: true, alt: true }, other: { code: 'Enter', ctrl: true, alt: true } },
|
|
75
|
+
{ command: 'toggleCaseSensitive', label: 'Match case', mac: { code: 'KeyC', meta: true, alt: true }, other: { code: 'KeyC', alt: true } },
|
|
76
|
+
{ command: 'toggleWholeWord', label: 'Match whole word', mac: { code: 'KeyW', meta: true, alt: true }, other: { code: 'KeyW', alt: true } },
|
|
77
|
+
{ command: 'toggleRegex', label: 'Use regular expression', mac: { code: 'KeyR', meta: true, alt: true }, other: { code: 'KeyR', alt: true } },
|
|
78
|
+
{ command: 'toggleWordWrap', label: 'Toggle word wrap', mac: { code: 'KeyZ', alt: true }, other: { code: 'KeyZ', alt: true } },
|
|
79
|
+
|
|
80
|
+
{ command: 'selectNextOccurrence', label: 'Add selection to next find match', mac: { code: 'KeyD', meta: true }, other: { code: 'KeyD', ctrl: true } },
|
|
81
|
+
{ command: 'selectAllOccurrences', label: 'Select all occurrences of current selection', mac: { code: 'KeyL', meta: true, shift: true }, other: { code: 'KeyL', ctrl: true, shift: true } },
|
|
82
|
+
{ command: 'insertCursorBelow', label: 'Insert cursor below', mac: { code: 'KeyD', meta: true, shift: true }, other: { code: 'KeyD', ctrl: true, shift: true } },
|
|
83
|
+
|
|
84
|
+
{ command: 'moveLineUp', label: 'Move line up', mac: { code: 'ArrowUp', alt: true }, other: { code: 'ArrowUp', alt: true } },
|
|
85
|
+
{ command: 'moveLineDown', label: 'Move line down', mac: { code: 'ArrowDown', alt: true }, other: { code: 'ArrowDown', alt: true } },
|
|
86
|
+
{ command: 'copyLineUp', label: 'Copy line up', mac: { code: 'ArrowUp', meta: true, alt: true, shift: true }, other: { code: 'ArrowUp', ctrl: true, alt: true, shift: true } },
|
|
87
|
+
{ command: 'copyLineDown', label: 'Copy line down', mac: { code: 'ArrowDown', meta: true, alt: true, shift: true }, other: { code: 'ArrowDown', ctrl: true, alt: true, shift: true } },
|
|
88
|
+
// `⇧⌘K` on macOS; VS Code's Windows/Linux chord is the two-stroke `Ctrl+K Ctrl+K`,
|
|
89
|
+
// which a single-stroke table cannot express, so the mac chord is used there too.
|
|
90
|
+
{ command: 'deleteLine', label: 'Delete line', mac: { code: 'KeyK', meta: true, shift: true }, other: { code: 'KeyK', ctrl: true, shift: true } },
|
|
91
|
+
// `Slash` is a physical key: on a US layout that is `/`, elsewhere it is
|
|
92
|
+
// wherever the user's own layout puts it.
|
|
93
|
+
{ command: 'toggleComment', label: 'Toggle line comment', mac: { code: 'Slash', meta: true }, other: { code: 'Slash', ctrl: true } },
|
|
94
|
+
|
|
95
|
+
{ command: 'insertLineBelow', label: 'Insert line below', mac: { code: 'Enter', meta: true }, other: { code: 'Enter', ctrl: true } },
|
|
96
|
+
{ command: 'insertLineAbove', label: 'Insert line above', mac: { code: 'Enter', meta: true, shift: true }, other: { code: 'Enter', ctrl: true, shift: true } },
|
|
97
|
+
|
|
98
|
+
{ command: 'indent', label: 'Indent line', mac: { code: 'BracketRight', meta: true }, other: { code: 'BracketRight', ctrl: true } },
|
|
99
|
+
{ command: 'outdent', label: 'Outdent line', mac: { code: 'BracketLeft', meta: true }, other: { code: 'BracketLeft', ctrl: true } },
|
|
100
|
+
|
|
101
|
+
{ command: 'undo', label: 'Undo', mac: { code: 'KeyZ', meta: true }, other: { code: 'KeyZ', ctrl: true } },
|
|
102
|
+
{
|
|
103
|
+
command: 'redo',
|
|
104
|
+
label: 'Redo',
|
|
105
|
+
mac: { code: 'KeyZ', meta: true, shift: true },
|
|
106
|
+
other: { code: 'KeyZ', ctrl: true, shift: true },
|
|
107
|
+
windows: { code: 'KeyY', ctrl: true },
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
{ command: 'lineStart', label: 'Go to line start', mac: { code: 'ArrowLeft', meta: true }, other: { code: 'Home' } },
|
|
111
|
+
{ command: 'lineEnd', label: 'Go to line end', mac: { code: 'ArrowRight', meta: true }, other: { code: 'End' } },
|
|
112
|
+
{ command: 'documentStart', label: 'Go to file start', mac: { code: 'ArrowUp', meta: true }, other: { code: 'ArrowUp', ctrl: true } },
|
|
113
|
+
{ command: 'documentEnd', label: 'Go to file end', mac: { code: 'ArrowDown', meta: true }, other: { code: 'ArrowDown', ctrl: true } },
|
|
114
|
+
{ command: 'selectToLineStart', label: 'Select to line start', mac: { code: 'ArrowLeft', meta: true, shift: true }, other: { code: 'ArrowLeft', ctrl: true, shift: true } },
|
|
115
|
+
{ command: 'selectToLineEnd', label: 'Select to line end', mac: { code: 'ArrowRight', meta: true, shift: true }, other: { code: 'ArrowRight', ctrl: true, shift: true } },
|
|
116
|
+
{ command: 'selectToDocumentStart', label: 'Select to file start', mac: { code: 'ArrowUp', meta: true, shift: true }, other: { code: 'ArrowUp', ctrl: true, shift: true } },
|
|
117
|
+
{ command: 'selectToDocumentEnd', label: 'Select to file end', mac: { code: 'ArrowDown', meta: true, shift: true }, other: { code: 'ArrowDown', ctrl: true, shift: true } },
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Commands the FIND BAR handles: the editor panel dispatches these to the find
|
|
122
|
+
* hook rather than running them against the buffer itself.
|
|
123
|
+
*/
|
|
124
|
+
export const FIND_BAR_COMMANDS: readonly EditorCommand[] = [
|
|
125
|
+
'find',
|
|
126
|
+
'replace',
|
|
127
|
+
'nextMatch',
|
|
128
|
+
'previousMatch',
|
|
129
|
+
'replaceOne',
|
|
130
|
+
'replaceAll',
|
|
131
|
+
'toggleCaseSensitive',
|
|
132
|
+
'toggleWholeWord',
|
|
133
|
+
'toggleRegex',
|
|
134
|
+
'toggleWordWrap',
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
/** Membership test for a call site that has a command and wants a yes/no. */
|
|
138
|
+
const FIND_BAR_SET: Record<string, true> = Object.fromEntries(FIND_BAR_COMMANDS.map((command) => [command, true]));
|
|
139
|
+
|
|
140
|
+
export function isFindBarCommand(command: EditorCommand): boolean {
|
|
141
|
+
return FIND_BAR_SET[command] === true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The chord for a command on the current platform. */
|
|
145
|
+
export function editorStrokeFor(command: EditorCommand, isMac: boolean, isWindows: boolean): KeyStroke | null {
|
|
146
|
+
const binding = EDITOR_KEY_BINDINGS.find((entry) => entry.command === command);
|
|
147
|
+
if (!binding) return null;
|
|
148
|
+
if (isMac) return binding.mac;
|
|
149
|
+
if (binding.windows && isWindows) return binding.windows;
|
|
150
|
+
return binding.other;
|
|
151
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The two-layer alignment contract.
|
|
8
|
+
*
|
|
9
|
+
* The transparent `<textarea>` sits exactly on top of the highlighted `<pre>`,
|
|
10
|
+
* so any typographic property that differs between them puts the caret on a
|
|
11
|
+
* different row than the glyphs. These assertions pin the two things that
|
|
12
|
+
* matter: both layers inherit their typography (a second copy of the font stack
|
|
13
|
+
* is what makes them drift), and the `<pre>` reserves the unrendered lines as
|
|
14
|
+
* padding while the textarea keeps the plain inset.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { describe, expect, test } from 'bun:test';
|
|
18
|
+
|
|
19
|
+
import { layerStyle, layerStyles } from '@/shared/lib/code/editor/layers';
|
|
20
|
+
|
|
21
|
+
describe('layerStyle', () => {
|
|
22
|
+
test('inherits every typographic property rather than restating it', () => {
|
|
23
|
+
const style = layerStyle();
|
|
24
|
+
|
|
25
|
+
for (const property of ['fontFamily', 'fontSize', 'lineHeight', 'letterSpacing', 'tabSize'] as const) {
|
|
26
|
+
expect(style[property]).toBe('inherit');
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('breaks words the same way the gutter measured them', () => {
|
|
31
|
+
// The measurement mirror copies these from the editor's computed style; a
|
|
32
|
+
// layer that broke differently would put the numbers on the wrong rows.
|
|
33
|
+
const style = layerStyle();
|
|
34
|
+
|
|
35
|
+
expect(style.whiteSpace).toBe('pre-wrap');
|
|
36
|
+
expect(style.overflowWrap).toBe('break-word');
|
|
37
|
+
expect(style.wordBreak).toBe('keep-all');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('takes its box from the caller, not from the UA', () => {
|
|
41
|
+
const style = layerStyle();
|
|
42
|
+
|
|
43
|
+
expect(style.boxSizing).toBe('inherit');
|
|
44
|
+
expect(style.display).toBe('inherit');
|
|
45
|
+
expect(style.margin).toBe(0);
|
|
46
|
+
expect(style.border).toBe(0);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('layerStyles', () => {
|
|
51
|
+
test('both layers carry the caller’s padding', () => {
|
|
52
|
+
const { pre, textarea } = layerStyles(16, null);
|
|
53
|
+
|
|
54
|
+
expect(textarea.paddingTop).toBe(16);
|
|
55
|
+
expect(textarea.paddingLeft).toBe(16);
|
|
56
|
+
expect(pre.paddingTop).toBe(16);
|
|
57
|
+
expect(pre.paddingBottom).toBe(16);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('the pre reserves the unrendered lines as padding', () => {
|
|
61
|
+
// Windowing is the whole trick: the textarea holds the document, the pre
|
|
62
|
+
// holds a slice of it, and the space of everything else is this padding.
|
|
63
|
+
const { pre, textarea } = layerStyles(8, { start: 100, end: 120, top: 900, bottom: 200, breakAtEnd: false });
|
|
64
|
+
|
|
65
|
+
expect(pre.paddingTop).toBe(908);
|
|
66
|
+
expect(pre.paddingBottom).toBe(208);
|
|
67
|
+
expect(textarea.paddingTop).toBe(8);
|
|
68
|
+
expect(textarea.paddingBottom).toBe(8);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('a window without space leaves the plain inset', () => {
|
|
72
|
+
const { pre } = layerStyles(4, { start: 0, end: 10, top: 0, bottom: 0, breakAtEnd: true });
|
|
73
|
+
|
|
74
|
+
expect(pre.paddingTop).toBe(4);
|
|
75
|
+
expect(pre.paddingBottom).toBe(4);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The inline styles that keep the editor's two layers in register.
|
|
8
|
+
*
|
|
9
|
+
* The transparent `<textarea>` (which owns the caret, the selection and every
|
|
10
|
+
* keystroke) sits exactly on top of the `<pre>` (which paints the glyphs), so
|
|
11
|
+
* both must lay text out identically: `layerStyles` is the shared half —
|
|
12
|
+
* every typographic property inherits rather than being restated, because a
|
|
13
|
+
* second copy of the font stack is what makes the two drift.
|
|
14
|
+
*
|
|
15
|
+
* The `<pre>` additionally carries the space of the lines the window is not
|
|
16
|
+
* rendering, as padding, which is the whole trick behind the lazy surface: the
|
|
17
|
+
* textarea still holds every line, so the caret lands where the reader expects
|
|
18
|
+
* without the highlighter ever tokenizing the rest of the document.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { CSSProperties } from 'preact';
|
|
22
|
+
import type { CodeWindow } from '@/shared/lib/code/lazy-window';
|
|
23
|
+
|
|
24
|
+
/** The shared typography, inherited from the caller's own class. */
|
|
25
|
+
export function layerStyle(): CSSProperties {
|
|
26
|
+
return {
|
|
27
|
+
margin: 0,
|
|
28
|
+
border: 0,
|
|
29
|
+
background: 'none',
|
|
30
|
+
boxSizing: 'inherit',
|
|
31
|
+
display: 'inherit',
|
|
32
|
+
fontFamily: 'inherit',
|
|
33
|
+
fontSize: 'inherit',
|
|
34
|
+
fontStyle: 'inherit',
|
|
35
|
+
fontVariantLigatures: 'inherit',
|
|
36
|
+
fontWeight: 'inherit',
|
|
37
|
+
letterSpacing: 'inherit',
|
|
38
|
+
lineHeight: 'inherit',
|
|
39
|
+
tabSize: 'inherit',
|
|
40
|
+
textIndent: 'inherit',
|
|
41
|
+
textRendering: 'inherit',
|
|
42
|
+
textTransform: 'inherit',
|
|
43
|
+
whiteSpace: 'pre-wrap',
|
|
44
|
+
wordBreak: 'keep-all',
|
|
45
|
+
overflowWrap: 'break-word',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The plain inset both layers take; `padding` is the editor's own padding. */
|
|
50
|
+
function contentStyle(padding: number): CSSProperties {
|
|
51
|
+
return { paddingTop: padding, paddingRight: padding, paddingBottom: padding, paddingLeft: padding };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Both layers' styles for one render: the shared typography plus each one's box. */
|
|
55
|
+
export function layerStyles(padding: number, virtual: CodeWindow | null | undefined) {
|
|
56
|
+
const content = contentStyle(padding);
|
|
57
|
+
return {
|
|
58
|
+
/** Textarea: the full document, so its padding is the plain inset. */
|
|
59
|
+
textarea: content,
|
|
60
|
+
/** `<pre>`: the window, with the unrendered lines reserved as padding. */
|
|
61
|
+
pre: {
|
|
62
|
+
...content,
|
|
63
|
+
paddingTop: padding + (virtual?.top ?? 0),
|
|
64
|
+
paddingBottom: padding + (virtual?.bottom ?? 0),
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The line commands: move, duplicate, delete, insert, and the caret's line
|
|
8
|
+
* edges.
|
|
9
|
+
*
|
|
10
|
+
* The cases that matter are the document's edges — the first and last line have
|
|
11
|
+
* no neighbour to swap with, a last line with no trailing newline still needs its
|
|
12
|
+
* own separator, and a line deleted rather than blanked. Each of those was a
|
|
13
|
+
* visible bug in the naive concatenation.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, expect, test } from 'bun:test';
|
|
17
|
+
|
|
18
|
+
import { deleteLines, duplicateLines, insertLine, lineEdges, moveLines } from '@/shared/lib/code/editor/line-edits';
|
|
19
|
+
|
|
20
|
+
describe('moveLines', () => {
|
|
21
|
+
test('moves the block up and carries the selection with it', () => {
|
|
22
|
+
const result = moveLines('one\ntwo\nthree', { start: 4, end: 8 }, -1);
|
|
23
|
+
|
|
24
|
+
expect(result.value).toBe('two\none\nthree');
|
|
25
|
+
expect([result.selectionStart, result.selectionEnd]).toEqual([0, 4]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('moves the block down', () => {
|
|
29
|
+
const result = moveLines('one\ntwo\nthree', { start: 4, end: 8 }, 1);
|
|
30
|
+
|
|
31
|
+
expect(result.value).toBe('one\nthree\ntwo');
|
|
32
|
+
expect([result.selectionStart, result.selectionEnd]).toEqual([10, 14]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('a block already at the edge does not move', () => {
|
|
36
|
+
const first = moveLines('one\ntwo', { start: 0, end: 4 }, -1);
|
|
37
|
+
expect(first.value).toBe('one\ntwo');
|
|
38
|
+
expect(first.selectionStart).toBe(0);
|
|
39
|
+
|
|
40
|
+
const last = moveLines('one\ntwo', { start: 4, end: 7 }, 1);
|
|
41
|
+
expect(last.value).toBe('one\ntwo');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('moving several lines keeps them together', () => {
|
|
45
|
+
const result = moveLines('a\nb\nc\nd', { start: 2, end: 6 }, 1);
|
|
46
|
+
|
|
47
|
+
expect(result.value).toBe('a\nd\nb\nc');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('duplicateLines', () => {
|
|
52
|
+
test('copies the line below and leaves the original selected', () => {
|
|
53
|
+
const result = duplicateLines('one\ntwo', { start: 0, end: 4 }, 1);
|
|
54
|
+
|
|
55
|
+
expect(result.value).toBe('one\none\ntwo');
|
|
56
|
+
expect([result.selectionStart, result.selectionEnd]).toEqual([0, 4]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('copies above and selects the copy', () => {
|
|
60
|
+
const result = duplicateLines('one\ntwo', { start: 4, end: 7 }, -1);
|
|
61
|
+
|
|
62
|
+
expect(result.value).toBe('one\ntwo\ntwo');
|
|
63
|
+
expect([result.selectionStart, result.selectionEnd]).toEqual([8, 11]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('a last line with no trailing newline still gets its own line', () => {
|
|
67
|
+
const result = duplicateLines('one\ntwo', { start: 4, end: 7 }, 1);
|
|
68
|
+
|
|
69
|
+
expect(result.value).toBe('one\ntwo\ntwo');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('deleteLines', () => {
|
|
74
|
+
test('removes the line rather than blanking it', () => {
|
|
75
|
+
const result = deleteLines('one\ntwo\nthree', { start: 4, end: 8 });
|
|
76
|
+
|
|
77
|
+
expect(result.value).toBe('one\nthree');
|
|
78
|
+
expect(result.selectionStart).toBe(4);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('deleting the last line leaves the caret at the buffer end', () => {
|
|
82
|
+
const result = deleteLines('one\ntwo', { start: 4, end: 7 });
|
|
83
|
+
|
|
84
|
+
expect(result.value).toBe('one\n');
|
|
85
|
+
expect(result.selectionStart).toBe(4);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('deleting a multi-line block removes all of it', () => {
|
|
89
|
+
const result = deleteLines('a\nb\nc\nd', { start: 2, end: 6 });
|
|
90
|
+
|
|
91
|
+
expect(result.value).toBe('a\nd');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('insertLine', () => {
|
|
96
|
+
test('opens a line below, keeping the indentation', () => {
|
|
97
|
+
const result = insertLine(' one\ntwo', 5, 'below');
|
|
98
|
+
|
|
99
|
+
expect(result.value).toBe(' one\n \ntwo');
|
|
100
|
+
expect(result.selectionStart).toBe(8);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('opens a line above, keeping the indentation', () => {
|
|
104
|
+
const result = insertLine(' one\ntwo', 5, 'above');
|
|
105
|
+
|
|
106
|
+
expect(result.value).toBe(' \n one\ntwo');
|
|
107
|
+
expect(result.selectionStart).toBe(2);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('on the buffer’s last line without a trailing newline the line is appended', () => {
|
|
111
|
+
const result = insertLine(' one', 5, 'below');
|
|
112
|
+
|
|
113
|
+
// No trailing newline: the new line simply follows, indented, and the caret
|
|
114
|
+
// lands after that indent. Writing a second newline (what this used to do)
|
|
115
|
+
// left a blank line under the caret.
|
|
116
|
+
expect(result.value).toBe(' one\n ');
|
|
117
|
+
expect(result.selectionStart).toBe(8);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('lineEdges', () => {
|
|
122
|
+
test('reports the start of the line and the end of its content', () => {
|
|
123
|
+
// The trailing newline is not part of the line's content, so ⌘→ stops
|
|
124
|
+
// before it rather than on the next line.
|
|
125
|
+
expect(lineEdges('one\ntwo\n', 5)).toEqual({ start: 4, end: 7 });
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('the last line has no newline to exclude', () => {
|
|
129
|
+
expect(lineEdges('one\ntwo', 5)).toEqual({ start: 4, end: 7 });
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The editor's line commands: move, duplicate, delete, insert a line, and the
|
|
8
|
+
* caret's line edges.
|
|
9
|
+
*
|
|
10
|
+
* Split from `editor-commands` (which owns the multi-selection and comment
|
|
11
|
+
* concerns) because the two families have nothing in common beyond both being
|
|
12
|
+
* pure string arithmetic — and because the repository caps a file at 350 lines.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { lineIndexAt, lineRangeAt, lineRangesInSelection, lineStartOffset, type TextRange } from '@/shared/lib/code/editor/lines';
|
|
16
|
+
|
|
17
|
+
/** The block of lines a selection touches, as ONE range. */
|
|
18
|
+
function selectionBlock(text: string, selection: TextRange): TextRange {
|
|
19
|
+
const lines = lineRangesInSelection(text, selection);
|
|
20
|
+
return { start: lines[0].start, end: lines[lines.length - 1].end };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Move the selected lines up (`-1`) or down (`1`).
|
|
25
|
+
*
|
|
26
|
+
* The two groups are swapped as a REGION rather than as two concatenations:
|
|
27
|
+
* the neighbour may be the document's last line with no trailing newline, so
|
|
28
|
+
* the separator between the swapped halves has to be re-emitted from the
|
|
29
|
+
* region's own shape. Getting that wrong turns `one\ntwo\nthree` into
|
|
30
|
+
* `one\nthreetwo` — which is what the naive `body + neighbour` swap produced.
|
|
31
|
+
*
|
|
32
|
+
* The selection travels with its lines, so a repeated ⌥↑ keeps moving the same
|
|
33
|
+
* block. At the document's first/last line there is no neighbour and the buffer
|
|
34
|
+
* is returned unchanged.
|
|
35
|
+
*/
|
|
36
|
+
export function moveLines(
|
|
37
|
+
text: string,
|
|
38
|
+
selection: TextRange,
|
|
39
|
+
direction: -1 | 1,
|
|
40
|
+
): { value: string; selectionStart: number; selectionEnd: number } {
|
|
41
|
+
const block = selectionBlock(text, selection);
|
|
42
|
+
const blockText = text.slice(block.start, block.end);
|
|
43
|
+
const neighbour = direction === -1 ? lineRangeAt(text, block.start - 1) : lineRangeAt(text, block.end);
|
|
44
|
+
if (direction === -1 && block.start === 0) {
|
|
45
|
+
return { value: text, selectionStart: selection.start, selectionEnd: selection.end };
|
|
46
|
+
}
|
|
47
|
+
if (direction === 1 && block.end >= text.length) {
|
|
48
|
+
return { value: text, selectionStart: selection.start, selectionEnd: selection.end };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const neighbourText = text.slice(neighbour.start, neighbour.end);
|
|
52
|
+
// Both groups are whole lines, so each carries at most one trailing newline.
|
|
53
|
+
const blockBody = blockText.replace(/\n$/, '');
|
|
54
|
+
const neighbourBody = neighbourText.replace(/\n$/, '');
|
|
55
|
+
const regionEndsWithNewline = text.slice(Math.min(block.start, neighbour.start), Math.max(block.end, neighbour.end)).endsWith('\n');
|
|
56
|
+
|
|
57
|
+
const ordered = direction === -1
|
|
58
|
+
? `${blockBody}\n${neighbourBody}`
|
|
59
|
+
: `${neighbourBody}\n${blockBody}`;
|
|
60
|
+
const region = ordered + (regionEndsWithNewline ? '\n' : '');
|
|
61
|
+
const regionStart = Math.min(block.start, neighbour.start);
|
|
62
|
+
const regionEnd = Math.max(block.end, neighbour.end);
|
|
63
|
+
|
|
64
|
+
// The block's own first line moves by exactly the neighbour's length plus the
|
|
65
|
+
// separator that now precedes it.
|
|
66
|
+
const shift = neighbourBody.length + 1;
|
|
67
|
+
const delta = direction === -1 ? -shift : shift;
|
|
68
|
+
return {
|
|
69
|
+
value: text.slice(0, regionStart) + region + text.slice(regionEnd),
|
|
70
|
+
selectionStart: selection.start + delta,
|
|
71
|
+
selectionEnd: selection.end + delta,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Copy the selected lines below (`1`) or above (`-1`); the copy stays selected. */
|
|
76
|
+
export function duplicateLines(
|
|
77
|
+
text: string,
|
|
78
|
+
selection: TextRange,
|
|
79
|
+
direction: -1 | 1,
|
|
80
|
+
): { value: string; selectionStart: number; selectionEnd: number } {
|
|
81
|
+
const block = selectionBlock(text, selection);
|
|
82
|
+
const body = text.slice(block.start, block.end);
|
|
83
|
+
// A block that reaches the end of a document with no trailing newline needs
|
|
84
|
+
// one, or the copy would land on the same line it came from.
|
|
85
|
+
const separator = body.endsWith('\n') ? '' : '\n';
|
|
86
|
+
|
|
87
|
+
if (direction === 1) {
|
|
88
|
+
const inserted = separator + body;
|
|
89
|
+
return {
|
|
90
|
+
value: text.slice(0, block.end) + inserted + text.slice(block.end),
|
|
91
|
+
selectionStart: selection.start,
|
|
92
|
+
selectionEnd: selection.end,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const inserted = body + separator;
|
|
96
|
+
return {
|
|
97
|
+
value: text.slice(0, block.start) + inserted + text.slice(block.start),
|
|
98
|
+
selectionStart: selection.start + inserted.length,
|
|
99
|
+
selectionEnd: selection.end + inserted.length,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Delete the selected lines, newline included — so the line is gone rather than
|
|
105
|
+
* blanked. The caret lands at the start of the line that took its place, or at
|
|
106
|
+
* the end of the buffer when the last line was deleted.
|
|
107
|
+
*/
|
|
108
|
+
export function deleteLines(
|
|
109
|
+
text: string,
|
|
110
|
+
selection: TextRange,
|
|
111
|
+
): { value: string; selectionStart: number; selectionEnd: number } {
|
|
112
|
+
const block = selectionBlock(text, selection);
|
|
113
|
+
const value = text.slice(0, block.start) + text.slice(block.end);
|
|
114
|
+
const caret = Math.min(block.start, value.length);
|
|
115
|
+
return { value, selectionStart: caret, selectionEnd: caret };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Open a new line above or below the caret's line, keeping that line's
|
|
120
|
+
* indentation. ⌘⏎ is "below" and ⌘⇧⏎ is "above", like VS Code.
|
|
121
|
+
*/
|
|
122
|
+
export function insertLine(
|
|
123
|
+
text: string,
|
|
124
|
+
offset: number,
|
|
125
|
+
where: 'above' | 'below',
|
|
126
|
+
): { value: string; selectionStart: number; selectionEnd: number } {
|
|
127
|
+
const line = lineRangeAt(text, offset);
|
|
128
|
+
const lineText = text.slice(line.start, line.end).replace(/\n$/, '');
|
|
129
|
+
const indent = lineText.match(/^\s*/)?.[0] ?? '';
|
|
130
|
+
|
|
131
|
+
if (where === 'above') {
|
|
132
|
+
const caret = line.start + indent.length;
|
|
133
|
+
return {
|
|
134
|
+
value: `${text.slice(0, line.start)}${indent}\n${text.slice(line.start)}`,
|
|
135
|
+
selectionStart: caret,
|
|
136
|
+
selectionEnd: caret,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Below a line that already ends in a newline the new line is inserted
|
|
141
|
+
// BEFORE that successor; below the document's last line the newline has to be
|
|
142
|
+
// written first. Emitting both unconditionally left a blank line behind
|
|
143
|
+
// (measured: `a\nb` + ⌘⏎ became `a\nb\n\n`).
|
|
144
|
+
const lineEndsWithNewline = text[line.end - 1] === '\n';
|
|
145
|
+
const inserted = lineEndsWithNewline ? `${indent}\n` : `\n${indent}`;
|
|
146
|
+
const caret = line.end + inserted.length - (lineEndsWithNewline ? 1 : 0);
|
|
147
|
+
return {
|
|
148
|
+
value: text.slice(0, line.end) + inserted + text.slice(line.end),
|
|
149
|
+
selectionStart: caret,
|
|
150
|
+
selectionEnd: caret,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The offset at the start of the caret's line, and at the end of it (⌘← / ⌘→). */
|
|
155
|
+
export function lineEdges(text: string, offset: number): { start: number; end: number } {
|
|
156
|
+
const line = lineRangeAt(text, offset);
|
|
157
|
+
const start = lineStartOffset(text, lineIndexAt(text, offset));
|
|
158
|
+
return { start, end: Math.max(start, line.end - (text[line.end - 1] === '\n' ? 1 : 0)) };
|
|
159
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Line arithmetic, which every line command and the gutter depend on agreeing
|
|
8
|
+
* about. The interesting cases are the two the arithmetic gets wrong by
|
|
9
|
+
* accident: a caret on the empty line a trailing newline opens, and a selection
|
|
10
|
+
* that ends exactly at a line start (which does not touch that line).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, expect, test } from 'bun:test';
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
countLines,
|
|
17
|
+
lineIndexAt,
|
|
18
|
+
lineRangeAt,
|
|
19
|
+
lineRangesInSelection,
|
|
20
|
+
lineStartOffset,
|
|
21
|
+
} from '@/shared/lib/code/editor/lines';
|
|
22
|
+
|
|
23
|
+
describe('lineIndexAt and lineStartOffset', () => {
|
|
24
|
+
test('are inverses for every line start', () => {
|
|
25
|
+
const text = 'one\ntwo\n\nfour';
|
|
26
|
+
// Line starts in `text`: 0, 4, 8 (the empty line) and 9.
|
|
27
|
+
for (const offset of [0, 4, 8, 9]) {
|
|
28
|
+
expect(lineStartOffset(text, lineIndexAt(text, offset))).toBe(offset);
|
|
29
|
+
}
|
|
30
|
+
expect(lineIndexAt(text, 13)).toBe(3);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('clamp an offset past the end to the last line', () => {
|
|
34
|
+
expect(lineIndexAt('one\ntwo', 999)).toBe(1);
|
|
35
|
+
expect(lineStartOffset('one\ntwo', 99)).toBe(7);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('count a trailing newline as opening one more, empty line', () => {
|
|
39
|
+
expect(countLines('one\ntwo')).toBe(2);
|
|
40
|
+
expect(countLines('one\ntwo\n')).toBe(3);
|
|
41
|
+
expect(countLines('')).toBe(1);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('lineRangeAt', () => {
|
|
46
|
+
test('includes the trailing newline, so the range can delete or move the line', () => {
|
|
47
|
+
expect(lineRangeAt('one\ntwo\nthree', 5)).toEqual({ start: 4, end: 8 });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('the last line has no newline to include', () => {
|
|
51
|
+
expect(lineRangeAt('one\ntwo', 5)).toEqual({ start: 4, end: 7 });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('a caret on the empty line a trailing newline opens yields an empty range there', () => {
|
|
55
|
+
// `a\n` has an empty second line at offset 2; the honest reading of that
|
|
56
|
+
// position is "no characters of its own", not the previous line.
|
|
57
|
+
expect(lineRangeAt('a\n', 2)).toEqual({ start: 2, end: 2 });
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('lineRangesInSelection', () => {
|
|
62
|
+
test('covers every line the selection touches', () => {
|
|
63
|
+
const text = 'one\ntwo\nthree';
|
|
64
|
+
expect(lineRangesInSelection(text, { start: 4, end: 7 })).toEqual([{ start: 4, end: 8 }]);
|
|
65
|
+
expect(lineRangesInSelection(text, { start: 0, end: 7 })).toEqual([
|
|
66
|
+
{ start: 0, end: 4 },
|
|
67
|
+
{ start: 4, end: 8 },
|
|
68
|
+
]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('a selection ending exactly at a line start does not touch that line', () => {
|
|
72
|
+
// The rule VS Code applies, and the one that keeps a whole-line selection
|
|
73
|
+
// from splitting off an empty trailing entry.
|
|
74
|
+
const text = 'one\ntwo';
|
|
75
|
+
expect(lineRangesInSelection(text, { start: 0, end: 4 })).toEqual([{ start: 0, end: 4 }]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('an empty selection still names the line the caret is on', () => {
|
|
79
|
+
expect(lineRangesInSelection('one\ntwo', { start: 5, end: 5 })).toEqual([{ start: 4, end: 7 }]);
|
|
80
|
+
});
|
|
81
|
+
});
|