@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
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* `useOccurrences` — the ranges behind ⌘D, ⌘⇧L and ⌘⇧D.
|
|
8
|
-
*
|
|
9
|
-
* The rules live in `shared/lib/code/editor/occurrences` and are tested there;
|
|
10
|
-
* what this file covers is the hook's own contract: the caller's copy wins
|
|
11
|
-
* (clearing the ranges in the panel must clear them here), every change is
|
|
12
|
-
* announced so the surface repaints, and the textarea is read rather than
|
|
13
|
-
* assumed.
|
|
14
|
-
*
|
|
15
|
-
* Runs against a real DOM (happy-dom) because it is a Preact hook; the modules
|
|
16
|
-
* below are imported dynamically, after those globals exist.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from 'bun:test';
|
|
20
|
-
import { Window } from 'happy-dom';
|
|
21
|
-
import type { h as PreactH, render as PreactRender } from 'preact';
|
|
22
|
-
import type { act as PreactAct } from 'preact/test-utils';
|
|
23
|
-
import type { OccurrencesApi, useOccurrences as UseOccurrencesHook } from '@/client/hooks/editor/use-occurrences';
|
|
24
|
-
import type { TextRange } from '@/shared/lib/code/editor/commands';
|
|
25
|
-
|
|
26
|
-
let useOccurrences: typeof UseOccurrencesHook;
|
|
27
|
-
let h: typeof PreactH;
|
|
28
|
-
let render: typeof PreactRender;
|
|
29
|
-
let act: typeof PreactAct;
|
|
30
|
-
|
|
31
|
-
let api: OccurrencesApi | null = null;
|
|
32
|
-
let container: HTMLElement;
|
|
33
|
-
let textarea: HTMLTextAreaElement;
|
|
34
|
-
/** What the caller passes in as its own copy, changed per test. */
|
|
35
|
-
let controlled: TextRange[] | undefined;
|
|
36
|
-
const announced: Array<readonly TextRange[]> = [];
|
|
37
|
-
const selections: Array<[number, number]> = [];
|
|
38
|
-
|
|
39
|
-
function Harness() {
|
|
40
|
-
api = useOccurrences({
|
|
41
|
-
controlled,
|
|
42
|
-
onChange: (ranges) => announced.push(ranges),
|
|
43
|
-
});
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function flush() {
|
|
48
|
-
for (let i = 0; i < 3; i++) {
|
|
49
|
-
await act(async () => {
|
|
50
|
-
for (let turn = 0; turn < 4; turn++) await Promise.resolve();
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async function rerender() {
|
|
56
|
-
await act(async () => {
|
|
57
|
-
render(h(Harness, {}), container);
|
|
58
|
-
});
|
|
59
|
-
await flush();
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function select(start: number, end = start) {
|
|
63
|
-
textarea.selectionStart = start;
|
|
64
|
-
textarea.selectionEnd = end;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Stands in for the editor's own `select`: it MOVES the textarea's selection and
|
|
69
|
-
* records it. The hook reads the textarea, so a callback that only recorded the
|
|
70
|
-
* range would leave every later press looking at the original caret.
|
|
71
|
-
*/
|
|
72
|
-
function selectingSurface() {
|
|
73
|
-
return (start: number, end: number) => {
|
|
74
|
-
selections.push([start, end]);
|
|
75
|
-
select(start, end);
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function domGlobals(win: Window): Record<string, unknown> {
|
|
80
|
-
return {
|
|
81
|
-
window: win,
|
|
82
|
-
document: win.document,
|
|
83
|
-
navigator: win.navigator,
|
|
84
|
-
Node: win.Node,
|
|
85
|
-
Element: win.Element,
|
|
86
|
-
HTMLElement: win.HTMLElement,
|
|
87
|
-
HTMLTextAreaElement: win.HTMLTextAreaElement,
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const installed: Record<string, unknown> = {};
|
|
92
|
-
const displaced: Record<string, unknown> = {};
|
|
93
|
-
|
|
94
|
-
beforeAll(async () => {
|
|
95
|
-
const win = new Window({ url: 'http://localhost' });
|
|
96
|
-
for (const [key, value] of Object.entries(domGlobals(win))) {
|
|
97
|
-
displaced[key] = (globalThis as Record<string, unknown>)[key];
|
|
98
|
-
installed[key] = value;
|
|
99
|
-
(globalThis as Record<string, unknown>)[key] = value;
|
|
100
|
-
}
|
|
101
|
-
({ useOccurrences } = await import('@/client/hooks/editor/use-occurrences'));
|
|
102
|
-
({ h, render } = await import('preact'));
|
|
103
|
-
({ act } = await import('preact/test-utils'));
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
afterAll(() => {
|
|
107
|
-
for (const key of Object.keys(installed)) {
|
|
108
|
-
if (displaced[key] === undefined) delete (globalThis as Record<string, unknown>)[key];
|
|
109
|
-
else (globalThis as Record<string, unknown>)[key] = displaced[key];
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
beforeEach(async () => {
|
|
114
|
-
api = null;
|
|
115
|
-
controlled = undefined;
|
|
116
|
-
announced.length = 0;
|
|
117
|
-
selections.length = 0;
|
|
118
|
-
container = document.createElement('div');
|
|
119
|
-
document.body.appendChild(container);
|
|
120
|
-
textarea = document.createElement('textarea');
|
|
121
|
-
textarea.value = 'alpha beta alpha gamma alpha';
|
|
122
|
-
container.appendChild(textarea);
|
|
123
|
-
await rerender();
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
afterEach(() => {
|
|
127
|
-
if (container) render(null, container);
|
|
128
|
-
container?.remove();
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
describe('useOccurrences', () => {
|
|
132
|
-
test('starts with no extra ranges', () => {
|
|
133
|
-
expect(api?.ranges).toEqual([]);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
test('⌘D selects the word first, then grows one occurrence per press', async () => {
|
|
137
|
-
select(2);
|
|
138
|
-
await act(async () => {
|
|
139
|
-
api?.selectNext(textarea, selectingSurface());
|
|
140
|
-
});
|
|
141
|
-
expect(selections.at(-1)).toEqual([0, 5]);
|
|
142
|
-
expect(api?.ranges).toEqual([]);
|
|
143
|
-
|
|
144
|
-
await act(async () => {
|
|
145
|
-
api?.selectNext(textarea, selectingSurface());
|
|
146
|
-
});
|
|
147
|
-
expect(api?.ranges).toEqual([{ start: 11, end: 16 }]);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
test('⌘⇧L takes every occurrence, primary first', async () => {
|
|
151
|
-
select(0, 5);
|
|
152
|
-
await act(async () => {
|
|
153
|
-
api?.selectAll(textarea, selectingSurface());
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
expect(selections.at(-1)).toEqual([0, 5]);
|
|
157
|
-
expect(api?.ranges).toEqual([
|
|
158
|
-
{ start: 11, end: 16 },
|
|
159
|
-
{ start: 23, end: 28 },
|
|
160
|
-
]);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
test('every change is announced so the surface can repaint', async () => {
|
|
164
|
-
select(0, 5);
|
|
165
|
-
await act(async () => {
|
|
166
|
-
api?.selectAll(textarea, () => {});
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
expect(announced.at(-1)?.length).toBe(2);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
test('clear drops every range and says so', async () => {
|
|
173
|
-
select(0, 5);
|
|
174
|
-
await act(async () => {
|
|
175
|
-
api?.selectAll(textarea, () => {});
|
|
176
|
-
});
|
|
177
|
-
await act(async () => {
|
|
178
|
-
api?.clear();
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
expect(api?.ranges).toEqual([]);
|
|
182
|
-
expect(announced.at(-1)).toEqual([]);
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
test('the caller’s copy is authoritative', async () => {
|
|
186
|
-
// The panel can clear the ranges without the editor asking (a file switch,
|
|
187
|
-
// a save); the hook has to follow, not keep its own stale list.
|
|
188
|
-
controlled = [{ start: 1, end: 3 }];
|
|
189
|
-
await rerender();
|
|
190
|
-
|
|
191
|
-
expect(api?.ranges).toEqual([{ start: 1, end: 3 }]);
|
|
192
|
-
|
|
193
|
-
controlled = [];
|
|
194
|
-
await rerender();
|
|
195
|
-
|
|
196
|
-
expect(api?.ranges).toEqual([]);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
test('⌘⇧D adds a caret below on the same column', async () => {
|
|
200
|
-
textarea.value = 'one\ntwo\nthree';
|
|
201
|
-
select(1);
|
|
202
|
-
await act(async () => {
|
|
203
|
-
api?.insertCursorBelow(textarea);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
expect(api?.ranges).toEqual([{ start: 5, end: 5 }]);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
test('⌘⇧D on the last line adds nothing', async () => {
|
|
210
|
-
textarea.value = 'one\ntwo';
|
|
211
|
-
select(5);
|
|
212
|
-
await act(async () => {
|
|
213
|
-
api?.insertCursorBelow(textarea);
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
expect(api?.ranges).toEqual([]);
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
test('replicate reports nothing without extra ranges', () => {
|
|
220
|
-
expect(api?.replicate('abc', 'abXc', { start: 2, end: 2 })).toBeNull();
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
test('replicate replays the browser’s edit at every extra range', async () => {
|
|
224
|
-
select(0, 5);
|
|
225
|
-
await act(async () => {
|
|
226
|
-
api?.selectAll(textarea, () => {});
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
// The browser replaced `alpha` at 0..5 with `Z`; the other two follow.
|
|
230
|
-
const applied = api?.replicate('alpha beta alpha gamma alpha', 'Z beta alpha gamma alpha', { start: 0, end: 5 });
|
|
231
|
-
|
|
232
|
-
expect(applied?.value).toBe('Z beta Z gamma Z');
|
|
233
|
-
expect(applied?.caret).toBe(1);
|
|
234
|
-
expect(applied?.rest.length).toBe(2);
|
|
235
|
-
});
|
|
236
|
-
});
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* The hook that owns the editor's extra selection ranges.
|
|
8
|
-
*
|
|
9
|
-
* It is a thin shell over `shared/lib/code/editor/occurrences`: the rules live
|
|
10
|
-
* there as pure functions, and this holds the ranges in a ref (the keydown and
|
|
11
|
-
* `input` handlers read them synchronously, inside one event) and tells the
|
|
12
|
-
* caller through `onChange` so the surface can paint them.
|
|
13
|
-
*
|
|
14
|
-
* The split is deliberate: a hook that decides where a range goes cannot be
|
|
15
|
-
* tested without a DOM, and every rule here is one a user notices when it is
|
|
16
|
-
* wrong.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { useRef } from 'preact/hooks';
|
|
20
|
-
|
|
21
|
-
import {
|
|
22
|
-
growToNextOccurrence,
|
|
23
|
-
offsetBelow,
|
|
24
|
-
replicateAtRanges,
|
|
25
|
-
selectAllOccurrences,
|
|
26
|
-
} from '@/shared/lib/code/editor/occurrences';
|
|
27
|
-
import type { TextRange } from '@/shared/lib/code/editor/commands';
|
|
28
|
-
|
|
29
|
-
export interface UseOccurrencesOptions {
|
|
30
|
-
/** Called with the extra ranges whenever they change, so the surface repaints. */
|
|
31
|
-
onChange: (ranges: readonly TextRange[]) => void;
|
|
32
|
-
/** The caller's copy of the ranges — clearing them there must clear them here. */
|
|
33
|
-
controlled?: readonly TextRange[];
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface OccurrencesApi {
|
|
37
|
-
/** The extra ranges, excluding the textarea's own (primary) selection. */
|
|
38
|
-
readonly ranges: readonly TextRange[];
|
|
39
|
-
/** Replace the ranges and notify the surface. */
|
|
40
|
-
set: (ranges: readonly TextRange[]) => void;
|
|
41
|
-
/** Drop every extra range. */
|
|
42
|
-
clear: () => void;
|
|
43
|
-
/** ⌘D — grow the primary selection to the next occurrence of what it covers. */
|
|
44
|
-
selectNext: (input: HTMLTextAreaElement, select: (start: number, end: number) => void) => void;
|
|
45
|
-
/** ⌘⇧L — select every occurrence of the selection/word at the caret. */
|
|
46
|
-
selectAll: (input: HTMLTextAreaElement, select: (start: number, end: number) => void) => void;
|
|
47
|
-
/** ⌘⇧D — add an empty caret on the line below. */
|
|
48
|
-
insertCursorBelow: (input: HTMLTextAreaElement) => void;
|
|
49
|
-
/**
|
|
50
|
-
* Replay the browser's edit at every extra range. Returns the buffer and
|
|
51
|
-
* caret the caller should commit, or null when there is nothing to replicate.
|
|
52
|
-
*/
|
|
53
|
-
replicate: (before: string, after: string, primaryBefore: TextRange) => { value: string; caret: number; rest: TextRange[] } | null;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function useOccurrences({ onChange, controlled }: UseOccurrencesOptions): OccurrencesApi {
|
|
57
|
-
const ref = useRef<readonly TextRange[]>([]);
|
|
58
|
-
// The caller may clear the ranges without the editor asking (a file switch, a
|
|
59
|
-
// save), so its copy is mirrored into the ref the handlers read.
|
|
60
|
-
if (controlled && controlled !== ref.current) ref.current = controlled;
|
|
61
|
-
|
|
62
|
-
const set = (ranges: readonly TextRange[]) => {
|
|
63
|
-
ref.current = ranges;
|
|
64
|
-
onChange(ranges);
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
return {
|
|
68
|
-
get ranges() {
|
|
69
|
-
return ref.current;
|
|
70
|
-
},
|
|
71
|
-
set,
|
|
72
|
-
clear: () => set([]),
|
|
73
|
-
|
|
74
|
-
selectNext: (input, select) => {
|
|
75
|
-
const current: TextRange = { start: input.selectionStart, end: input.selectionEnd };
|
|
76
|
-
const next = growToNextOccurrence(input.value, current, ref.current);
|
|
77
|
-
if (!next) return;
|
|
78
|
-
select(next.primary.start, next.primary.end);
|
|
79
|
-
set(next.extras);
|
|
80
|
-
},
|
|
81
|
-
|
|
82
|
-
selectAll: (input, select) => {
|
|
83
|
-
const current: TextRange = { start: input.selectionStart, end: input.selectionEnd };
|
|
84
|
-
const all = selectAllOccurrences(input.value, current);
|
|
85
|
-
if (!all) return;
|
|
86
|
-
select(all.primary.start, all.primary.end);
|
|
87
|
-
set(all.extras);
|
|
88
|
-
},
|
|
89
|
-
|
|
90
|
-
insertCursorBelow: (input) => {
|
|
91
|
-
const target = offsetBelow(input.value, input.selectionStart);
|
|
92
|
-
if (target === null) return;
|
|
93
|
-
set([...ref.current, { start: target, end: target }]);
|
|
94
|
-
},
|
|
95
|
-
|
|
96
|
-
replicate: (before, after, primaryBefore) => replicateAtRanges(before, after, primaryBefore, ref.current),
|
|
97
|
-
};
|
|
98
|
-
}
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { describe, expect, test } from 'bun:test';
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
commentSyntaxFor,
|
|
10
|
-
diffEdit,
|
|
11
|
-
nextOccurrence,
|
|
12
|
-
occurrenceRanges,
|
|
13
|
-
replicateEdit,
|
|
14
|
-
toggleLineComment,
|
|
15
|
-
wordRangeAt,
|
|
16
|
-
} from '@/shared/lib/code/editor/commands';
|
|
17
|
-
|
|
18
|
-
describe('wordRangeAt', () => {
|
|
19
|
-
test('selects the word the caret sits inside or at the end of', () => {
|
|
20
|
-
expect(wordRangeAt('const alpha = 1', 8)).toEqual({ start: 6, end: 11 });
|
|
21
|
-
// A caret just after the word still selects it — that is where ⌘D is
|
|
22
|
-
// pressed when the user has just typed the identifier.
|
|
23
|
-
expect(wordRangeAt('const alpha = 1', 11)).toEqual({ start: 6, end: 11 });
|
|
24
|
-
expect(wordRangeAt('const alpha = 1', 6)).toEqual({ start: 6, end: 11 });
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test('treats `$` and `_` as word characters', () => {
|
|
28
|
-
expect(wordRangeAt('$foo_bar baz', 3)).toEqual({ start: 0, end: 8 });
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test('a caret at the edge of a word takes that word, whitespace alone takes none', () => {
|
|
32
|
-
// Offset 5 in `alpha beta` is the space right after `alpha` — the position
|
|
33
|
-
// a caret sits at after typing the word, which is where ⌘D is pressed.
|
|
34
|
-
expect(wordRangeAt('alpha beta', 5)).toEqual({ start: 0, end: 5 });
|
|
35
|
-
// Whitespace with no word on either side: nothing to select.
|
|
36
|
-
expect(wordRangeAt(' foo', 0)).toBeNull();
|
|
37
|
-
expect(wordRangeAt('', 0)).toBeNull();
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
describe('occurrenceRanges and nextOccurrence', () => {
|
|
42
|
-
test('finds every occurrence in document order', () => {
|
|
43
|
-
expect(occurrenceRanges('alpha beta alpha', 'alpha')).toEqual([
|
|
44
|
-
{ start: 0, end: 5 },
|
|
45
|
-
{ start: 11, end: 16 },
|
|
46
|
-
]);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test('the next occurrence is the first one after the selection, then it wraps', () => {
|
|
50
|
-
const text = 'a a a';
|
|
51
|
-
const first = occurrenceRanges(text, 'a')[0];
|
|
52
|
-
expect(nextOccurrence(text, 'a', [first])).toEqual({ start: 2, end: 3 });
|
|
53
|
-
const all = occurrenceRanges(text, 'a');
|
|
54
|
-
expect(nextOccurrence(text, 'a', all)).toBeNull();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test('skips ranges that are already selected', () => {
|
|
58
|
-
const all = occurrenceRanges('a a a', 'a');
|
|
59
|
-
expect(nextOccurrence('a a a', 'a', [all[0], all[2]])).toEqual({ start: 2, end: 3 });
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
describe('diffEdit', () => {
|
|
64
|
-
test('recovers an insertion from two buffers', () => {
|
|
65
|
-
expect(diffEdit('abc', 'abXc')).toEqual({ start: 2, end: 2, inserted: 'X' });
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test('recovers a deletion', () => {
|
|
69
|
-
expect(diffEdit('abXc', 'abc')).toEqual({ start: 2, end: 3, inserted: '' });
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test('recovers a replacement that swallowed a selection', () => {
|
|
73
|
-
// `abc` selected and typed over with `Z`: the diff is one replacement, not
|
|
74
|
-
// a deletion followed by an insertion.
|
|
75
|
-
expect(diffEdit('hello world', 'hello Z')).toEqual({ start: 6, end: 11, inserted: 'Z' });
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test('a limit pins the reported region over the caret, not wherever the diff prefers', () => {
|
|
79
|
-
// On repetitive text a prefix/suffix diff is ambiguous — `aaXa` could be read
|
|
80
|
-
// as an insertion at offset 2 (where the diff finds it) or at offset 0 (where
|
|
81
|
-
// the caret was). The limit wins, and the diff stays exact either way.
|
|
82
|
-
const diff = diffEdit('aaa', 'aaXa', { start: 0, end: 0 });
|
|
83
|
-
|
|
84
|
-
expect(diff.start).toBeLessThanOrEqual(0);
|
|
85
|
-
expect(diff.end).toBeGreaterThanOrEqual(0);
|
|
86
|
-
expect(diff.start).toBe(0);
|
|
87
|
-
// Applying the reported edit must reproduce the buffer, whichever shape the
|
|
88
|
-
// limit forced: that is the property the caller depends on.
|
|
89
|
-
expect('aaa'.slice(0, diff.start) + diff.inserted + 'aaa'.slice(diff.end)).toBe('aaXa');
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
describe('replicateEdit', () => {
|
|
94
|
-
test('applies one insertion at every range, back to front', () => {
|
|
95
|
-
// Ranges 0..1 and 2..3 of `ab` — inserting `X` at both must not let the
|
|
96
|
-
// first insertion shift the second range's offsets.
|
|
97
|
-
const result = replicateEdit('abcd', [{ start: 0, end: 0 }, { start: 2, end: 2 }], 'X');
|
|
98
|
-
|
|
99
|
-
expect(result.value).toBe('XabXcd');
|
|
100
|
-
expect(result.carets).toEqual([1, 4]);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test('replaces the selected text at each range', () => {
|
|
104
|
-
const result = replicateEdit('foo bar foo', [{ start: 0, end: 3 }, { start: 8, end: 11 }], 'qux');
|
|
105
|
-
|
|
106
|
-
expect(result.value).toBe('qux bar qux');
|
|
107
|
-
expect(result.carets).toEqual([3, 11]);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
test('ranges given out of order are still applied in document order', () => {
|
|
111
|
-
const result = replicateEdit('abcd', [{ start: 2, end: 2 }, { start: 0, end: 0 }], 'X');
|
|
112
|
-
|
|
113
|
-
expect(result.value).toBe('XabXcd');
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
describe('toggleLineComment', () => {
|
|
118
|
-
test('comments the selected lines at their first non-space character', () => {
|
|
119
|
-
const result = toggleLineComment(' one\n two', { start: 0, end: 11 }, { line: '//' });
|
|
120
|
-
|
|
121
|
-
expect(result.value).toBe(' // one\n // two');
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test('uncomments when every touched line is already commented', () => {
|
|
125
|
-
const result = toggleLineComment('// one\n// two', { start: 0, end: 13 }, { line: '//' });
|
|
126
|
-
|
|
127
|
-
expect(result.value).toBe('one\ntwo');
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
test('a mixed selection comments every line rather than half-uncommenting', () => {
|
|
131
|
-
const result = toggleLineComment('// one\ntwo', { start: 0, end: 11 }, { line: '//' });
|
|
132
|
-
|
|
133
|
-
expect(result.value).toBe('// // one\n// two');
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
test('only the lines the selection touches are affected', () => {
|
|
137
|
-
const result = toggleLineComment('one\ntwo\nthree', { start: 4, end: 7 }, { line: '//' });
|
|
138
|
-
|
|
139
|
-
expect(result.value).toBe('one\n// two\nthree');
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test('a block-only language wraps the selection once', () => {
|
|
143
|
-
const result = toggleLineComment('color: red;', { start: 0, end: 11 }, commentSyntaxFor('css'));
|
|
144
|
-
|
|
145
|
-
expect(result.value).toBe('/* color: red; */');
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
test('the hash languages use their own token', () => {
|
|
149
|
-
expect(commentSyntaxFor('python')).toEqual({ line: '#' });
|
|
150
|
-
expect(commentSyntaxFor('yaml')).toEqual({ line: '#' });
|
|
151
|
-
expect(commentSyntaxFor('sql')).toEqual({ line: '--' });
|
|
152
|
-
// The default is the C-family token, which every other grammar here uses.
|
|
153
|
-
expect(commentSyntaxFor('typescript')).toEqual({ line: '//' });
|
|
154
|
-
});
|
|
155
|
-
});
|