ompchamber 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -5
- package/dist/client/index.html +1 -1
- package/dist/client/index.js +333 -333
- package/dist/client/static/js/{index-prbmx3yj.js → index-8t70jhyj.js} +643 -617
- package/dist/client/static/js/index-n4wkqj7t.css +1 -0
- package/package.json +1 -1
- package/src/client/components/common/code-editor/handler.test.ts +274 -0
- package/src/client/components/common/code-editor/handler.ts +175 -0
- package/src/client/components/common/code-editor/index.tsx +185 -220
- package/src/client/components/common/code-surface/index.tsx +109 -12
- package/src/client/components/common/lazy-panels.tsx +5 -1
- package/src/client/components/common/viewport.ts +5 -3
- package/src/client/components/layout/RightActivityBar.tsx +2 -1
- package/src/client/components/layout/desktop-layout/TopNavbar.tsx +21 -4
- package/src/client/components/layout/desktop-layout/index.tsx +24 -4
- package/src/client/components/mobile/RightSidebar.tsx +11 -4
- package/src/client/components/workspace/browser-panel/AddressBar.tsx +1 -10
- package/src/client/components/workspace/browser-panel/Screencast.tsx +4 -5
- package/src/client/components/workspace/browser-panel/index.tsx +6 -6
- package/src/client/components/workspace/browser-toolbar/index.tsx +22 -12
- package/src/client/components/workspace/chat-timeline/ToolCallCard.tsx +1 -1
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Todo.tsx +1 -1
- package/src/client/components/workspace/editor/CommandPalette.test.ts +242 -0
- package/src/client/components/workspace/editor/CommandPalette.tsx +138 -0
- package/src/client/components/workspace/editor/FindWidget.test.ts +261 -0
- package/src/client/components/workspace/editor/FindWidget.tsx +219 -0
- package/src/client/components/workspace/editor/Toolbar.tsx +19 -1
- package/src/client/components/workspace/editor/index.tsx +153 -53
- package/src/client/components/workspace/file-explorer/Modals.tsx +79 -2
- package/src/client/components/workspace/file-explorer/TreeItem.tsx +43 -113
- package/src/client/components/workspace/todo-panel/PhaseGroup.tsx +76 -0
- package/src/client/components/workspace/todo-panel/Row.tsx +69 -0
- package/src/client/components/workspace/todo-panel/index.tsx +161 -0
- package/src/client/hooks/editor/find-harness.test-util.ts +170 -0
- package/src/client/hooks/editor/scroll-container.test.ts +102 -0
- package/src/client/hooks/editor/scroll-container.ts +26 -0
- package/src/client/hooks/editor/use-code-window.ts +2 -20
- package/src/client/hooks/editor/use-editor-find-write.test.ts +170 -0
- package/src/client/hooks/editor/use-editor-find.test.ts +118 -0
- package/src/client/hooks/editor/use-editor-find.ts +321 -0
- package/src/client/hooks/editor/use-editor-history.test.ts +216 -0
- package/src/client/hooks/editor/use-editor-history.ts +134 -0
- package/src/client/hooks/editor/use-occurrences.test.ts +236 -0
- package/src/client/hooks/editor/use-occurrences.ts +98 -0
- package/src/client/hooks/workspace/file-tree-actions.test.ts +348 -0
- package/src/client/hooks/workspace/file-tree-actions.ts +250 -0
- package/src/client/hooks/workspace/session-todos.ts +102 -0
- package/src/client/tailwind.css +30 -0
- package/src/server/lib/omp/session/todos.ts +100 -0
- package/src/server/routes/fs/action-create.test.ts +90 -0
- package/src/server/routes/fs/action.ts +23 -0
- package/src/server/routes/omp/index.ts +2 -0
- package/src/server/routes/omp/session-todos.ts +80 -0
- package/src/shared/lib/chat/{todo-parser.ts → todo/parser.ts} +17 -0
- package/src/shared/lib/chat/todo/progress.ts +97 -0
- package/src/shared/lib/chat/todo/snapshot.test.ts +156 -0
- package/src/shared/lib/chat/todo/snapshot.ts +215 -0
- package/src/shared/lib/code/editor/commands.test.ts +155 -0
- package/src/shared/lib/code/editor/commands.ts +260 -0
- package/src/shared/lib/code/editor/edits.test.ts +100 -0
- package/src/shared/lib/code/editor/edits.ts +122 -0
- package/src/shared/lib/code/editor/find-status.test.ts +50 -0
- package/src/shared/lib/code/editor/find-status.ts +36 -0
- package/src/shared/lib/code/editor/find.test.ts +203 -0
- package/src/shared/lib/code/editor/find.ts +224 -0
- package/src/{client/components/common/code-editor → shared/lib/code/editor}/history.test.ts +1 -1
- package/src/shared/lib/code/editor/keymap.test.ts +109 -0
- package/src/shared/lib/code/editor/keymap.ts +151 -0
- package/src/shared/lib/code/editor/layers.test.ts +77 -0
- package/src/shared/lib/code/editor/layers.ts +67 -0
- package/src/shared/lib/code/editor/line-edits.test.ts +131 -0
- package/src/shared/lib/code/editor/line-edits.ts +159 -0
- package/src/shared/lib/code/editor/lines.test.ts +81 -0
- package/src/shared/lib/code/editor/lines.ts +83 -0
- package/src/shared/lib/code/editor/marks.test.ts +86 -0
- package/src/shared/lib/code/editor/marks.ts +106 -0
- package/src/shared/lib/code/editor/occurrences.test.ts +117 -0
- package/src/shared/lib/code/editor/occurrences.ts +111 -0
- package/src/shared/lib/code/editor/palette.test.ts +91 -0
- package/src/shared/lib/code/editor/palette.ts +92 -0
- package/src/shared/lib/code/editor/reset-css.test.ts +35 -0
- package/src/shared/lib/code/editor/reset-css.ts +32 -0
- package/src/shared/lib/code/editor/text-commands.test.ts +178 -0
- package/src/shared/lib/code/editor/text-commands.ts +154 -0
- package/src/shared/lib/code/syntax-highlight.test.ts +75 -0
- package/src/shared/lib/code/syntax-highlight.ts +29 -17
- package/src/shared/lib/code/windowed-highlight.ts +52 -1
- package/src/shared/lib/ui/key-binding.test.ts +115 -0
- package/src/shared/lib/ui/key-binding.ts +143 -0
- package/src/shared/lib/util/platform.test.ts +87 -0
- package/src/shared/lib/util/platform.ts +30 -0
- package/src/shared/lib/workspace/keymap.test.ts +82 -0
- package/src/shared/lib/workspace/keymap.ts +30 -0
- package/src/shared/lib/workspace/refresh-cadence.ts +9 -0
- package/src/shared/lib/workspace/right-panels.ts +8 -0
- package/src/shared/lib/workspace/session-state/store.ts +0 -1
- package/src/shared/types/index.ts +1 -0
- package/src/shared/types/todo.ts +108 -0
- package/dist/client/static/js/index-ranztes9.css +0 -1
- /package/src/{client/components/common/code-editor → shared/lib/code/editor}/history.ts +0 -0
|
@@ -0,0 +1,102 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
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,15 +1,8 @@
|
|
|
1
1
|
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
|
|
2
2
|
import type { RefObject } from 'preact';
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
firstWindow,
|
|
7
|
-
resolveWindow,
|
|
8
|
-
sameWindow,
|
|
9
|
-
type CodeWindow,
|
|
10
|
-
type LineGeometry,
|
|
11
|
-
type LineWindow,
|
|
12
|
-
} from '@/shared/lib/code/lazy-window';
|
|
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';
|
|
13
6
|
|
|
14
7
|
export interface UseCodeWindowOptions {
|
|
15
8
|
/** The surface's root box: the frame every line offset in `geometry` is measured from. */
|
|
@@ -22,17 +15,6 @@ export interface UseCodeWindowOptions {
|
|
|
22
15
|
enabled: boolean;
|
|
23
16
|
}
|
|
24
17
|
|
|
25
|
-
/** Nearest ancestor the document scrolls inside, or null when the page itself scrolls. */
|
|
26
|
-
function findScrollContainer(element: HTMLElement): HTMLElement | null {
|
|
27
|
-
for (let node = element.parentElement; node; node = node.parentElement) {
|
|
28
|
-
const overflowY = getComputedStyle(node).overflowY;
|
|
29
|
-
if ((overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay') && node.scrollHeight > node.clientHeight) {
|
|
30
|
-
return node;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
18
|
/**
|
|
37
19
|
* The lines visible inside the scroll container `root` lives in.
|
|
38
20
|
*
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `useEditorFind` — writing.
|
|
8
|
+
*
|
|
9
|
+
* The rules asserted here are the ones a user notices when they break: a
|
|
10
|
+
* replacement is ONE undoable write that re-aims at the match after the text it
|
|
11
|
+
* just inserted, "replace all" reharness.writes every match, and a replacement that
|
|
12
|
+
* changes nothing harness.writes nothing at all.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, expect, test } from 'bun:test';
|
|
16
|
+
|
|
17
|
+
import { createFindHarness } from '@/client/hooks/editor/find-harness.test-util';
|
|
18
|
+
|
|
19
|
+
const harness = await createFindHarness();
|
|
20
|
+
harness.install();
|
|
21
|
+
|
|
22
|
+
describe('useEditorFind replacing', () => {
|
|
23
|
+
test('replace harness.writes one undoable edit and re-aims past the inserted text', async () => {
|
|
24
|
+
await harness.act(async () => {
|
|
25
|
+
harness.find?.openReplace();
|
|
26
|
+
});
|
|
27
|
+
await harness.act(async () => {
|
|
28
|
+
harness.find?.setReplacement('OMEGA');
|
|
29
|
+
});
|
|
30
|
+
await harness.act(async () => {
|
|
31
|
+
harness.find?.replaceCurrent();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
expect(harness.writes[0].value).toBe('OMEGA beta alpha');
|
|
35
|
+
expect(harness.writes[0].caretStart).toBe(5);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('replace all reharness.writes every match', async () => {
|
|
39
|
+
await harness.act(async () => {
|
|
40
|
+
harness.find?.openReplace();
|
|
41
|
+
});
|
|
42
|
+
await harness.act(async () => {
|
|
43
|
+
harness.find?.setReplacement('X');
|
|
44
|
+
});
|
|
45
|
+
await harness.act(async () => {
|
|
46
|
+
harness.find?.replaceAll();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(harness.writes[0].value).toBe('X beta X');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('replace all on an unchanged buffer harness.writes nothing', async () => {
|
|
53
|
+
await harness.act(async () => {
|
|
54
|
+
harness.find?.openReplace();
|
|
55
|
+
});
|
|
56
|
+
await harness.act(async () => {
|
|
57
|
+
harness.find?.setQuery('beta');
|
|
58
|
+
});
|
|
59
|
+
await harness.act(async () => {
|
|
60
|
+
harness.find?.setReplacement('beta');
|
|
61
|
+
});
|
|
62
|
+
await harness.act(async () => {
|
|
63
|
+
harness.find?.replaceAll();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(harness.writes).toEqual([]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('replace with no current match harness.writes nothing', async () => {
|
|
70
|
+
await harness.act(async () => {
|
|
71
|
+
harness.find?.openReplace();
|
|
72
|
+
});
|
|
73
|
+
await harness.act(async () => {
|
|
74
|
+
harness.find?.setQuery('zzz');
|
|
75
|
+
});
|
|
76
|
+
await harness.act(async () => {
|
|
77
|
+
harness.find?.replaceCurrent();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(harness.writes).toEqual([]);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('useEditorFind options', () => {
|
|
85
|
+
test('toggling match case narrows the matches', async () => {
|
|
86
|
+
await harness.act(async () => {
|
|
87
|
+
harness.find?.openFind();
|
|
88
|
+
});
|
|
89
|
+
await harness.act(async () => {
|
|
90
|
+
harness.find?.setQuery('ALPHA');
|
|
91
|
+
});
|
|
92
|
+
expect(harness.find?.matches.length).toBe(2);
|
|
93
|
+
|
|
94
|
+
await harness.act(async () => {
|
|
95
|
+
harness.find?.toggleOption('matchCase');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(harness.find?.matches.length).toBe(0);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('an invalid pattern is reported as such, not as an empty result', async () => {
|
|
102
|
+
await harness.act(async () => {
|
|
103
|
+
harness.find?.openFind();
|
|
104
|
+
});
|
|
105
|
+
await harness.act(async () => {
|
|
106
|
+
harness.find?.toggleOption('isRegex');
|
|
107
|
+
});
|
|
108
|
+
await harness.act(async () => {
|
|
109
|
+
harness.find?.setQuery('a(');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(harness.find?.invalid).toBe(true);
|
|
113
|
+
expect(harness.find?.matches).toEqual([]);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('useEditorFind commands', () => {
|
|
118
|
+
test('runCommand routes a find-bar chord to the bar’s own action', async () => {
|
|
119
|
+
await harness.act(async () => {
|
|
120
|
+
harness.find?.runCommand('find');
|
|
121
|
+
});
|
|
122
|
+
expect(harness.find?.open).toBe(true);
|
|
123
|
+
|
|
124
|
+
await harness.act(async () => {
|
|
125
|
+
harness.find?.runCommand('replace');
|
|
126
|
+
});
|
|
127
|
+
expect(harness.find?.replaceOpen).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('an editor command the bar does not own is ignored, not misrouted', async () => {
|
|
131
|
+
await harness.act(async () => {
|
|
132
|
+
harness.find?.openFind();
|
|
133
|
+
});
|
|
134
|
+
await harness.act(async () => {
|
|
135
|
+
harness.find?.runCommand('deleteLine');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
expect(harness.writes).toEqual([]);
|
|
139
|
+
expect(harness.find?.open).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('closing puts focus back on the current match', async () => {
|
|
143
|
+
await harness.act(async () => {
|
|
144
|
+
harness.find?.openFind();
|
|
145
|
+
});
|
|
146
|
+
harness.selections.length = 0;
|
|
147
|
+
|
|
148
|
+
await harness.act(async () => {
|
|
149
|
+
harness.find?.close();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
expect(harness.find?.open).toBe(false);
|
|
153
|
+
expect(harness.selections).toEqual([[0, 5, true]]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('a new document re-aims at the top instead of keeping the old offset', async () => {
|
|
157
|
+
await harness.act(async () => {
|
|
158
|
+
harness.find?.openFind();
|
|
159
|
+
});
|
|
160
|
+
await harness.act(async () => {
|
|
161
|
+
harness.find?.step(1);
|
|
162
|
+
});
|
|
163
|
+
expect(harness.find?.currentIndex).toBe(1);
|
|
164
|
+
|
|
165
|
+
harness.input.documentKey = 'file-2';
|
|
166
|
+
await harness.rerender();
|
|
167
|
+
|
|
168
|
+
expect(harness.find?.currentIndex).toBe(0);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `useEditorFind` — opening and stepping.
|
|
8
|
+
*
|
|
9
|
+
* The rules asserted here are the ones a user notices when they break: the query
|
|
10
|
+
* seeds from the selection (VS Code's behaviour, and the reason ⌘F on a word is
|
|
11
|
+
* one keystroke), ⌘F on an open bar refocuses the field instead of overwriting
|
|
12
|
+
* what is in it, and a new document re-aims at the top rather than keeping an
|
|
13
|
+
* offset that means something else in this file.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, expect, test } from 'bun:test';
|
|
17
|
+
|
|
18
|
+
import { createFindHarness } from '@/client/hooks/editor/find-harness.test-util';
|
|
19
|
+
|
|
20
|
+
const harness = await createFindHarness();
|
|
21
|
+
harness.install();
|
|
22
|
+
|
|
23
|
+
describe('useEditorFind opening', () => {
|
|
24
|
+
test('⌘F seeds the query from the selection and jumps to the first match', async () => {
|
|
25
|
+
await harness.act(async () => {
|
|
26
|
+
harness.find?.openFind();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
expect(harness.find?.open).toBe(true);
|
|
30
|
+
expect(harness.find?.query).toBe('alpha');
|
|
31
|
+
expect(harness.find?.matches.length).toBe(2);
|
|
32
|
+
expect(harness.find?.currentIndex).toBe(0);
|
|
33
|
+
expect(harness.reveals).toEqual([0]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('an empty or multi-line selection does not seed a query', async () => {
|
|
37
|
+
harness.input.selection = { start: 0, end: 0 };
|
|
38
|
+
await harness.act(async () => {
|
|
39
|
+
harness.find?.openFind();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(harness.find?.query).toBe('');
|
|
43
|
+
expect(harness.find?.open).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('⌘F on an open bar keeps the query rather than re-seeding it', async () => {
|
|
47
|
+
// The bar keeps the current match selected in the document, so re-seeding
|
|
48
|
+
// would overwrite what the user typed with that match's text.
|
|
49
|
+
await harness.act(async () => {
|
|
50
|
+
harness.find?.openFind();
|
|
51
|
+
});
|
|
52
|
+
await harness.act(async () => {
|
|
53
|
+
harness.find?.setQuery('beta');
|
|
54
|
+
});
|
|
55
|
+
harness.input.selection = { start: 0, end: 5 };
|
|
56
|
+
|
|
57
|
+
await harness.act(async () => {
|
|
58
|
+
harness.find?.openFind();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(harness.find?.query).toBe('beta');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('opening replace shows the second row and keeps the same match', async () => {
|
|
65
|
+
await harness.act(async () => {
|
|
66
|
+
harness.find?.openReplace();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(harness.find?.open).toBe(true);
|
|
70
|
+
expect(harness.find?.replaceOpen).toBe(true);
|
|
71
|
+
expect(harness.find?.currentIndex).toBe(0);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('useEditorFind stepping', () => {
|
|
76
|
+
test('F3 moves to the next match and wraps', async () => {
|
|
77
|
+
await harness.act(async () => {
|
|
78
|
+
harness.find?.openFind();
|
|
79
|
+
});
|
|
80
|
+
await harness.act(async () => {
|
|
81
|
+
harness.find?.step(1);
|
|
82
|
+
});
|
|
83
|
+
expect(harness.find?.currentIndex).toBe(1);
|
|
84
|
+
|
|
85
|
+
await harness.act(async () => {
|
|
86
|
+
harness.find?.step(1);
|
|
87
|
+
});
|
|
88
|
+
expect(harness.find?.currentIndex).toBe(0);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('⇧F3 moves back, wrapping to the last match', async () => {
|
|
92
|
+
await harness.act(async () => {
|
|
93
|
+
harness.find?.openFind();
|
|
94
|
+
});
|
|
95
|
+
await harness.act(async () => {
|
|
96
|
+
harness.find?.step(-1);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
expect(harness.find?.currentIndex).toBe(1);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('stepping with no matches does nothing', async () => {
|
|
103
|
+
await harness.act(async () => {
|
|
104
|
+
harness.find?.openFind();
|
|
105
|
+
});
|
|
106
|
+
await harness.act(async () => {
|
|
107
|
+
harness.find?.setQuery('zzz');
|
|
108
|
+
});
|
|
109
|
+
const before = harness.find?.currentIndex;
|
|
110
|
+
|
|
111
|
+
await harness.act(async () => {
|
|
112
|
+
harness.find?.step(1);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
expect(harness.find?.currentIndex).toBe(before);
|
|
116
|
+
expect(harness.find?.matches.length).toBe(0);
|
|
117
|
+
});
|
|
118
|
+
});
|