@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,102 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* The scroll-container walk, shared by the editor's lazy window and the find
|
|
8
|
-
* widget's reveal.
|
|
9
|
-
*
|
|
10
|
-
* The two must agree: one resolves which lines to draw from the visible band,
|
|
11
|
-
* the other moves that band. Two copies of this walk would let the widget scroll
|
|
12
|
-
* a container the window never reads — the match would be marked somewhere
|
|
13
|
-
* nobody can see. Exercised against a real DOM (happy-dom) because the answer is
|
|
14
|
-
* a computed style.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
|
|
18
|
-
import { Window } from 'happy-dom';
|
|
19
|
-
|
|
20
|
-
import { findScrollContainer } from '@/client/hooks/editor/scroll-container';
|
|
21
|
-
|
|
22
|
-
const installed: Record<string, unknown> = {};
|
|
23
|
-
const displaced: Record<string, unknown> = {};
|
|
24
|
-
|
|
25
|
-
beforeAll(() => {
|
|
26
|
-
const win = new Window({ url: 'http://localhost' });
|
|
27
|
-
for (const key of ['window', 'document', 'Node', 'Element', 'HTMLElement', 'getComputedStyle']) {
|
|
28
|
-
displaced[key] = (globalThis as Record<string, unknown>)[key];
|
|
29
|
-
installed[key] = (win as unknown as Record<string, unknown>)[key];
|
|
30
|
-
(globalThis as Record<string, unknown>)[key] = installed[key];
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
afterAll(() => {
|
|
35
|
-
for (const key of Object.keys(installed)) {
|
|
36
|
-
if (displaced[key] === undefined) delete (globalThis as Record<string, unknown>)[key];
|
|
37
|
-
else (globalThis as Record<string, unknown>)[key] = displaced[key];
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
/** An element whose computed style reports `overflow-y`, with a scrollable height. */
|
|
42
|
-
function makeElement(overflowY: string, scrollable: boolean): HTMLElement {
|
|
43
|
-
const element = document.createElement('div');
|
|
44
|
-
element.style.overflowY = overflowY;
|
|
45
|
-
if (scrollable) {
|
|
46
|
-
// happy-dom reports clientHeight 0, so the walk's `scrollHeight > clientHeight`
|
|
47
|
-
// test needs a stubbed pair to describe a container that can actually scroll.
|
|
48
|
-
Object.defineProperty(element, 'scrollHeight', { value: 400, configurable: true });
|
|
49
|
-
Object.defineProperty(element, 'clientHeight', { value: 100, configurable: true });
|
|
50
|
-
}
|
|
51
|
-
return element;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
describe('findScrollContainer', () => {
|
|
55
|
-
test('finds the nearest ancestor that scrolls', () => {
|
|
56
|
-
const outer = makeElement('auto', true);
|
|
57
|
-
const inner = makeElement('visible', false);
|
|
58
|
-
const surface = document.createElement('div');
|
|
59
|
-
inner.appendChild(surface);
|
|
60
|
-
outer.appendChild(inner);
|
|
61
|
-
document.body.appendChild(outer);
|
|
62
|
-
|
|
63
|
-
expect(findScrollContainer(surface)).toBe(outer);
|
|
64
|
-
outer.remove();
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test('skips a container that cannot actually scroll', () => {
|
|
68
|
-
// `overflow: auto` on a box whose content fits is not a scroller: returning
|
|
69
|
-
// it would make `revealOffset` write scrollTop onto a box that ignores it.
|
|
70
|
-
const outer = makeElement('auto', true);
|
|
71
|
-
const fitted = makeElement('auto', false);
|
|
72
|
-
const surface = document.createElement('div');
|
|
73
|
-
fitted.appendChild(surface);
|
|
74
|
-
outer.appendChild(fitted);
|
|
75
|
-
document.body.appendChild(outer);
|
|
76
|
-
|
|
77
|
-
expect(findScrollContainer(surface)).toBe(outer);
|
|
78
|
-
outer.remove();
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test('reports nothing when no ancestor scrolls', () => {
|
|
82
|
-
const plain = document.createElement('div');
|
|
83
|
-
const surface = document.createElement('div');
|
|
84
|
-
plain.appendChild(surface);
|
|
85
|
-
document.body.appendChild(plain);
|
|
86
|
-
|
|
87
|
-
expect(findScrollContainer(surface)).toBeNull();
|
|
88
|
-
plain.remove();
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test('prefers the nearest scroller, so a nested panel scrolls itself', () => {
|
|
92
|
-
const outer = makeElement('auto', true);
|
|
93
|
-
const inner = makeElement('scroll', true);
|
|
94
|
-
const surface = document.createElement('div');
|
|
95
|
-
inner.appendChild(surface);
|
|
96
|
-
outer.appendChild(inner);
|
|
97
|
-
document.body.appendChild(outer);
|
|
98
|
-
|
|
99
|
-
expect(findScrollContainer(surface)).toBe(inner);
|
|
100
|
-
outer.remove();
|
|
101
|
-
});
|
|
102
|
-
});
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* The ancestor a code surface actually scrolls inside.
|
|
8
|
-
*
|
|
9
|
-
* The lazy editor window and the find widget's "scroll to this match" both need
|
|
10
|
-
* this answer, and they must agree: one resolves which lines to draw from the
|
|
11
|
-
* visible band, the other moves that band. Two copies of the walk would let the
|
|
12
|
-
* widget scroll a container the window never reads.
|
|
13
|
-
*
|
|
14
|
-
* Walks up rather than taking a ref: the surface is mounted under different
|
|
15
|
-
* scrollers (the desktop editor panel, the phone's full-screen editor) and is
|
|
16
|
-
* also mounted inside a page that scrolls itself, which is the `null` case.
|
|
17
|
-
*/
|
|
18
|
-
export function findScrollContainer(element: HTMLElement): HTMLElement | null {
|
|
19
|
-
for (let node = element.parentElement; node; node = node.parentElement) {
|
|
20
|
-
const overflowY = getComputedStyle(node).overflowY;
|
|
21
|
-
if ((overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay') && node.scrollHeight > node.clientHeight) {
|
|
22
|
-
return node;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
|
|
2
|
-
import type { RefObject } from 'preact';
|
|
3
|
-
|
|
4
|
-
import { codeWindow, firstWindow, resolveWindow, sameWindow, type CodeWindow, type LineGeometry, type LineWindow } from '@/shared/lib/code/lazy-window';
|
|
5
|
-
import { findScrollContainer } from '@/client/hooks/editor/scroll-container';
|
|
6
|
-
|
|
7
|
-
export interface UseCodeWindowOptions {
|
|
8
|
-
/** The surface's root box: the frame every line offset in `geometry` is measured from. */
|
|
9
|
-
rootRef: RefObject<HTMLElement | null>;
|
|
10
|
-
/** Lines in the document on screen. */
|
|
11
|
-
lineCount: number;
|
|
12
|
-
/** Document geometry, or null until the wrap/row measurement lands. */
|
|
13
|
-
geometry: LineGeometry | null;
|
|
14
|
-
/** False for short documents (they render whole) and when geometry cannot be measured. */
|
|
15
|
-
enabled: boolean;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* The lines visible inside the scroll container `root` lives in.
|
|
20
|
-
*
|
|
21
|
-
* Both rectangles are read in viewport coordinates, so this needs no knowledge
|
|
22
|
-
* of scroll offsets: the part of the editor's box the container actually shows
|
|
23
|
-
* is exactly the band between the two.
|
|
24
|
-
*/
|
|
25
|
-
function visibleWindow(root: HTMLElement, geometry: LineGeometry): LineWindow {
|
|
26
|
-
const frameTop = root.getBoundingClientRect().top;
|
|
27
|
-
const container = findScrollContainer(root);
|
|
28
|
-
const viewTop = container ? container.getBoundingClientRect().top : 0;
|
|
29
|
-
const viewBottom = container
|
|
30
|
-
? container.getBoundingClientRect().bottom
|
|
31
|
-
: globalThis.innerHeight || geometry.rowHeight;
|
|
32
|
-
return resolveWindow(geometry, viewTop - frameTop, viewBottom - viewTop);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Rendering window for the lazy editor surface.
|
|
37
|
-
*
|
|
38
|
-
* A long document is highlighted, gutters and all, only where it can be seen;
|
|
39
|
-
* this hook owns which lines those are and re-resolves them on scroll, on
|
|
40
|
-
* resize, and whenever the geometry moves (a keystroke re-wraps a line). The
|
|
41
|
-
* scroll listener is a capturing one on the window: scroll events do not
|
|
42
|
-
* bubble, but they do reach a capturing listener, so the surface follows
|
|
43
|
-
* whichever ancestor scrolls — the desktop editor panel, the phone's
|
|
44
|
-
* full-screen editor, or the page.
|
|
45
|
-
*
|
|
46
|
-
* Returns null for a document that renders whole, which is the caller's cue to
|
|
47
|
-
* keep the plain, unwindowed surface.
|
|
48
|
-
*/
|
|
49
|
-
export function useCodeWindow({ rootRef, lineCount, geometry, enabled }: UseCodeWindowOptions): CodeWindow | null {
|
|
50
|
-
const [lineWindow, setLineWindow] = useState<LineWindow | null>(null);
|
|
51
|
-
|
|
52
|
-
const compute = useCallback(() => {
|
|
53
|
-
const root = rootRef.current;
|
|
54
|
-
if (!enabled || !root) return;
|
|
55
|
-
const next = geometry ? visibleWindow(root, geometry) : firstWindow(lineCount);
|
|
56
|
-
setLineWindow((previous) => (sameWindow(previous, next) ? previous : next));
|
|
57
|
-
}, [enabled, geometry, lineCount, rootRef]);
|
|
58
|
-
|
|
59
|
-
// Layout effect: the first window has to be resolved before the browser
|
|
60
|
-
// paints, or a long document flashes its untouched opening lines.
|
|
61
|
-
useLayoutEffect(() => {
|
|
62
|
-
compute();
|
|
63
|
-
}, [compute]);
|
|
64
|
-
|
|
65
|
-
// The listeners follow `enabled`, not `compute`: re-resolving on every
|
|
66
|
-
// keystroke must not tear down and rebuild the observer each time.
|
|
67
|
-
const computeRef = useRef(compute);
|
|
68
|
-
computeRef.current = compute;
|
|
69
|
-
useEffect(() => {
|
|
70
|
-
if (!enabled) return;
|
|
71
|
-
let frame = 0;
|
|
72
|
-
const schedule = () => {
|
|
73
|
-
if (frame) return;
|
|
74
|
-
frame = requestAnimationFrame(() => {
|
|
75
|
-
frame = 0;
|
|
76
|
-
computeRef.current();
|
|
77
|
-
});
|
|
78
|
-
};
|
|
79
|
-
window.addEventListener('scroll', schedule, { passive: true, capture: true });
|
|
80
|
-
window.addEventListener('resize', schedule);
|
|
81
|
-
const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(schedule);
|
|
82
|
-
const root = rootRef.current;
|
|
83
|
-
if (root) observer?.observe(root);
|
|
84
|
-
return () => {
|
|
85
|
-
if (frame) cancelAnimationFrame(frame);
|
|
86
|
-
window.removeEventListener('scroll', schedule, { capture: true });
|
|
87
|
-
window.removeEventListener('resize', schedule);
|
|
88
|
-
observer?.disconnect();
|
|
89
|
-
};
|
|
90
|
-
}, [enabled, rootRef]);
|
|
91
|
-
|
|
92
|
-
if (!enabled) return null;
|
|
93
|
-
if (!geometry) {
|
|
94
|
-
// First commit: the layout effect above lands the real window in the same
|
|
95
|
-
// frame, so this only bounds how much the opening render is allowed to draw.
|
|
96
|
-
const opening = firstWindow(lineCount);
|
|
97
|
-
return { ...opening, top: 0, bottom: 0, breakAtEnd: opening.end >= lineCount };
|
|
98
|
-
}
|
|
99
|
-
return codeWindow(geometry, lineWindow ?? firstWindow(geometry.lineCount));
|
|
100
|
-
}
|
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* The editor's undo stack.
|
|
8
|
-
*
|
|
9
|
-
* The rule under test is the one that was wrong: the re-seed exists for a value
|
|
10
|
-
* replaced from the OUTSIDE (a file switch, a refresh), but it also fired for
|
|
11
|
-
* the component's own edits — every keystroke pushed a second, identical entry,
|
|
12
|
-
* so the first ⌘Z restored the buffer to the state it was already in and undo
|
|
13
|
-
* looked broken until the second press. `markApplied` is what separates the two,
|
|
14
|
-
* and these assertions are what keeps it separating them.
|
|
15
|
-
*
|
|
16
|
-
* Runs against a real DOM (happy-dom) because it is a Preact hook holding a
|
|
17
|
-
* `<textarea>`; the modules below are imported dynamically, after those globals
|
|
18
|
-
* exist (Preact binds its environment at evaluation time).
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from 'bun:test';
|
|
22
|
-
import { Window } from 'happy-dom';
|
|
23
|
-
import type { h as PreactH, render as PreactRender } from 'preact';
|
|
24
|
-
import type { act as PreactAct } from 'preact/test-utils';
|
|
25
|
-
import type { EditorHistory, useEditorHistory as UseEditorHistoryHook } from '@/client/hooks/editor/use-editor-history';
|
|
26
|
-
|
|
27
|
-
let useEditorHistory: typeof UseEditorHistoryHook;
|
|
28
|
-
let h: typeof PreactH;
|
|
29
|
-
let render: typeof PreactRender;
|
|
30
|
-
let act: typeof PreactAct;
|
|
31
|
-
|
|
32
|
-
let api: EditorHistory | null = null;
|
|
33
|
-
let container: HTMLElement;
|
|
34
|
-
let textarea: HTMLTextAreaElement;
|
|
35
|
-
|
|
36
|
-
/** The buffer the hook is told is on screen, changed from the outside by a test. */
|
|
37
|
-
let externalValue = 'one';
|
|
38
|
-
|
|
39
|
-
function Harness() {
|
|
40
|
-
api = useEditorHistory({
|
|
41
|
-
value: externalValue,
|
|
42
|
-
inputRef: { current: textarea },
|
|
43
|
-
onValueChange: (next) => {
|
|
44
|
-
textarea.value = next;
|
|
45
|
-
},
|
|
46
|
-
});
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Flush the effect that watches `value`, with no timers: it resolves on a render. */
|
|
51
|
-
async function flush() {
|
|
52
|
-
for (let i = 0; i < 3; i++) {
|
|
53
|
-
await act(async () => {
|
|
54
|
-
for (let turn = 0; turn < 4; turn++) await Promise.resolve();
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function domGlobals(win: Window): Record<string, unknown> {
|
|
60
|
-
return {
|
|
61
|
-
window: win,
|
|
62
|
-
document: win.document,
|
|
63
|
-
navigator: win.navigator,
|
|
64
|
-
Node: win.Node,
|
|
65
|
-
Element: win.Element,
|
|
66
|
-
HTMLElement: win.HTMLElement,
|
|
67
|
-
HTMLTextAreaElement: win.HTMLTextAreaElement,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const installed: Record<string, unknown> = {};
|
|
72
|
-
const displaced: Record<string, unknown> = {};
|
|
73
|
-
|
|
74
|
-
beforeAll(async () => {
|
|
75
|
-
const win = new Window({ url: 'http://localhost' });
|
|
76
|
-
for (const [key, value] of Object.entries(domGlobals(win))) {
|
|
77
|
-
displaced[key] = (globalThis as Record<string, unknown>)[key];
|
|
78
|
-
installed[key] = value;
|
|
79
|
-
(globalThis as Record<string, unknown>)[key] = value;
|
|
80
|
-
}
|
|
81
|
-
({ useEditorHistory } = await import('@/client/hooks/editor/use-editor-history'));
|
|
82
|
-
({ h, render } = await import('preact'));
|
|
83
|
-
({ act } = await import('preact/test-utils'));
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
afterAll(() => {
|
|
87
|
-
for (const key of Object.keys(installed)) {
|
|
88
|
-
if (displaced[key] === undefined) delete (globalThis as Record<string, unknown>)[key];
|
|
89
|
-
else (globalThis as Record<string, unknown>)[key] = displaced[key];
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
beforeEach(async () => {
|
|
94
|
-
api = null;
|
|
95
|
-
externalValue = 'one';
|
|
96
|
-
container = document.createElement('div');
|
|
97
|
-
document.body.appendChild(container);
|
|
98
|
-
textarea = document.createElement('textarea');
|
|
99
|
-
textarea.value = 'one';
|
|
100
|
-
container.appendChild(textarea);
|
|
101
|
-
await act(async () => {
|
|
102
|
-
render(h(Harness, {}), container);
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
afterEach(() => {
|
|
107
|
-
if (container) render(null, container);
|
|
108
|
-
container?.remove();
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
describe('useEditorHistory', () => {
|
|
112
|
-
test('commit writes the buffer, the caret and the caller’s state', async () => {
|
|
113
|
-
await act(async () => {
|
|
114
|
-
api?.commit({ value: 'two', selectionStart: 2, selectionEnd: 2 });
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
expect(textarea.value).toBe('two');
|
|
118
|
-
expect(textarea.selectionStart).toBe(2);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test('an external value replacement is recorded, so undo reaches the old file', async () => {
|
|
122
|
-
// A file switch: the hook did not write this, so it becomes a history entry
|
|
123
|
-
// rather than silently replacing the stack.
|
|
124
|
-
externalValue = 'switched';
|
|
125
|
-
await act(async () => {
|
|
126
|
-
render(h(Harness, {}), container);
|
|
127
|
-
});
|
|
128
|
-
await flush();
|
|
129
|
-
|
|
130
|
-
await act(async () => {
|
|
131
|
-
api?.step('undo');
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
expect(textarea.value).toBe('one');
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
test('a value the hook itself wrote is NOT re-recorded', async () => {
|
|
138
|
-
// The bug this guards: an extra entry per keystroke made the first ⌘Z a
|
|
139
|
-
// no-op, because it restored the buffer to what was already on screen.
|
|
140
|
-
await act(async () => {
|
|
141
|
-
api?.commit({ value: 'two', selectionStart: 2, selectionEnd: 2 });
|
|
142
|
-
});
|
|
143
|
-
externalValue = 'two';
|
|
144
|
-
await act(async () => {
|
|
145
|
-
render(h(Harness, {}), container);
|
|
146
|
-
});
|
|
147
|
-
await flush();
|
|
148
|
-
|
|
149
|
-
await act(async () => {
|
|
150
|
-
api?.step('undo');
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
// One press reaches the ORIGINAL buffer; a duplicate entry would have
|
|
154
|
-
// restored 'two' again.
|
|
155
|
-
expect(textarea.value).toBe('one');
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
test('markApplied declares a browser edit as already recorded', async () => {
|
|
159
|
-
await act(async () => {
|
|
160
|
-
api?.markApplied('typed');
|
|
161
|
-
});
|
|
162
|
-
externalValue = 'typed';
|
|
163
|
-
await act(async () => {
|
|
164
|
-
render(h(Harness, {}), container);
|
|
165
|
-
});
|
|
166
|
-
await flush();
|
|
167
|
-
|
|
168
|
-
await act(async () => {
|
|
169
|
-
api?.step('undo');
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
// Nothing was pushed for `typed`, so there is nothing to step back to: the
|
|
173
|
-
// stack still holds only the initial record.
|
|
174
|
-
expect(textarea.value).toBe('one');
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
test('step at the ends of the stack is a no-op', async () => {
|
|
178
|
-
await act(async () => {
|
|
179
|
-
api?.step('undo');
|
|
180
|
-
});
|
|
181
|
-
const afterUndo = textarea.value;
|
|
182
|
-
await act(async () => {
|
|
183
|
-
api?.step('redo');
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
expect(afterUndo).toBe('one');
|
|
187
|
-
expect(textarea.value).toBe('one');
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
test('commitEdit supersedes the top entry, keeping its caret for the undo', async () => {
|
|
191
|
-
// A line command replaces a whole range at once; undo must return the caret
|
|
192
|
-
// to the selection the command replaced, not to where the edit ended.
|
|
193
|
-
textarea.selectionStart = 1;
|
|
194
|
-
textarea.selectionEnd = 3;
|
|
195
|
-
await act(async () => {
|
|
196
|
-
api?.commitEdit({ value: 'one-changed', selectionStart: 11, selectionEnd: 11 });
|
|
197
|
-
});
|
|
198
|
-
await act(async () => {
|
|
199
|
-
api?.step('undo');
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
expect(textarea.value).toBe('one');
|
|
203
|
-
expect(textarea.selectionStart).toBe(1);
|
|
204
|
-
expect(textarea.selectionEnd).toBe(3);
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
test('the exposed buffer is never a render behind', async () => {
|
|
208
|
-
// `handleChange` diffs against this; a value that lagged a keystroke read a
|
|
209
|
-
// one-character edit as a whole-word replacement.
|
|
210
|
-
await act(async () => {
|
|
211
|
-
api?.commit({ value: 'fresh', selectionStart: 5, selectionEnd: 5 });
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
expect(api?.buffer).toBe('fresh');
|
|
215
|
-
});
|
|
216
|
-
});
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* The editor's undo stack, wired to the DOM node.
|
|
8
|
-
*
|
|
9
|
-
* `history.ts` is the pure bookkeeping (push, merge, step, cap); this is the
|
|
10
|
-
* part that has to touch the `<textarea>`: committing an edit as one entry,
|
|
11
|
-
* restoring a stepped record, and re-seeding the stack when the value is
|
|
12
|
-
* replaced from the OUTSIDE.
|
|
13
|
-
*
|
|
14
|
-
* The re-seed is the subtle one. It exists for a file switch or a refresh, but
|
|
15
|
-
* a naive `[value]` effect also fires for the component's own edits — every
|
|
16
|
-
* keystroke then pushed a second, identical entry, so the first ⌘Z restored the
|
|
17
|
-
* buffer to the state it was already in and undo looked broken until the second
|
|
18
|
-
* press (and the no-op `onValueChange` truncated the redo stack). The `applied`
|
|
19
|
-
* ref is what distinguishes "someone else replaced the value" from "we did".
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import { useEffect, useRef } from 'preact/hooks';
|
|
23
|
-
|
|
24
|
-
import { createHistory, pushEdit, pushRecord, stepHistory, type History, type HistoryRecord } from '@/shared/lib/code/editor/history';
|
|
25
|
-
|
|
26
|
-
/** The fields every write path needs: a buffer and where the caret goes. */
|
|
27
|
-
export type HistoryEdit = Pick<HistoryRecord, 'value' | 'selectionStart' | 'selectionEnd'>;
|
|
28
|
-
|
|
29
|
-
export interface EditorHistory {
|
|
30
|
-
/** The stack itself, for the typing-burst merge in the change handler. */
|
|
31
|
-
history: History;
|
|
32
|
-
/**
|
|
33
|
-
* The buffer as this component last knew it — its own writes included, and
|
|
34
|
-
* never a render behind. Diffing a browser edit has to start from here: the
|
|
35
|
-
* `value` prop lags a keystroke, and a stale `before` reads a one-character
|
|
36
|
-
* edit as a whole-word replacement.
|
|
37
|
-
*/
|
|
38
|
-
readonly buffer: string;
|
|
39
|
-
/** Commit an edit as ONE undoable entry. */
|
|
40
|
-
commit: (edit: HistoryEdit) => void;
|
|
41
|
-
/**
|
|
42
|
-
* Commit an edit that SUPERSEDES the entry on top, keeping that entry's
|
|
43
|
-
* caret so undo returns to the selection the edit replaced.
|
|
44
|
-
*/
|
|
45
|
-
commitEdit: (edit: HistoryEdit) => void;
|
|
46
|
-
/** Step the stack and restore the record; no-op at either end. */
|
|
47
|
-
step: (direction: 'undo' | 'redo') => void;
|
|
48
|
-
/**
|
|
49
|
-
* Declare a value the BROWSER produced as already applied, so the re-seed
|
|
50
|
-
* effect below does not record it a second time. The change handler calls
|
|
51
|
-
* this for every keystroke and every replicated multi-cursor edit.
|
|
52
|
-
*/
|
|
53
|
-
markApplied: (value: string) => void;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export interface UseEditorHistoryOptions {
|
|
57
|
-
/** The controlled buffer, used to detect an external replacement. */
|
|
58
|
-
value: string;
|
|
59
|
-
/** The live `<textarea>`, so an external replacement records its own caret. */
|
|
60
|
-
inputRef: { current: HTMLTextAreaElement | null };
|
|
61
|
-
/** Push the committed buffer to the caller. */
|
|
62
|
-
onValueChange: (value: string) => void;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function useEditorHistory({ value, inputRef, onValueChange }: UseEditorHistoryOptions): EditorHistory {
|
|
66
|
-
const historyRef = useRef<History>(createHistory());
|
|
67
|
-
/** The value this component itself last wrote — see the module note. */
|
|
68
|
-
const appliedRef = useRef<string | null>(null);
|
|
69
|
-
|
|
70
|
-
// Re-record when the controlled value is replaced from the outside
|
|
71
|
-
// (file switch, refresh) so undo does not jump between documents.
|
|
72
|
-
useEffect(() => {
|
|
73
|
-
const input = inputRef.current;
|
|
74
|
-
if (!input || appliedRef.current === value) return;
|
|
75
|
-
appliedRef.current = value;
|
|
76
|
-
pushRecord(historyRef.current, {
|
|
77
|
-
value: input.value,
|
|
78
|
-
selectionStart: input.selectionStart,
|
|
79
|
-
selectionEnd: input.selectionEnd,
|
|
80
|
-
timestamp: Date.now(),
|
|
81
|
-
});
|
|
82
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
83
|
-
}, [value]);
|
|
84
|
-
|
|
85
|
-
/** Put a buffer on screen without touching the stack. */
|
|
86
|
-
const write = (edit: HistoryEdit) => {
|
|
87
|
-
appliedRef.current = edit.value;
|
|
88
|
-
onValueChange(edit.value);
|
|
89
|
-
const input = inputRef.current;
|
|
90
|
-
if (!input) return;
|
|
91
|
-
input.value = edit.value;
|
|
92
|
-
input.selectionStart = edit.selectionStart;
|
|
93
|
-
input.selectionEnd = edit.selectionEnd;
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
return {
|
|
97
|
-
history: historyRef.current,
|
|
98
|
-
|
|
99
|
-
get buffer() {
|
|
100
|
-
return appliedRef.current ?? value;
|
|
101
|
-
},
|
|
102
|
-
|
|
103
|
-
commit: (edit) => {
|
|
104
|
-
pushEdit(historyRef.current, { ...edit, timestamp: Date.now() });
|
|
105
|
-
write(edit);
|
|
106
|
-
},
|
|
107
|
-
|
|
108
|
-
commitEdit: (edit) => {
|
|
109
|
-
const history = historyRef.current;
|
|
110
|
-
const last = history.stack[history.offset];
|
|
111
|
-
const input = inputRef.current;
|
|
112
|
-
if (last && input) {
|
|
113
|
-
// The entry on top is about to be superseded: keep where its caret was,
|
|
114
|
-
// so undo returns to the selection this edit replaced.
|
|
115
|
-
history.stack[history.offset] = {
|
|
116
|
-
...last,
|
|
117
|
-
selectionStart: input.selectionStart,
|
|
118
|
-
selectionEnd: input.selectionEnd,
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
pushEdit(history, { ...edit, timestamp: Date.now() });
|
|
122
|
-
write(edit);
|
|
123
|
-
},
|
|
124
|
-
|
|
125
|
-
step: (direction) => {
|
|
126
|
-
const record = stepHistory(historyRef.current, direction);
|
|
127
|
-
if (record) write(record);
|
|
128
|
-
},
|
|
129
|
-
|
|
130
|
-
markApplied: (next) => {
|
|
131
|
-
appliedRef.current = next;
|
|
132
|
-
},
|
|
133
|
-
};
|
|
134
|
-
}
|