@kabmo-labs/kabmo 0.8.0 → 0.9.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 +7 -0
- package/dist/client/index.html +1 -1
- package/dist/client/index.js +314 -314
- package/dist/client/static/js/{index-qwpvd9fv.js → index-bpptfkz2.js} +922 -892
- package/dist/client/static/js/index-k315p1e8.css +1 -0
- package/package.json +9 -1
- package/src/client/components/common/code-surface/documents.test.ts +120 -0
- package/src/client/components/common/code-surface/documents.ts +201 -0
- package/src/client/components/common/code-surface/extensions.test.ts +110 -0
- package/src/client/components/common/code-surface/extensions.ts +162 -0
- package/src/client/components/common/code-surface/handle.ts +102 -0
- package/src/client/components/common/code-surface/index.tsx +253 -314
- package/src/client/components/common/code-surface/keymap.test.ts +133 -0
- package/src/client/components/common/code-surface/keymap.ts +267 -0
- package/src/client/components/common/code-surface/marks.ts +74 -0
- package/src/client/components/common/code-surface/shiki.ts +234 -0
- package/src/client/components/common/code-surface/sync.test.ts +185 -0
- package/src/client/components/common/code-surface/sync.ts +58 -0
- package/src/client/components/common/code-surface/theme.test.ts +202 -0
- package/src/client/components/common/code-surface/theme.ts +111 -0
- package/src/client/components/common/editor-view-buttons/index.test.tsx +148 -0
- package/src/client/components/common/editor-view-buttons/index.tsx +109 -0
- package/src/client/components/common/shell-update-button/Modal.tsx +163 -0
- package/src/client/components/common/shell-update-button/index.test.tsx +251 -0
- package/src/client/components/common/shell-update-button/index.tsx +125 -0
- package/src/client/components/common/toolbar-row/index.test.tsx +166 -0
- package/src/client/components/common/toolbar-row/index.tsx +189 -0
- package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +41 -78
- package/src/client/components/layout/desktop-layout/index.tsx +47 -67
- package/src/client/components/layout/desktop-layout/resizer/index.tsx +6 -3
- package/src/client/components/layout/session-sidebar/Footer.tsx +8 -15
- package/src/client/components/mobile/LayoutWrapper.tsx +1 -0
- package/src/client/components/mobile/mobile-right-sidebar/FullDiff.tsx +3 -0
- package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +63 -38
- package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.test.tsx +137 -0
- package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.tsx +14 -3
- package/src/client/components/mobile/mobile-session-sidebar/Footer.tsx +9 -21
- package/src/client/components/settings/Modal.tsx +8 -0
- package/src/client/components/settings/categories/AppearanceSettings.tsx +3 -0
- package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.test.tsx +167 -0
- package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.tsx +184 -0
- package/src/client/components/settings/categories/project-settings/WorktreeSettings.tsx +8 -2
- package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +16 -8
- package/src/client/components/workspace/chat-timeline/SystemNotice.tsx +9 -4
- package/src/client/components/workspace/chat-timeline/TaskResultContent.tsx +9 -7
- package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.test.tsx +132 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.tsx +120 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.test.tsx +319 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.tsx +285 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/index.test.ts +2 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/hashline-patch/index.tsx +34 -29
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/AstEdit.tsx +11 -9
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Bash.tsx +10 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Edit.tsx +25 -8
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.tsx +8 -11
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.test.ts +114 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.tsx +111 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Read.tsx +7 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/SearchTool.tsx +9 -59
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ArtifactBody.tsx +2 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.test.ts +27 -28
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.tsx +39 -26
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ExcerptCode.tsx +8 -8
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/HighlightedCode.tsx +35 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/JsonCodeBlock.tsx +4 -4
- package/src/client/components/workspace/diff-panel/MergeDiffView.tsx +179 -0
- package/src/client/components/workspace/diff-panel/ThemedMergeDiffView.tsx +24 -0
- package/src/client/components/workspace/diff-panel/Toolbar.tsx +224 -186
- package/src/client/components/workspace/diff-panel/ToolbarSegments.tsx +98 -0
- package/src/client/components/workspace/diff-panel/index.tsx +76 -145
- package/src/client/components/workspace/diff-panel/merge.ts +315 -0
- package/src/client/components/workspace/editor/Body.tsx +4 -7
- package/src/client/components/workspace/editor/CommandPalette.tsx +2 -2
- package/src/client/components/workspace/editor/FileDocument.tsx +58 -63
- package/src/client/components/workspace/editor/FilePane.tsx +4 -7
- package/src/client/components/workspace/editor/FindWidget.tsx +7 -2
- package/src/client/components/workspace/editor/Toolbar.tsx +220 -123
- package/src/client/components/workspace/editor/index.tsx +7 -9
- package/src/client/components/workspace/file-explorer/OutlineSection.test.tsx +18 -0
- package/src/client/components/workspace/file-explorer/OutlineSection.tsx +10 -3
- package/src/client/components/workspace/search-panel/Body.tsx +1 -1
- package/src/client/components/workspace/worktree-modal/index.test.tsx +118 -0
- package/src/client/hooks/chat/omp/reconnect.test.ts +338 -0
- package/src/client/hooks/chat/omp/reconnect.ts +187 -0
- package/src/client/hooks/chat/omp/run-liveness.test.ts +71 -0
- package/src/client/hooks/chat/omp/run-liveness.ts +65 -0
- package/src/client/hooks/chat/omp/session-list.activity.test.ts +114 -0
- package/src/client/hooks/chat/omp/session-list.test-util.ts +164 -0
- package/src/client/hooks/chat/omp/session-list.test.ts +30 -150
- package/src/client/hooks/chat/omp/session-list.tsx +69 -9
- package/src/client/hooks/chat/timeline/generating.ts +19 -2
- package/src/client/hooks/chat/timeline/index.ts +19 -24
- package/src/client/hooks/chat/timeline/lost-run.test.ts +329 -0
- package/src/client/hooks/chat/timeline/lost-run.ts +177 -0
- package/src/client/hooks/chat/timeline/session-load.test-util.ts +205 -0
- package/src/client/hooks/chat/timeline/session-load.test.ts +61 -162
- package/src/client/hooks/chat/timeline/session-load.ts +69 -37
- package/src/client/hooks/chat/timeline/session-switch.test.ts +65 -0
- package/src/client/hooks/chat/timeline/session-switch.ts +63 -0
- package/src/client/hooks/code/deferred-highlight.test.ts +182 -0
- package/src/client/hooks/code/deferred-highlight.ts +129 -0
- package/src/client/hooks/editor/find-state.ts +1 -1
- package/src/client/hooks/editor/theme.test.ts +206 -0
- package/src/client/hooks/editor/theme.ts +82 -0
- package/src/client/hooks/editor/use-editor-drop.test.ts +101 -0
- package/src/client/hooks/editor/use-editor-drop.ts +67 -14
- package/src/client/hooks/editor/use-editor-find.ts +7 -7
- package/src/client/hooks/editor/use-outline-view.test.ts +182 -0
- package/src/client/hooks/editor/use-outline-view.ts +94 -7
- package/src/client/hooks/editor/use-scroll-restore.test.ts +55 -2
- package/src/client/hooks/editor/use-scroll-restore.ts +124 -4
- package/src/client/hooks/editor/use-search-reveal.ts +1 -1
- package/src/client/hooks/ui/settings-modal.ts +56 -0
- package/src/client/hooks/ui/updates/shell-download.test.ts +2 -0
- package/src/client/hooks/ui/updates/shell-download.ts +55 -14
- package/src/client/hooks/ui/updates/shell-offer.test.ts +26 -0
- package/src/client/hooks/ui/updates/shell-update.test.ts +140 -0
- package/src/client/hooks/ui/updates/shell-update.ts +119 -0
- package/src/client/hooks/workspace/diff-file.ts +29 -10
- package/src/client/hooks/workspace/diff-git-actions.ts +147 -0
- package/src/client/hooks/workspace/editor-view.ts +61 -0
- package/src/client/hooks/workspace/file-tabs.test.ts +62 -5
- package/src/client/hooks/workspace/file-tabs.ts +24 -11
- package/src/client/hooks/workspace/navbar-session-actions.ts +72 -0
- package/src/client/hooks/workspace/worktree/index.ts +20 -5
- package/src/client/lib/desktop/bridge.ts +45 -0
- package/src/client/lib/search-reveal.ts +1 -1
- package/src/client/main.tsx +5 -1
- package/src/client/tailwind.css +37 -8
- package/src/server/lib/auth/guard.test.ts +28 -0
- package/src/server/lib/auth/guard.ts +10 -0
- package/src/server/lib/fs/worktree/create.ts +2 -3
- package/src/server/lib/fs/worktree/list.ts +13 -0
- package/src/server/lib/fs/worktree/remove.ts +3 -3
- package/src/server/lib/lifecycle/claim-port.test.ts +156 -0
- package/src/server/lib/lifecycle/port-guard.test.ts +20 -164
- package/src/server/lib/lifecycle/port-guard.ts +39 -7
- package/src/server/lib/lifecycle/probe-health.test.ts +166 -0
- package/src/server/lib/lifecycle/probe.test.ts +77 -164
- package/src/server/lib/lifecycle/probe.ts +121 -13
- package/src/server/lib/omp/rpc/frame-fold-signals.test.ts +112 -0
- package/src/server/lib/omp/rpc/frame-fold.ts +10 -5
- package/src/server/lib/omp/session/sidebar-activity.server.test.ts +162 -0
- package/src/server/lib/omp/session/sidebar-activity.server.ts +70 -0
- package/src/server/lib/omp/session/telemetry/compute.ts +13 -1
- package/src/server/lib/omp/session/telemetry.test.ts +33 -0
- package/src/server/lib/realtime/signals.server.ts +10 -0
- package/src/server/lib/realtime/topics/catalog.server.ts +4 -1
- package/src/server/lib/realtime/topics.server.ts +16 -0
- package/src/server/lib/updates/shell-assets.ts +94 -3
- package/src/server/lib/updates/shell-cache.ts +105 -0
- package/src/server/lib/updates/shell-pages.test.ts +33 -2
- package/src/server/lib/updates/shell-pages.ts +17 -5
- package/src/server/lib/updates/shell-release-desktop.test.ts +166 -0
- package/src/server/lib/updates/shell-release.test.ts +10 -27
- package/src/server/lib/updates/shell-release.ts +107 -111
- package/src/server/routes/fs/git-merge.test.ts +11 -1
- package/src/server/routes/fs/read.test.ts +24 -7
- package/src/server/routes/fs/scope.test.ts +15 -10
- package/src/server/routes/fs/scope.ts +3 -14
- package/src/server/routes/updates/android.ts +6 -1
- package/src/server/routes/updates/desktop.ts +29 -7
- package/src/server/routes/updates/ios.ts +5 -1
- package/src/server/routes/worktrees/list.ts +11 -1
- package/src/server/routes/worktrees/routes.test.ts +17 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.test.ts +36 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.ts +18 -0
- package/src/shared/lib/chat/timeline/run-signals.test.ts +2 -0
- package/src/shared/lib/code/editor/comment-syntax.test.ts +37 -0
- package/src/shared/lib/code/editor/comment-syntax.ts +55 -0
- package/src/shared/lib/code/editor/find-replace.test.ts +134 -0
- package/src/shared/lib/code/editor/{find.ts → find-replace.ts} +23 -107
- package/src/shared/lib/code/editor/{find.test.ts → find-scan.test.ts} +19 -73
- package/src/shared/lib/code/editor/find-scan.ts +177 -0
- package/src/shared/lib/code/editor/keymap.ts +3 -3
- package/src/shared/lib/code/editor/lines.test.ts +7 -55
- package/src/shared/lib/code/editor/lines.ts +8 -48
- package/src/shared/lib/code/editor/marks.test.ts +4 -5
- package/src/shared/lib/code/editor/marks.ts +13 -25
- package/src/shared/lib/code/editor/theme-loader.ts +97 -0
- package/src/shared/lib/code/editor/themes.generated.ts +156 -0
- package/src/shared/lib/code/editor/themes.test.ts +165 -0
- package/src/shared/lib/code/editor/themes.ts +165 -0
- package/src/shared/lib/code/highlight-memo.test.ts +103 -0
- package/src/shared/lib/code/highlight-memo.ts +149 -0
- package/src/shared/lib/code/highlighter-warmup.ts +112 -0
- package/src/shared/lib/code/highlighter.ts +3 -1
- package/src/shared/lib/code/idle.ts +37 -0
- package/src/shared/lib/code/outline/rows.test.ts +96 -0
- package/src/shared/lib/code/outline/rows.ts +94 -0
- package/src/shared/lib/code/syntax-highlight.test.ts +1 -79
- package/src/shared/lib/code/syntax-highlight.ts +2 -3
- package/src/shared/lib/fs/file-kind-classify.test.ts +25 -11
- package/src/shared/lib/fs/file-kind.ts +47 -13
- package/src/shared/lib/fs/file-url.test.ts +52 -0
- package/src/shared/lib/fs/file-url.ts +37 -0
- package/src/shared/lib/fs/patch-sides.test.ts +114 -0
- package/src/shared/lib/fs/patch-sides.ts +139 -0
- package/src/shared/lib/realtime/protocol.ts +16 -0
- package/src/shared/lib/ui/toolbar-overflow.test.ts +83 -0
- package/src/shared/lib/ui/toolbar-overflow.ts +86 -0
- package/src/shared/lib/updates/shell-arch.ts +54 -0
- package/src/shared/lib/updates/shell-ua.ts +4 -4
- package/src/shared/lib/workspace/editor-view-mode.test.ts +70 -0
- package/src/shared/lib/workspace/editor-view-mode.ts +48 -0
- package/src/shared/lib/workspace/panel-widths.test.ts +30 -0
- package/src/shared/lib/workspace/panel-widths.ts +25 -5
- package/src/shared/lib/workspace/session-state/cache.ts +159 -0
- package/src/shared/lib/workspace/session-state/harness.test-util.ts +55 -0
- package/src/shared/lib/workspace/session-state/merge.ts +100 -0
- package/src/shared/lib/workspace/session-state/store-load.test.ts +206 -0
- package/src/shared/lib/workspace/session-state/store.test.ts +17 -139
- package/src/shared/lib/workspace/session-state/store.ts +92 -130
- package/src/shared/lib/workspace/workspace-panel-widths.ts +124 -0
- package/src/shared/types/realtime.ts +29 -0
- package/src/shared/types/settings/state.ts +10 -0
- package/src/test-support/loopback.ts +183 -0
- package/dist/client/static/js/index-gtbpewr7.css +0 -1
- package/src/client/components/common/code-editor/handler.test.ts +0 -274
- package/src/client/components/common/code-editor/handler.ts +0 -175
- package/src/client/components/common/code-editor/index.tsx +0 -314
- package/src/client/components/common/code-surface/measure.ts +0 -148
- package/src/client/components/workspace/chat-timeline/target-picker/index.test.tsx +0 -227
- package/src/client/components/workspace/chat-timeline/target-picker/index.tsx +0 -260
- package/src/client/components/workspace/diff-panel/SplitView.tsx +0 -209
- package/src/client/components/workspace/diff-panel/UnifiedView.tsx +0 -150
- package/src/client/hooks/editor/scroll-container.test.ts +0 -102
- package/src/client/hooks/editor/scroll-container.ts +0 -26
- package/src/client/hooks/editor/use-code-window.ts +0 -100
- package/src/client/hooks/editor/use-editor-history.test.ts +0 -216
- package/src/client/hooks/editor/use-editor-history.ts +0 -134
- package/src/client/hooks/editor/use-occurrences.test.ts +0 -236
- package/src/client/hooks/editor/use-occurrences.ts +0 -98
- package/src/shared/lib/code/editor/commands.test.ts +0 -155
- package/src/shared/lib/code/editor/commands.ts +0 -260
- package/src/shared/lib/code/editor/edits.test.ts +0 -100
- package/src/shared/lib/code/editor/edits.ts +0 -122
- package/src/shared/lib/code/editor/history.test.ts +0 -118
- package/src/shared/lib/code/editor/history.ts +0 -101
- package/src/shared/lib/code/editor/layers.test.ts +0 -77
- package/src/shared/lib/code/editor/layers.ts +0 -67
- package/src/shared/lib/code/editor/line-edits.test.ts +0 -131
- package/src/shared/lib/code/editor/line-edits.ts +0 -159
- package/src/shared/lib/code/editor/occurrences.test.ts +0 -117
- package/src/shared/lib/code/editor/occurrences.ts +0 -111
- package/src/shared/lib/code/editor/reset-css.test.ts +0 -35
- package/src/shared/lib/code/editor/reset-css.ts +0 -32
- package/src/shared/lib/code/editor/text-commands.test.ts +0 -178
- package/src/shared/lib/code/editor/text-commands.ts +0 -154
- package/src/shared/lib/code/lazy-window.test.ts +0 -161
- package/src/shared/lib/code/lazy-window.ts +0 -156
- package/src/shared/lib/code/windowed-highlight.ts +0 -114
- package/src/shared/lib/fs/diff-parser.test.ts +0 -116
- package/src/shared/lib/fs/diff-parser.ts +0 -213
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The keymap generator.
|
|
8
|
+
*
|
|
9
|
+
* Its whole contract is negative: a command the palette lists must be BOUND, or
|
|
10
|
+
* the palette advertises a shortcut nothing listens for. That property is only
|
|
11
|
+
* checkable by comparing the generated CM6 bindings against `EDITOR_KEY_BINDINGS`
|
|
12
|
+
* — which is what this does, row by row, so adding a row to the table and
|
|
13
|
+
* forgetting the binding fails here instead of in a user's hands.
|
|
14
|
+
*
|
|
15
|
+
* The name mapping is the part that is easy to get silently wrong: CM6 matches
|
|
16
|
+
* on `event.keyCode` through `w3c-keyname`, so `Digit1` is `1` and `Slash` is
|
|
17
|
+
* `/` — a binding written `Digit1` never fires.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { describe, expect, test } from 'bun:test';
|
|
21
|
+
import { EditorState, type TransactionSpec } from '@codemirror/state';
|
|
22
|
+
import type { EditorView } from '@codemirror/view';
|
|
23
|
+
|
|
24
|
+
import { EDITOR_KEY_BINDINGS } from '@/shared/lib/code/editor/keymap';
|
|
25
|
+
import { codeMirrorKey, runSurfaceCommand } from '@/client/components/common/code-surface/keymap';
|
|
26
|
+
|
|
27
|
+
describe('codeMirrorKey', () => {
|
|
28
|
+
test('maps letters, digits and function keys to the names CM6 matches', () => {
|
|
29
|
+
// Lower case for a bare letter: `keyName` reports the character the key
|
|
30
|
+
// produces, which is `d` unshifted. The shifted form (`D`) is reached
|
|
31
|
+
// through CM6's own base-character fallback, so the row still fires.
|
|
32
|
+
expect(codeMirrorKey({ code: 'KeyD', meta: true })).toBe('Meta-d');
|
|
33
|
+
expect(codeMirrorKey({ code: 'Digit1', meta: true, shift: true })).toBe('Shift-Meta-1');
|
|
34
|
+
expect(codeMirrorKey({ code: 'F3' })).toBe('F3');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('maps punctuation to the character the key produces unshifted', () => {
|
|
38
|
+
// `Slash` is `/` on every layout that has it; `BracketRight` is `]`.
|
|
39
|
+
expect(codeMirrorKey({ code: 'Slash', ctrl: true })).toBe('Ctrl-/');
|
|
40
|
+
expect(codeMirrorKey({ code: 'BracketRight', meta: true })).toBe('Meta-]');
|
|
41
|
+
expect(codeMirrorKey({ code: 'BracketLeft', ctrl: true })).toBe('Ctrl-[');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('names the keys that have their own spelling', () => {
|
|
45
|
+
expect(codeMirrorKey({ code: 'Enter', meta: true })).toBe('Meta-Enter');
|
|
46
|
+
expect(codeMirrorKey({ code: 'ArrowUp', alt: true })).toBe('Alt-ArrowUp');
|
|
47
|
+
expect(codeMirrorKey({ code: 'Home' })).toBe('Home');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('orders modifiers the way CM6 canonicalises them', () => {
|
|
51
|
+
// CM6's `modifiers()` prepends alt, then ctrl, then meta, then shift, so
|
|
52
|
+
// the canonical spelling runs `Shift-Meta-Ctrl-Alt-<key>`. A different
|
|
53
|
+
// order here would be a binding CM6 never matches.
|
|
54
|
+
expect(codeMirrorKey({ code: 'ArrowDown', ctrl: true, alt: true, shift: true })).toBe('Shift-Ctrl-Alt-ArrowDown');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('reports null for a key with no name, so the row is skipped', () => {
|
|
58
|
+
expect(codeMirrorKey({ code: 'IntlBackslash' })).toBeNull();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('EDITOR_KEY_BINDINGS coverage', () => {
|
|
63
|
+
test('every row resolves to a chord on both platforms', () => {
|
|
64
|
+
const missing = EDITOR_KEY_BINDINGS.filter(
|
|
65
|
+
(binding) => codeMirrorKey(binding.mac) === null || codeMirrorKey(binding.other) === null,
|
|
66
|
+
).map((binding) => binding.command);
|
|
67
|
+
|
|
68
|
+
// A row whose chord cannot be named is a palette entry that does nothing —
|
|
69
|
+
// the exact failure this table exists to prevent.
|
|
70
|
+
expect(missing).toEqual([]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('no two rows claim the same chord on the same platform', () => {
|
|
74
|
+
for (const platform of ['mac', 'other'] as const) {
|
|
75
|
+
const seen = new Map<string, string>();
|
|
76
|
+
for (const binding of EDITOR_KEY_BINDINGS) {
|
|
77
|
+
const chord = codeMirrorKey(binding[platform]);
|
|
78
|
+
if (!chord) continue;
|
|
79
|
+
// A duplicate would make the first row win silently and leave the
|
|
80
|
+
// second's palette entry dead.
|
|
81
|
+
expect(seen.has(chord) ? `${chord} (${seen.get(chord)} and ${binding.command})` : null).toBeNull();
|
|
82
|
+
seen.set(chord, binding.command);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The two commands the surface implements itself, run against a state.
|
|
90
|
+
*
|
|
91
|
+
* `insertLineAbove` is the one that has no CM6 equivalent, and its indentation
|
|
92
|
+
* rule is the part a rewrite loses silently: a bare newline above an indented
|
|
93
|
+
* line puts the caret at column 0, so the next thing typed lands outside the
|
|
94
|
+
* block. Driven through `runSurfaceCommand` — the palette's and the handle's own
|
|
95
|
+
* path in — with a `{ state, dispatch }` view, which is all a `StateCommand`
|
|
96
|
+
* touches.
|
|
97
|
+
*/
|
|
98
|
+
describe('surface commands', () => {
|
|
99
|
+
/**
|
|
100
|
+
* The two commands are `StateCommand`s: they read `state` and hand a
|
|
101
|
+
* transaction SPEC to `dispatch`, nothing else. A view-shaped stand-in is what
|
|
102
|
+
* `runSurfaceCommand` takes, so the cast states that boundary once rather than
|
|
103
|
+
* mounting an `EditorView` in happy-dom for a rule with no DOM in it.
|
|
104
|
+
*/
|
|
105
|
+
function viewFor(doc: string, anchor: number): EditorView {
|
|
106
|
+
let state = EditorState.create({ doc, selection: { anchor } });
|
|
107
|
+
const view = {
|
|
108
|
+
get state() {
|
|
109
|
+
return state;
|
|
110
|
+
},
|
|
111
|
+
// A command dispatches a `TransactionSpec`; `EditorState.update` is the
|
|
112
|
+
// half of the real view's `dispatch` these commands depend on.
|
|
113
|
+
dispatch: (...specs: TransactionSpec[]) => {
|
|
114
|
+
state = state.update(...specs).state;
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
return view as unknown as EditorView;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
test('insertLineAbove keeps the current line’s indentation and lands after it', () => {
|
|
121
|
+
const view = viewFor(' const a = 1;\nconst b = 2;\n', 4);
|
|
122
|
+
expect(runSurfaceCommand('insertLineAbove', view)).toBe(true);
|
|
123
|
+
expect(view.state.doc.toString()).toBe(' \n const a = 1;\nconst b = 2;\n');
|
|
124
|
+
expect(view.state.selection.main.head).toBe(2);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('insertLineAbove on an unindented line inserts a bare blank line', () => {
|
|
128
|
+
const view = viewFor('one\ntwo\n', 5);
|
|
129
|
+
runSurfaceCommand('insertLineAbove', view);
|
|
130
|
+
expect(view.state.doc.toString()).toBe('one\n\ntwo\n');
|
|
131
|
+
expect(view.state.selection.main.head).toBe(4);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The editor's chords, generated from `EDITOR_KEY_BINDINGS`.
|
|
8
|
+
*
|
|
9
|
+
* The table in `shared/lib/code/editor/keymap.ts` is the ONE statement of what
|
|
10
|
+
* each chord means: the command palette lists it, the find bar's tooltips print
|
|
11
|
+
* it, and this module turns it into CodeMirror bindings. A hand-written CM6
|
|
12
|
+
* keymap would be a second statement that drifts — the palette would advertise
|
|
13
|
+
* `⌥⌘C` for a key CM6 never bound.
|
|
14
|
+
*
|
|
15
|
+
* Panel-owned commands (`find`, `replace`, the match steps, word wrap) are bound
|
|
16
|
+
* HERE too and delegate outward through `onCommand`, so a chord pressed inside
|
|
17
|
+
* the document reaches the same dispatcher the palette uses. Consuming the key
|
|
18
|
+
* is the point: `⌘F` inside the editor must open Kabmo's find bar, never the
|
|
19
|
+
* browser's.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { Extension, StateCommand } from '@codemirror/state';
|
|
23
|
+
import { EditorSelection } from '@codemirror/state';
|
|
24
|
+
import {
|
|
25
|
+
addCursorBelow,
|
|
26
|
+
defaultKeymap,
|
|
27
|
+
copyLineDown,
|
|
28
|
+
copyLineUp,
|
|
29
|
+
cursorDocEnd,
|
|
30
|
+
cursorDocStart,
|
|
31
|
+
cursorLineEnd,
|
|
32
|
+
cursorLineStart,
|
|
33
|
+
deleteLine,
|
|
34
|
+
indentLess,
|
|
35
|
+
indentMore,
|
|
36
|
+
insertBlankLine,
|
|
37
|
+
moveLineDown,
|
|
38
|
+
moveLineUp,
|
|
39
|
+
redo,
|
|
40
|
+
selectDocEnd,
|
|
41
|
+
selectDocStart,
|
|
42
|
+
selectLine,
|
|
43
|
+
selectLineEnd,
|
|
44
|
+
selectLineStart,
|
|
45
|
+
simplifySelection,
|
|
46
|
+
toggleComment,
|
|
47
|
+
undo,
|
|
48
|
+
} from '@codemirror/commands';
|
|
49
|
+
import { selectNextOccurrence, selectSelectionMatches } from '@codemirror/search';
|
|
50
|
+
import { keymap, type Command, type EditorView, type KeyBinding } from '@codemirror/view';
|
|
51
|
+
|
|
52
|
+
import { EDITOR_KEY_BINDINGS, type EditorCommand } from '@/shared/lib/code/editor/keymap';
|
|
53
|
+
import type { KeyStroke } from '@/shared/lib/ui/key-binding';
|
|
54
|
+
|
|
55
|
+
/** A command as this module invokes it: CM6's `Command` and `StateCommand` both fit. */
|
|
56
|
+
type SurfaceCommand = (view: EditorView) => boolean;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Insert a blank line ABOVE the caret's line, which CM6 has no command for.
|
|
60
|
+
*
|
|
61
|
+
* The new line carries the current line's own leading whitespace, and the caret
|
|
62
|
+
* lands after it — the rule the textarea surface applied, and the one that makes
|
|
63
|
+
* the key useful inside a block: a bare newline above an indented line left the
|
|
64
|
+
* caret at column 0 and the new line unindented, so the very next thing typed
|
|
65
|
+
* was in the wrong place. (CM6's `insertBlankLine`, used for "below", derives
|
|
66
|
+
* the same indentation from `IndentContext`; this mirrors it without needing a
|
|
67
|
+
* grammar.)
|
|
68
|
+
*/
|
|
69
|
+
const insertLineAbove: SurfaceCommand = (view) => {
|
|
70
|
+
const { state } = view;
|
|
71
|
+
const range = state.selection.main;
|
|
72
|
+
const line = state.doc.lineAt(range.from);
|
|
73
|
+
const indent = /^[ \t]*/.exec(line.text)?.[0] ?? '';
|
|
74
|
+
view.dispatch({
|
|
75
|
+
changes: { from: line.from, insert: `${indent}\n` },
|
|
76
|
+
selection: { anchor: line.from + indent.length },
|
|
77
|
+
});
|
|
78
|
+
return true;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** Select the word under the caret, or nothing when the caret is between words. */
|
|
82
|
+
const selectWord: SurfaceCommand = (view) => {
|
|
83
|
+
const { state } = view;
|
|
84
|
+
const range = state.selection.main;
|
|
85
|
+
if (range.from !== range.to) return false;
|
|
86
|
+
const line = state.doc.lineAt(range.from);
|
|
87
|
+
const offset = range.from - line.from;
|
|
88
|
+
const isWord = (char: string | undefined) => char !== undefined && /[\w$]/.test(char);
|
|
89
|
+
let start = offset;
|
|
90
|
+
let end = offset;
|
|
91
|
+
if (!isWord(line.text[start]) && isWord(line.text[start - 1])) start--;
|
|
92
|
+
if (!isWord(line.text[start])) return false;
|
|
93
|
+
end = start;
|
|
94
|
+
while (isWord(line.text[start - 1])) start--;
|
|
95
|
+
while (isWord(line.text[end])) end++;
|
|
96
|
+
view.dispatch({
|
|
97
|
+
selection: EditorSelection.range(line.from + start, line.from + end),
|
|
98
|
+
});
|
|
99
|
+
return true;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Collapse extra selections, keeping the main one.
|
|
104
|
+
*
|
|
105
|
+
* Escape's first job when ⌘D left several carets behind; only the next press
|
|
106
|
+
* hands the keyboard back to the page, which the panel's own capture handler
|
|
107
|
+
* does once this reports there was nothing left to collapse.
|
|
108
|
+
*/
|
|
109
|
+
const collapseSelections: SurfaceCommand = (view) => {
|
|
110
|
+
if (view.state.selection.ranges.length <= 1) return false;
|
|
111
|
+
return (simplifySelection as StateCommand)(view);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Commands the BUFFER owns. Everything absent from this table is either the
|
|
116
|
+
* panel's (`find`, `replace`, `toggleWordWrap`) or a chord with no meaning in a
|
|
117
|
+
* CM6 surface.
|
|
118
|
+
*/
|
|
119
|
+
const BUFFER_COMMANDS: Partial<Record<EditorCommand, SurfaceCommand>> = {
|
|
120
|
+
undo: undo as SurfaceCommand,
|
|
121
|
+
redo: redo as SurfaceCommand,
|
|
122
|
+
selectNextOccurrence: selectNextOccurrence as SurfaceCommand,
|
|
123
|
+
selectAllOccurrences: selectSelectionMatches as SurfaceCommand,
|
|
124
|
+
insertCursorBelow: addCursorBelow as SurfaceCommand,
|
|
125
|
+
moveLineUp: moveLineUp as SurfaceCommand,
|
|
126
|
+
moveLineDown: moveLineDown as SurfaceCommand,
|
|
127
|
+
copyLineUp: copyLineUp as SurfaceCommand,
|
|
128
|
+
copyLineDown: copyLineDown as SurfaceCommand,
|
|
129
|
+
deleteLine: deleteLine as SurfaceCommand,
|
|
130
|
+
toggleComment: toggleComment as SurfaceCommand,
|
|
131
|
+
insertLineBelow: insertBlankLine as SurfaceCommand,
|
|
132
|
+
insertLineAbove,
|
|
133
|
+
indent: indentMore as SurfaceCommand,
|
|
134
|
+
outdent: indentLess as SurfaceCommand,
|
|
135
|
+
lineStart: cursorLineStart as SurfaceCommand,
|
|
136
|
+
lineEnd: cursorLineEnd as SurfaceCommand,
|
|
137
|
+
documentStart: cursorDocStart as SurfaceCommand,
|
|
138
|
+
documentEnd: cursorDocEnd as SurfaceCommand,
|
|
139
|
+
selectToLineStart: selectLineStart as SurfaceCommand,
|
|
140
|
+
selectToLineEnd: selectLineEnd as SurfaceCommand,
|
|
141
|
+
selectToDocumentStart: selectDocStart as SurfaceCommand,
|
|
142
|
+
selectToDocumentEnd: selectDocEnd as SurfaceCommand,
|
|
143
|
+
selectWord,
|
|
144
|
+
selectLine: selectLine as SurfaceCommand,
|
|
145
|
+
escape: collapseSelections,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/** Run a command against a live view — the palette's and the handle's path in. */
|
|
149
|
+
export function runSurfaceCommand(command: EditorCommand, view: EditorView, onCommand?: (command: EditorCommand) => void): boolean {
|
|
150
|
+
const run = BUFFER_COMMANDS[command];
|
|
151
|
+
if (run) return run(view);
|
|
152
|
+
onCommand?.(command);
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Codes whose printed name is not their `KeyboardEvent.code` suffix. */
|
|
157
|
+
const NAMED_KEYS: Record<string, string> = {
|
|
158
|
+
Enter: 'Enter',
|
|
159
|
+
Escape: 'Escape',
|
|
160
|
+
Backspace: 'Backspace',
|
|
161
|
+
Delete: 'Delete',
|
|
162
|
+
Tab: 'Tab',
|
|
163
|
+
Space: 'Space',
|
|
164
|
+
ArrowUp: 'ArrowUp',
|
|
165
|
+
ArrowDown: 'ArrowDown',
|
|
166
|
+
ArrowLeft: 'ArrowLeft',
|
|
167
|
+
ArrowRight: 'ArrowRight',
|
|
168
|
+
Home: 'Home',
|
|
169
|
+
End: 'End',
|
|
170
|
+
PageUp: 'PageUp',
|
|
171
|
+
PageDown: 'PageDown',
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const PUNCTUATION_KEYS: Record<string, string> = {
|
|
175
|
+
Slash: '/',
|
|
176
|
+
Backslash: '\\',
|
|
177
|
+
Comma: ',',
|
|
178
|
+
Period: '.',
|
|
179
|
+
Semicolon: ';',
|
|
180
|
+
Quote: "'",
|
|
181
|
+
Backquote: '`',
|
|
182
|
+
Minus: '-',
|
|
183
|
+
Equal: '=',
|
|
184
|
+
BracketLeft: '[',
|
|
185
|
+
BracketRight: ']',
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The key CodeMirror names a physical key by, WITHOUT modifiers.
|
|
190
|
+
*
|
|
191
|
+
* CM6 matches on `event.keyCode` through `w3c-keyname`, so the base name is the
|
|
192
|
+
* character the key produces unshifted — `Digit1` is `1`, not `!`, and a
|
|
193
|
+
* `Shift` binding is written `Shift-1`. Punctuation is its US character, which
|
|
194
|
+
* is what that library reports for the same key code.
|
|
195
|
+
*/
|
|
196
|
+
function baseKey(code: string): string | null {
|
|
197
|
+
if (/^Key[A-Z]$/.test(code)) return code.slice(3).toLowerCase();
|
|
198
|
+
if (/^Digit\d$/.test(code)) return code.slice(5);
|
|
199
|
+
if (/^F\d{1,2}$/.test(code)) return code;
|
|
200
|
+
return NAMED_KEYS[code] ?? PUNCTUATION_KEYS[code] ?? null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** `{code:'Digit1', meta:true, shift:true}` → `Meta-Shift-1` (CM6's canonical order). */
|
|
204
|
+
export function codeMirrorKey(stroke: KeyStroke): string | null {
|
|
205
|
+
const key = baseKey(stroke.code);
|
|
206
|
+
if (!key) return null;
|
|
207
|
+
let name = key;
|
|
208
|
+
if (stroke.alt) name = `Alt-${name}`;
|
|
209
|
+
if (stroke.ctrl) name = `Ctrl-${name}`;
|
|
210
|
+
if (stroke.meta) name = `Meta-${name}`;
|
|
211
|
+
if (stroke.shift) name = `Shift-${name}`;
|
|
212
|
+
return name;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* One CM6 binding per keymap row, carrying each platform's own chord.
|
|
217
|
+
*
|
|
218
|
+
* `key` is the Windows/Linux chord, `mac` the macOS one, and `win` the Windows
|
|
219
|
+
* override for the single row that has one (`redo` is `Ctrl-Y` there and
|
|
220
|
+
* `Ctrl-Shift-Z` on Linux). CM6 falls back to `key` when a platform field is
|
|
221
|
+
* absent, so a row without an override needs nothing extra.
|
|
222
|
+
*/
|
|
223
|
+
function bindingsFor(onCommand?: (command: EditorCommand) => void): KeyBinding[] {
|
|
224
|
+
const bindings: KeyBinding[] = [];
|
|
225
|
+
for (const binding of EDITOR_KEY_BINDINGS) {
|
|
226
|
+
const other = codeMirrorKey(binding.other);
|
|
227
|
+
const mac = codeMirrorKey(binding.mac);
|
|
228
|
+
const win = binding.windows ? codeMirrorKey(binding.windows) : null;
|
|
229
|
+
if (!other && !mac) continue;
|
|
230
|
+
const command = binding.command;
|
|
231
|
+
bindings.push({
|
|
232
|
+
key: other ?? undefined,
|
|
233
|
+
mac: mac ?? undefined,
|
|
234
|
+
win: win ?? undefined,
|
|
235
|
+
run: (view) => runSurfaceCommand(command, view, onCommand),
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
return bindings;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Tab indents and ⇧Tab outdents, exactly as `indentWithTab` does.
|
|
243
|
+
*
|
|
244
|
+
* Written out rather than imported so the row can be dropped when the surface is
|
|
245
|
+
* read-only: a viewer that still indented would edit the document. Leaving the
|
|
246
|
+
* editor with the keyboard is CM6's own answer — `Escape` arms a two-second tab
|
|
247
|
+
* focus mode, after which `Tab` moves focus instead of indenting.
|
|
248
|
+
*/
|
|
249
|
+
const TAB_BINDINGS: KeyBinding[] = [
|
|
250
|
+
{ key: 'Tab', run: indentMore as Command, shift: indentLess as Command },
|
|
251
|
+
];
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* The editor's own keymap, as one extension.
|
|
255
|
+
*
|
|
256
|
+
* CM6's `defaultKeymap` is APPENDED rather than omitted: it is what makes the
|
|
257
|
+
* ordinary editing keys work — Enter, Backspace, arrows, Home/End, `Mod-End` to
|
|
258
|
+
* the document end, word-wise movement — and none of that is a Kabmo command.
|
|
259
|
+
* Appending it AFTER our rows is what keeps a chord the table claims from being
|
|
260
|
+
* shadowed by a CM6 default (`Mod-a` is a good example: both mean "select all",
|
|
261
|
+
* but only one of them also moves the selection the way the palette describes).
|
|
262
|
+
*/
|
|
263
|
+
export function surfaceKeymap(onCommand?: (command: EditorCommand) => void, readOnly = false): Extension {
|
|
264
|
+
const rows = readOnly ? [] : TAB_BINDINGS;
|
|
265
|
+
return keymap.of([...bindingsFor(onCommand), ...rows, ...defaultKeymap]);
|
|
266
|
+
}
|
|
267
|
+
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Find matches, as a decoration field.
|
|
8
|
+
*
|
|
9
|
+
* The old surface spliced `<mark>` elements into the highlighter's own HTML, so
|
|
10
|
+
* every find result forced a re-tokenize of the window and the marks could not
|
|
11
|
+
* exist without the token layer. A field is the CM6 shape: the ranges are state,
|
|
12
|
+
* the decorations are derived from it, and a keystroke in the query costs one
|
|
13
|
+
* rebuild of the ranges instead of a second trip through Shiki.
|
|
14
|
+
*
|
|
15
|
+
* The geometry rules are inherited from that markup layer and are still
|
|
16
|
+
* load-bearing: `<mark>` had a background and no padding, and the current match
|
|
17
|
+
* is marked with horizontal edges only. A match routinely spans several tokens,
|
|
18
|
+
* so any decoration that draws vertical edges would paint a seam in the middle
|
|
19
|
+
* of it.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { RangeSetBuilder, StateEffect, StateField, type Text } from '@codemirror/state';
|
|
23
|
+
import { Decoration, EditorView, type DecorationSet } from '@codemirror/view';
|
|
24
|
+
|
|
25
|
+
/** A document-offset range; the editor's own `TextRange` shape. */
|
|
26
|
+
export interface MarkRange {
|
|
27
|
+
start: number;
|
|
28
|
+
end: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface FindMarkState {
|
|
32
|
+
marks: readonly MarkRange[];
|
|
33
|
+
/** Index into `marks` of the match the caret is aimed at, or -1. */
|
|
34
|
+
current: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Replace the whole mark set. */
|
|
38
|
+
export const setFindMarks = StateEffect.define<FindMarkState>();
|
|
39
|
+
|
|
40
|
+
const MATCH = Decoration.mark({ class: 'find-match' });
|
|
41
|
+
const CURRENT = Decoration.mark({ class: 'find-match', attributes: { 'data-find-current': '' } });
|
|
42
|
+
|
|
43
|
+
function build(state: FindMarkState, doc: Text): DecorationSet {
|
|
44
|
+
if (state.marks.length === 0) return Decoration.none;
|
|
45
|
+
const builder = new RangeSetBuilder<Decoration>();
|
|
46
|
+
for (let index = 0; index < state.marks.length; index++) {
|
|
47
|
+
const mark = state.marks[index];
|
|
48
|
+
const from = Math.max(0, Math.min(mark.start, doc.length));
|
|
49
|
+
const to = Math.max(from, Math.min(mark.end, doc.length));
|
|
50
|
+
if (to === from) continue;
|
|
51
|
+
builder.add(from, to, index === state.current ? CURRENT : MATCH);
|
|
52
|
+
}
|
|
53
|
+
return builder.finish();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The mark set as editor state.
|
|
58
|
+
*
|
|
59
|
+
* A document edit MAPS the existing decorations rather than dropping them: the
|
|
60
|
+
* owner recomputes the list on the next render anyway (`matches` is derived from
|
|
61
|
+
* the live buffer), and mapping keeps the highlights roughly right for the frame
|
|
62
|
+
* in between instead of blinking off on every keystroke.
|
|
63
|
+
*/
|
|
64
|
+
export const findMarksField = StateField.define<DecorationSet>({
|
|
65
|
+
create: () => Decoration.none,
|
|
66
|
+
update(marks, transaction) {
|
|
67
|
+
for (const effect of transaction.effects) {
|
|
68
|
+
if (effect.is(setFindMarks)) return build(effect.value, transaction.state.doc);
|
|
69
|
+
}
|
|
70
|
+
if (transaction.docChanged) return marks.map(transaction.changes);
|
|
71
|
+
return marks;
|
|
72
|
+
},
|
|
73
|
+
provide: (field) => EditorView.decorations.from(field),
|
|
74
|
+
});
|