@tooee/search 0.2.1 → 0.2.3

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/dist/index.d.ts CHANGED
@@ -1,4 +1,8 @@
1
1
  export { useSearch } from "./search-hook.js";
2
+ export { useNavSearchStore, useSearchBindings } from "./search-hook.js";
3
+ export { createNavSearchStore } from "./nav-search-store.js";
4
+ export type { NavSearchStore, NavSearchContext, NavSearchEvents, RowKey, } from "./nav-search-store.js";
5
+ export { selectCursor, selectRowKeys, selectSelectionAnchor, selectToggledKeys, selectSearchStatus, selectSearchActive, selectSearchQuery, selectMatches, selectCurrentMatchIndex, deriveSelection, resolveIndex, } from "./nav-search-store.js";
2
6
  export type { UseSearchOptions, SearchState } from "./search-hook.js";
3
7
  export { findMatchingLines } from "./search.js";
4
8
  export { SearchBar } from "./SearchBar.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAC5C,YAAY,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AACrE,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC1C,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAC5C,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AACvE,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AAC5D,YAAY,EACV,cAAc,EACd,gBAAgB,EAChB,eAAe,EACf,MAAM,GACP,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EACL,YAAY,EACZ,aAAa,EACb,qBAAqB,EACrB,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EACjB,aAAa,EACb,uBAAuB,EACvB,eAAe,EACf,YAAY,GACb,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AACrE,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC1C,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA"}
package/dist/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  export { useSearch } from "./search-hook.js";
2
+ export { useNavSearchStore, useSearchBindings } from "./search-hook.js";
3
+ export { createNavSearchStore } from "./nav-search-store.js";
4
+ export { selectCursor, selectRowKeys, selectSelectionAnchor, selectToggledKeys, selectSearchStatus, selectSearchActive, selectSearchQuery, selectMatches, selectCurrentMatchIndex, deriveSelection, resolveIndex, } from "./nav-search-store.js";
2
5
  export { findMatchingLines } from "./search.js";
3
6
  export { SearchBar } from "./SearchBar.js";
4
7
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAE5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAC5C,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AACvE,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AAO5D,OAAO,EACL,YAAY,EACZ,aAAa,EACb,qBAAqB,EACrB,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EACjB,aAAa,EACb,uBAAuB,EACvB,eAAe,EACf,YAAY,GACb,MAAM,uBAAuB,CAAA;AAE9B,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA"}
@@ -0,0 +1,82 @@
1
+ import type { Mode } from "@tooee/commands";
2
+ export type RowKey = string | number | bigint;
3
+ export type SearchStatus = "idle" | "editing" | "committed";
4
+ export interface NavSearchContext {
5
+ rowKeys: readonly RowKey[];
6
+ cursor: number | null;
7
+ selectionAnchor: number | null;
8
+ toggledKeys: ReadonlySet<RowKey>;
9
+ search: {
10
+ status: SearchStatus;
11
+ query: string;
12
+ committedQuery: string;
13
+ matches: readonly number[];
14
+ currentMatchIndex: number;
15
+ preSearchMode: Mode;
16
+ };
17
+ }
18
+ export interface NavSearchDeps {
19
+ isSelectable(index: number): boolean;
20
+ }
21
+ export type NavSearchEvents = {
22
+ rowsChanged: {
23
+ keys: readonly RowKey[];
24
+ preserveCursorByKey?: boolean;
25
+ };
26
+ move: {
27
+ delta: number;
28
+ };
29
+ jump: {
30
+ index: number;
31
+ direction: 1 | -1;
32
+ };
33
+ setCursor: {
34
+ index: number;
35
+ };
36
+ enterSelect: {};
37
+ cancelSelect: {};
38
+ toggleCurrent: {};
39
+ toggleAndMove: {
40
+ delta: number;
41
+ };
42
+ searchStarted: {
43
+ mode: Mode;
44
+ };
45
+ searchChanged: {
46
+ query: string;
47
+ matches: readonly number[];
48
+ };
49
+ searchSubmitted: {};
50
+ searchCancelled: {};
51
+ searchNext: {};
52
+ searchPrevious: {};
53
+ };
54
+ type NavSearchEmitted = {
55
+ restoreMode: {
56
+ mode: Mode;
57
+ };
58
+ jumped: {
59
+ index: number;
60
+ };
61
+ };
62
+ export declare function resolveIndex(target: number, direction: 1 | -1, rowCount: number, isSelectable: (index: number) => boolean): number | null;
63
+ export declare function createNavSearchStore(options?: {
64
+ keys?: readonly RowKey[];
65
+ deps?: NavSearchDeps;
66
+ }): import("@xstate/store").Store<NavSearchContext, NavSearchEvents, import("@xstate/store").ExtractEvents<NavSearchEmitted>>;
67
+ export type NavSearchStore = ReturnType<typeof createNavSearchStore>;
68
+ export declare const selectCursor: (ctx: NavSearchContext) => number | null;
69
+ export declare const selectRowKeys: (ctx: NavSearchContext) => readonly RowKey[];
70
+ export declare const selectSelectionAnchor: (ctx: NavSearchContext) => number | null;
71
+ export declare const selectToggledKeys: (ctx: NavSearchContext) => ReadonlySet<RowKey>;
72
+ export declare const selectSearchStatus: (ctx: NavSearchContext) => SearchStatus;
73
+ export declare const selectSearchActive: (ctx: NavSearchContext) => boolean;
74
+ export declare const selectSearchQuery: (ctx: NavSearchContext) => string;
75
+ export declare const selectMatches: (ctx: NavSearchContext) => readonly number[];
76
+ export declare const selectCurrentMatchIndex: (ctx: NavSearchContext) => number;
77
+ export declare function deriveSelection(ctx: NavSearchContext, mode: Mode): {
78
+ start: number;
79
+ end: number;
80
+ } | null;
81
+ export {};
82
+ //# sourceMappingURL=nav-search-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nav-search-store.d.ts","sourceRoot":"","sources":["../src/nav-search-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,193 @@
1
+ import { createStore } from "@xstate/store";
2
+ export function resolveIndex(target, direction, rowCount, isSelectable) {
3
+ if (rowCount <= 0)
4
+ return null;
5
+ const max = rowCount - 1;
6
+ const clamped = Math.max(0, Math.min(target, max));
7
+ if (isSelectable(clamped))
8
+ return clamped;
9
+ for (let index = clamped + direction; direction === 1 ? index <= max : index >= 0; index += direction) {
10
+ if (isSelectable(index))
11
+ return index;
12
+ }
13
+ for (let index = clamped - direction; direction === 1 ? index >= 0 : index <= max; index -= direction) {
14
+ if (isSelectable(index))
15
+ return index;
16
+ }
17
+ return null;
18
+ }
19
+ const EMPTY_KEYS = [];
20
+ export function createNavSearchStore(options = {}) {
21
+ const deps = options.deps ?? { isSelectable: () => true };
22
+ const keys = options.keys ?? EMPTY_KEYS;
23
+ const resolve = (index, direction, count = keys.length) => resolveIndex(index, direction, count, deps.isSelectable);
24
+ return createStore({
25
+ context: {
26
+ rowKeys: keys,
27
+ cursor: resolve(0, 1),
28
+ selectionAnchor: null,
29
+ toggledKeys: new Set(),
30
+ search: {
31
+ status: "idle",
32
+ query: "",
33
+ committedQuery: "",
34
+ matches: [],
35
+ currentMatchIndex: 0,
36
+ preSearchMode: "cursor",
37
+ },
38
+ },
39
+ on: {
40
+ rowsChanged: (ctx, event) => {
41
+ const previousKey = ctx.cursor === null ? undefined : ctx.rowKeys[ctx.cursor];
42
+ let cursor = ctx.cursor;
43
+ if (event.keys.length === 0)
44
+ cursor = null;
45
+ else if (event.preserveCursorByKey && previousKey !== undefined) {
46
+ const preserved = event.keys.indexOf(previousKey);
47
+ cursor =
48
+ preserved >= 0
49
+ ? resolveIndex(preserved, 1, event.keys.length, deps.isSelectable)
50
+ : resolveIndex(Math.min(ctx.cursor, event.keys.length - 1), 1, event.keys.length, deps.isSelectable);
51
+ }
52
+ else if (cursor === null || cursor >= event.keys.length) {
53
+ cursor = resolveIndex(cursor === null ? 0 : event.keys.length - 1, cursor === null ? 1 : -1, event.keys.length, deps.isSelectable);
54
+ }
55
+ else if (!deps.isSelectable(cursor)) {
56
+ cursor = resolveIndex(cursor, 1, event.keys.length, deps.isSelectable);
57
+ }
58
+ const available = new Set(event.keys);
59
+ const filtered = new Set([...ctx.toggledKeys].filter((key) => available.has(key)));
60
+ const toggledKeys = filtered.size === ctx.toggledKeys.size ? ctx.toggledKeys : filtered;
61
+ return { ...ctx, rowKeys: event.keys, cursor, toggledKeys };
62
+ },
63
+ move: (ctx, event) => {
64
+ if (ctx.cursor === null)
65
+ return ctx;
66
+ const direction = event.delta < 0 ? -1 : 1;
67
+ const cursor = resolveIndex(ctx.cursor + event.delta, direction, ctx.rowKeys.length, deps.isSelectable);
68
+ return cursor === null || cursor === ctx.cursor ? ctx : { ...ctx, cursor };
69
+ },
70
+ jump: (ctx, event) => ({
71
+ ...ctx,
72
+ cursor: resolveIndex(event.index, event.direction, ctx.rowKeys.length, deps.isSelectable),
73
+ }),
74
+ setCursor: (ctx, event) => {
75
+ const direction = ctx.cursor !== null && event.index < ctx.cursor ? -1 : 1;
76
+ const cursor = resolveIndex(event.index, direction, ctx.rowKeys.length, deps.isSelectable);
77
+ return cursor === null ? ctx : { ...ctx, cursor };
78
+ },
79
+ enterSelect: (ctx) => ({ ...ctx, selectionAnchor: ctx.cursor }),
80
+ cancelSelect: (ctx) => ctx.selectionAnchor === null ? ctx : { ...ctx, selectionAnchor: null },
81
+ toggleCurrent: (ctx) => toggle(ctx),
82
+ toggleAndMove: (ctx, event) => {
83
+ const toggled = toggle(ctx);
84
+ if (toggled.cursor === null)
85
+ return toggled;
86
+ const direction = event.delta < 0 ? -1 : 1;
87
+ const cursor = resolveIndex(toggled.cursor + event.delta, direction, toggled.rowKeys.length, deps.isSelectable);
88
+ return cursor === null ? toggled : { ...toggled, cursor };
89
+ },
90
+ searchStarted: (ctx, event) => ({
91
+ ...ctx,
92
+ search: {
93
+ ...ctx.search,
94
+ status: "editing",
95
+ query: "",
96
+ matches: [],
97
+ currentMatchIndex: 0,
98
+ preSearchMode: event.mode,
99
+ },
100
+ }),
101
+ searchChanged: (ctx, event, enqueue) => {
102
+ const first = event.matches[0];
103
+ if (first !== undefined)
104
+ enqueue.emit.jumped({ index: first });
105
+ return {
106
+ ...ctx,
107
+ cursor: first === undefined
108
+ ? ctx.cursor
109
+ : resolveIndex(first, 1, ctx.rowKeys.length, deps.isSelectable),
110
+ search: {
111
+ ...ctx.search,
112
+ query: event.query,
113
+ matches: event.matches,
114
+ currentMatchIndex: 0,
115
+ },
116
+ };
117
+ },
118
+ searchSubmitted: (ctx, _event, enqueue) => {
119
+ const first = ctx.search.matches[0];
120
+ if (first !== undefined)
121
+ enqueue.emit.jumped({ index: first });
122
+ enqueue.emit.restoreMode({ mode: ctx.search.preSearchMode });
123
+ return {
124
+ ...ctx,
125
+ cursor: first === undefined
126
+ ? ctx.cursor
127
+ : resolveIndex(first, 1, ctx.rowKeys.length, deps.isSelectable),
128
+ search: {
129
+ ...ctx.search,
130
+ status: "committed",
131
+ committedQuery: ctx.search.query,
132
+ currentMatchIndex: 0,
133
+ },
134
+ };
135
+ },
136
+ searchCancelled: (ctx, _event, enqueue) => {
137
+ enqueue.emit.restoreMode({ mode: ctx.search.preSearchMode });
138
+ return {
139
+ ...ctx,
140
+ search: {
141
+ ...ctx.search,
142
+ status: "idle",
143
+ query: "",
144
+ committedQuery: "",
145
+ matches: [],
146
+ currentMatchIndex: 0,
147
+ },
148
+ };
149
+ },
150
+ searchNext: (ctx, _event, enqueue) => searchStep(ctx, 1, enqueue),
151
+ searchPrevious: (ctx, _event, enqueue) => searchStep(ctx, -1, enqueue),
152
+ },
153
+ });
154
+ }
155
+ function toggle(ctx) {
156
+ if (ctx.cursor === null)
157
+ return ctx;
158
+ const key = ctx.rowKeys[ctx.cursor];
159
+ if (key === undefined)
160
+ return ctx;
161
+ const toggledKeys = new Set(ctx.toggledKeys);
162
+ if (toggledKeys.has(key))
163
+ toggledKeys.delete(key);
164
+ else
165
+ toggledKeys.add(key);
166
+ return { ...ctx, toggledKeys };
167
+ }
168
+ function searchStep(ctx, delta, enqueue) {
169
+ if (ctx.search.matches.length === 0)
170
+ return ctx;
171
+ const currentMatchIndex = (ctx.search.currentMatchIndex + delta + ctx.search.matches.length) % ctx.search.matches.length;
172
+ const cursor = ctx.search.matches[currentMatchIndex];
173
+ enqueue.emit.jumped({ index: cursor });
174
+ return { ...ctx, cursor, search: { ...ctx.search, currentMatchIndex } };
175
+ }
176
+ export const selectCursor = (ctx) => ctx.cursor;
177
+ export const selectRowKeys = (ctx) => ctx.rowKeys;
178
+ export const selectSelectionAnchor = (ctx) => ctx.selectionAnchor;
179
+ export const selectToggledKeys = (ctx) => ctx.toggledKeys;
180
+ export const selectSearchStatus = (ctx) => ctx.search.status;
181
+ export const selectSearchActive = (ctx) => ctx.search.status === "editing";
182
+ export const selectSearchQuery = (ctx) => ctx.search.status === "editing" ? ctx.search.query : ctx.search.committedQuery;
183
+ export const selectMatches = (ctx) => ctx.search.matches;
184
+ export const selectCurrentMatchIndex = (ctx) => ctx.search.currentMatchIndex;
185
+ export function deriveSelection(ctx, mode) {
186
+ return mode === "select" && ctx.selectionAnchor !== null && ctx.cursor !== null
187
+ ? {
188
+ start: Math.min(ctx.selectionAnchor, ctx.cursor),
189
+ end: Math.max(ctx.selectionAnchor, ctx.cursor),
190
+ }
191
+ : null;
192
+ }
193
+ //# sourceMappingURL=nav-search-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nav-search-store.js","sourceRoot":"","sources":["../src/nav-search-store.ts"],"names":[],"mappings":"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"}
@@ -1,17 +1,8 @@
1
+ import { type NavSearchStore, type RowKey } from "./nav-search-store.js";
1
2
  export interface UseSearchOptions {
2
3
  match: (query: string) => number[];
3
4
  onJump: (index: number) => void;
4
- /**
5
- * Register the search commands (default true). `false` leaves the state
6
- * inert: no `/`, `n`, `shift+n` or cancel command exists.
7
- */
8
5
  enabled?: boolean;
9
- /**
10
- * Values the searched content is derived from. Matches are memoized on the
11
- * query, so a committed query goes stale when the content changes underneath
12
- * it (streaming or reloaded documents). List the content here to re-match.
13
- * The array length must be stable across renders.
14
- */
15
6
  deps?: readonly unknown[];
16
7
  }
17
8
  export interface SearchState {
@@ -22,5 +13,13 @@ export interface SearchState {
22
13
  currentMatchIndex: number;
23
14
  submitSearch: () => void;
24
15
  }
25
- export declare function useSearch({ match, onJump, enabled, deps, }: UseSearchOptions): SearchState;
16
+ export declare function useNavSearchStore(options: {
17
+ keys: readonly RowKey[];
18
+ isSelectable?: (index: number) => boolean;
19
+ preserveCursorByKey?: boolean;
20
+ }): NavSearchStore;
21
+ export declare function useSearchBindings(store: NavSearchStore, { match, onJump, enabled, deps, }: Omit<UseSearchOptions, "onJump"> & {
22
+ onJump?: (index: number) => void;
23
+ }): SearchState;
24
+ export declare function useSearch(options: UseSearchOptions): SearchState;
26
25
  //# sourceMappingURL=search-hook.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"search-hook.d.ts","sourceRoot":"","sources":["../src/search-hook.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,EAAE,CAAA;IAClC,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAC/B;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,SAAS,OAAO,EAAE,CAAA;CAC1B;AAED,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,OAAO,CAAA;IACrB,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACvC,aAAa,EAAE,MAAM,EAAE,CAAA;IACvB,iBAAiB,EAAE,MAAM,CAAA;IACzB,YAAY,EAAE,MAAM,IAAI,CAAA;CACzB;AAkBD,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,MAAM,EACN,OAAc,EACd,IAAiB,GAClB,EAAE,gBAAgB,GAAG,WAAW,CA6IhC"}
1
+ {"version":3,"file":"search-hook.d.ts","sourceRoot":"","sources":["../src/search-hook.ts"],"names":[],"mappings":"AAGA,OAAO,EAOL,KAAK,cAAc,EACnB,KAAK,MAAM,EACZ,MAAM,uBAAuB,CAAA;AAE9B,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,EAAE,CAAA;IAClC,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAC/B,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,IAAI,CAAC,EAAE,SAAS,OAAO,EAAE,CAAA;CAC1B;AAED,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,OAAO,CAAA;IACrB,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACvC,aAAa,EAAE,MAAM,EAAE,CAAA;IACvB,iBAAiB,EAAE,MAAM,CAAA;IACzB,YAAY,EAAE,MAAM,IAAI,CAAA;CACzB;AAKD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE;IACzC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IACvB,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAA;IACzC,mBAAmB,CAAC,EAAE,OAAO,CAAA;CAC9B,GAAG,cAAc,CAmBjB;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,cAAc,EACrB,EACE,KAAK,EACL,MAAM,EACN,OAAc,EACd,IAAS,GACV,EAAE,IAAI,CAAC,gBAAgB,EAAE,QAAQ,CAAC,GAAG;IAAE,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,GACzE,WAAW,CA6Fb;AAED,wBAAgB,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,WAAW,CAGhE"}
@@ -1,56 +1,59 @@
1
- import { useCallback, useRef, useState } from "react";
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ import { useSelector } from "@xstate/store-react";
2
3
  import { useCommand, useMode, useSetMode } from "@tooee/commands";
3
- const EMPTY = [];
4
- const EMPTY_DEPS = [];
5
- function sameDeps(left, right) {
6
- if (left.length !== right.length)
7
- return false;
8
- return left.every((value, index) => Object.is(value, right[index]));
9
- }
4
+ import { createNavSearchStore, selectCurrentMatchIndex, selectMatches, selectSearchActive, selectSearchQuery, } from "./nav-search-store.js";
10
5
  const CURSOR_MODES = ["cursor"];
11
6
  const ALL_MODES = ["cursor", "select", "insert"];
12
- export function useSearch({ match, onJump, enabled = true, deps = EMPTY_DEPS, }) {
7
+ export function useNavSearchStore(options) {
8
+ const depsRef = useRef({ isSelectable: options.isSelectable ?? (() => true) });
9
+ depsRef.current.isSelectable = options.isSelectable ?? (() => true);
10
+ const [store] = useState(() => createNavSearchStore({ keys: options.keys, deps: depsRef.current }));
11
+ useEffect(() => {
12
+ const currentKeys = store.getSnapshot().context.rowKeys;
13
+ const keysChanged = currentKeys.length !== options.keys.length ||
14
+ currentKeys.some((key, index) => !Object.is(key, options.keys[index]));
15
+ if (keysChanged) {
16
+ store.trigger.rowsChanged({
17
+ keys: options.keys,
18
+ preserveCursorByKey: options.preserveCursorByKey,
19
+ });
20
+ }
21
+ }, [store, options.keys, options.preserveCursorByKey]);
22
+ return store;
23
+ }
24
+ export function useSearchBindings(store, { match, onJump, enabled = true, deps = [], }) {
13
25
  const mode = useMode();
14
26
  const setMode = useSetMode();
15
- const [searchQuery, setSearchQuery] = useState("");
16
- const [searchActive, setSearchActive] = useState(false);
17
- const [currentMatchIndex, setCurrentMatchIndex] = useState(0);
18
- const [committedQuery, setCommittedQuery] = useState("");
19
- const preSearchModeRef = useRef("cursor");
20
27
  const matchRef = useRef(match);
21
28
  matchRef.current = match;
22
29
  const onJumpRef = useRef(onJump);
23
30
  onJumpRef.current = onJump;
24
- const activeQuery = searchActive ? searchQuery : committedQuery;
25
- // Memoized on the query *and* the searched content: a committed query must
26
- // re-match when rows arrive or change. `deps` is caller-sized, so this is a
27
- // hand-rolled cache rather than a useMemo dependency array.
28
- const cacheRef = useRef(null);
29
- let matchingLines;
30
- if (!activeQuery) {
31
- matchingLines = EMPTY;
32
- }
33
- else {
34
- const cache = cacheRef.current;
35
- if (cache && cache.query === activeQuery && sameDeps(cache.deps, deps)) {
36
- matchingLines = cache.matches;
37
- }
38
- else {
39
- matchingLines = matchRef.current(activeQuery);
40
- cacheRef.current = { query: activeQuery, deps: [...deps], matches: matchingLines };
41
- }
42
- }
43
- const matchingLinesRef = useRef(matchingLines);
44
- matchingLinesRef.current = matchingLines;
45
- // Imperatively set search query, reset match index, and jump to first match.
31
+ useEffect(() => {
32
+ const subscription = store.on("jumped", ({ index }) => onJumpRef.current?.(index));
33
+ return () => subscription.unsubscribe();
34
+ }, [store]);
35
+ useEffect(() => {
36
+ const subscription = store.on("restoreMode", ({ mode: restored }) => setMode(restored));
37
+ return () => subscription.unsubscribe();
38
+ }, [store, setMode]);
39
+ const searchQuery = useSelector(store, (snapshot) => selectSearchQuery(snapshot.context));
40
+ const searchActive = useSelector(store, (snapshot) => selectSearchActive(snapshot.context));
41
+ const matchingLines = useSelector(store, (snapshot) => selectMatches(snapshot.context));
42
+ const currentMatchIndex = useSelector(store, (snapshot) => selectCurrentMatchIndex(snapshot.context));
46
43
  const updateSearchQuery = useCallback((query) => {
47
- setSearchQuery(query);
48
- setCurrentMatchIndex(0);
49
- const matches = query ? matchRef.current(query) : [];
50
- if (matches[0] != null) {
51
- onJumpRef.current(matches[0]);
52
- }
53
- }, []);
44
+ store.trigger.searchChanged({ query, matches: query ? matchRef.current(query) : [] });
45
+ }, [store]);
46
+ // Re-match a committed query once when the caller-declared content changes.
47
+ const depsRef = useRef(deps);
48
+ useEffect(() => {
49
+ if (depsRef.current.every((value, index) => Object.is(value, deps[index])) &&
50
+ depsRef.current.length === deps.length)
51
+ return;
52
+ depsRef.current = deps;
53
+ const query = selectSearchQuery(store.getSnapshot().context);
54
+ if (query)
55
+ store.trigger.searchChanged({ query, matches: matchRef.current(query) });
56
+ }, [deps, store]);
54
57
  useCommand({
55
58
  id: "cursor-search-start",
56
59
  title: "Search",
@@ -58,9 +61,7 @@ export function useSearch({ match, onJump, enabled = true, deps = EMPTY_DEPS, })
58
61
  modes: CURSOR_MODES,
59
62
  enabled,
60
63
  handler: () => {
61
- preSearchModeRef.current = mode;
62
- setSearchActive(true);
63
- setSearchQuery("");
64
+ store.trigger.searchStarted({ mode });
64
65
  setMode("insert");
65
66
  },
66
67
  });
@@ -70,20 +71,8 @@ export function useSearch({ match, onJump, enabled = true, deps = EMPTY_DEPS, })
70
71
  hotkey: "n",
71
72
  modes: CURSOR_MODES,
72
73
  enabled,
73
- when: () => !searchActive,
74
- handler: () => {
75
- const matches = matchingLinesRef.current;
76
- if (matches.length === 0)
77
- return;
78
- setCurrentMatchIndex((index) => {
79
- const nextIndex = (index + 1) % matches.length;
80
- const nextMatch = matches[nextIndex];
81
- if (nextMatch != null) {
82
- onJumpRef.current(nextMatch);
83
- }
84
- return nextIndex;
85
- });
86
- },
74
+ when: () => !selectSearchActive(store.getSnapshot().context),
75
+ handler: () => store.trigger.searchNext({}),
87
76
  });
88
77
  useCommand({
89
78
  id: "cursor-search-prev",
@@ -91,20 +80,8 @@ export function useSearch({ match, onJump, enabled = true, deps = EMPTY_DEPS, })
91
80
  hotkey: "shift+n",
92
81
  modes: CURSOR_MODES,
93
82
  enabled,
94
- when: () => !searchActive,
95
- handler: () => {
96
- const matches = matchingLinesRef.current;
97
- if (matches.length === 0)
98
- return;
99
- setCurrentMatchIndex((index) => {
100
- const nextIndex = (index - 1 + matches.length) % matches.length;
101
- const nextMatch = matches[nextIndex];
102
- if (nextMatch != null) {
103
- onJumpRef.current(nextMatch);
104
- }
105
- return nextIndex;
106
- });
107
- },
83
+ when: () => !selectSearchActive(store.getSnapshot().context),
84
+ handler: () => store.trigger.searchPrevious({}),
108
85
  });
109
86
  useCommand({
110
87
  id: "search-cancel",
@@ -112,32 +89,20 @@ export function useSearch({ match, onJump, enabled = true, deps = EMPTY_DEPS, })
112
89
  hotkey: "escape",
113
90
  modes: ALL_MODES,
114
91
  enabled,
115
- when: () => searchActive,
116
- handler: () => {
117
- setSearchActive(false);
118
- setSearchQuery("");
119
- setCommittedQuery("");
120
- setCurrentMatchIndex(0);
121
- setMode(preSearchModeRef.current);
122
- },
92
+ when: () => selectSearchActive(store.getSnapshot().context),
93
+ handler: () => store.trigger.searchCancelled({}),
123
94
  });
124
- const submitSearch = useCallback(() => {
125
- setCommittedQuery(searchQuery);
126
- setSearchActive(false);
127
- setCurrentMatchIndex(0);
128
- const matches = searchQuery ? matchRef.current(searchQuery) : [];
129
- if (matches[0] != null) {
130
- onJumpRef.current(matches[0]);
131
- }
132
- setMode(preSearchModeRef.current);
133
- }, [searchQuery, setMode]);
134
95
  return {
135
96
  searchQuery,
136
97
  searchActive,
137
98
  setSearchQuery: updateSearchQuery,
138
99
  matchingLines,
139
100
  currentMatchIndex,
140
- submitSearch,
101
+ submitSearch: () => store.trigger.searchSubmitted({}),
141
102
  };
142
103
  }
104
+ export function useSearch(options) {
105
+ const store = useNavSearchStore({ keys: [] });
106
+ return useSearchBindings(store, options);
107
+ }
143
108
  //# sourceMappingURL=search-hook.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"search-hook.js","sourceRoot":"","sources":["../src/search-hook.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"search-hook.js","sourceRoot":"","sources":["../src/search-hook.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tooee/search",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Search hook, utilities, and SearchBar component for Tooee",
5
5
  "keywords": [
6
6
  "cli",
@@ -35,8 +35,10 @@
35
35
  "typecheck": "tsc --noEmit"
36
36
  },
37
37
  "dependencies": {
38
- "@tooee/commands": "0.2.1",
39
- "@tooee/themes": "0.2.1"
38
+ "@tooee/commands": "0.2.3",
39
+ "@tooee/themes": "0.2.3",
40
+ "@xstate/store": "^4",
41
+ "@xstate/store-react": "^2"
40
42
  },
41
43
  "devDependencies": {
42
44
  "@opentui/core": "^0.4.3",
package/src/index.ts CHANGED
@@ -1,4 +1,25 @@
1
1
  export { useSearch } from "./search-hook.js"
2
+ export { useNavSearchStore, useSearchBindings } from "./search-hook.js"
3
+ export { createNavSearchStore } from "./nav-search-store.js"
4
+ export type {
5
+ NavSearchStore,
6
+ NavSearchContext,
7
+ NavSearchEvents,
8
+ RowKey,
9
+ } from "./nav-search-store.js"
10
+ export {
11
+ selectCursor,
12
+ selectRowKeys,
13
+ selectSelectionAnchor,
14
+ selectToggledKeys,
15
+ selectSearchStatus,
16
+ selectSearchActive,
17
+ selectSearchQuery,
18
+ selectMatches,
19
+ selectCurrentMatchIndex,
20
+ deriveSelection,
21
+ resolveIndex,
22
+ } from "./nav-search-store.js"
2
23
  export type { UseSearchOptions, SearchState } from "./search-hook.js"
3
24
  export { findMatchingLines } from "./search.js"
4
25
  export { SearchBar } from "./SearchBar.js"
@@ -0,0 +1,278 @@
1
+ import { createStore } from "@xstate/store"
2
+ import type { Mode } from "@tooee/commands"
3
+
4
+ export type RowKey = string | number | bigint
5
+ export type SearchStatus = "idle" | "editing" | "committed"
6
+
7
+ export interface NavSearchContext {
8
+ rowKeys: readonly RowKey[]
9
+ cursor: number | null
10
+ selectionAnchor: number | null
11
+ toggledKeys: ReadonlySet<RowKey>
12
+ search: {
13
+ status: SearchStatus
14
+ query: string
15
+ committedQuery: string
16
+ matches: readonly number[]
17
+ currentMatchIndex: number
18
+ preSearchMode: Mode
19
+ }
20
+ }
21
+
22
+ export interface NavSearchDeps {
23
+ isSelectable(index: number): boolean
24
+ }
25
+
26
+ export type NavSearchEvents = {
27
+ rowsChanged: { keys: readonly RowKey[]; preserveCursorByKey?: boolean }
28
+ move: { delta: number }
29
+ jump: { index: number; direction: 1 | -1 }
30
+ setCursor: { index: number }
31
+ enterSelect: {}
32
+ cancelSelect: {}
33
+ toggleCurrent: {}
34
+ toggleAndMove: { delta: number }
35
+ searchStarted: { mode: Mode }
36
+ searchChanged: { query: string; matches: readonly number[] }
37
+ searchSubmitted: {}
38
+ searchCancelled: {}
39
+ searchNext: {}
40
+ searchPrevious: {}
41
+ }
42
+
43
+ type NavSearchEmitted = {
44
+ restoreMode: { mode: Mode }
45
+ jumped: { index: number }
46
+ }
47
+
48
+ export function resolveIndex(
49
+ target: number,
50
+ direction: 1 | -1,
51
+ rowCount: number,
52
+ isSelectable: (index: number) => boolean,
53
+ ): number | null {
54
+ if (rowCount <= 0) return null
55
+ const max = rowCount - 1
56
+ const clamped = Math.max(0, Math.min(target, max))
57
+ if (isSelectable(clamped)) return clamped
58
+ for (
59
+ let index = clamped + direction;
60
+ direction === 1 ? index <= max : index >= 0;
61
+ index += direction
62
+ ) {
63
+ if (isSelectable(index)) return index
64
+ }
65
+ for (
66
+ let index = clamped - direction;
67
+ direction === 1 ? index >= 0 : index <= max;
68
+ index -= direction
69
+ ) {
70
+ if (isSelectable(index)) return index
71
+ }
72
+ return null
73
+ }
74
+
75
+ const EMPTY_KEYS: readonly RowKey[] = []
76
+
77
+ export function createNavSearchStore(
78
+ options: {
79
+ keys?: readonly RowKey[]
80
+ deps?: NavSearchDeps
81
+ } = {},
82
+ ) {
83
+ const deps = options.deps ?? { isSelectable: () => true }
84
+ const keys = options.keys ?? EMPTY_KEYS
85
+ const resolve = (index: number, direction: 1 | -1, count = keys.length) =>
86
+ resolveIndex(index, direction, count, deps.isSelectable)
87
+
88
+ return createStore<NavSearchContext, NavSearchEvents, NavSearchEmitted>({
89
+ context: {
90
+ rowKeys: keys,
91
+ cursor: resolve(0, 1),
92
+ selectionAnchor: null,
93
+ toggledKeys: new Set(),
94
+ search: {
95
+ status: "idle",
96
+ query: "",
97
+ committedQuery: "",
98
+ matches: [],
99
+ currentMatchIndex: 0,
100
+ preSearchMode: "cursor",
101
+ },
102
+ },
103
+ on: {
104
+ rowsChanged: (ctx, event) => {
105
+ const previousKey = ctx.cursor === null ? undefined : ctx.rowKeys[ctx.cursor]
106
+ let cursor = ctx.cursor
107
+ if (event.keys.length === 0) cursor = null
108
+ else if (event.preserveCursorByKey && previousKey !== undefined) {
109
+ const preserved = event.keys.indexOf(previousKey)
110
+ cursor =
111
+ preserved >= 0
112
+ ? resolveIndex(preserved, 1, event.keys.length, deps.isSelectable)
113
+ : resolveIndex(
114
+ Math.min(ctx.cursor!, event.keys.length - 1),
115
+ 1,
116
+ event.keys.length,
117
+ deps.isSelectable,
118
+ )
119
+ } else if (cursor === null || cursor >= event.keys.length) {
120
+ cursor = resolveIndex(
121
+ cursor === null ? 0 : event.keys.length - 1,
122
+ cursor === null ? 1 : -1,
123
+ event.keys.length,
124
+ deps.isSelectable,
125
+ )
126
+ } else if (!deps.isSelectable(cursor)) {
127
+ cursor = resolveIndex(cursor, 1, event.keys.length, deps.isSelectable)
128
+ }
129
+ const available = new Set(event.keys)
130
+ const filtered = new Set([...ctx.toggledKeys].filter((key) => available.has(key)))
131
+ const toggledKeys = filtered.size === ctx.toggledKeys.size ? ctx.toggledKeys : filtered
132
+ return { ...ctx, rowKeys: event.keys, cursor, toggledKeys }
133
+ },
134
+ move: (ctx, event) => {
135
+ if (ctx.cursor === null) return ctx
136
+ const direction: 1 | -1 = event.delta < 0 ? -1 : 1
137
+ const cursor = resolveIndex(
138
+ ctx.cursor + event.delta,
139
+ direction,
140
+ ctx.rowKeys.length,
141
+ deps.isSelectable,
142
+ )
143
+ return cursor === null || cursor === ctx.cursor ? ctx : { ...ctx, cursor }
144
+ },
145
+ jump: (ctx, event) => ({
146
+ ...ctx,
147
+ cursor: resolveIndex(event.index, event.direction, ctx.rowKeys.length, deps.isSelectable),
148
+ }),
149
+ setCursor: (ctx, event) => {
150
+ const direction: 1 | -1 = ctx.cursor !== null && event.index < ctx.cursor ? -1 : 1
151
+ const cursor = resolveIndex(event.index, direction, ctx.rowKeys.length, deps.isSelectable)
152
+ return cursor === null ? ctx : { ...ctx, cursor }
153
+ },
154
+ enterSelect: (ctx) => ({ ...ctx, selectionAnchor: ctx.cursor }),
155
+ cancelSelect: (ctx) =>
156
+ ctx.selectionAnchor === null ? ctx : { ...ctx, selectionAnchor: null },
157
+ toggleCurrent: (ctx) => toggle(ctx),
158
+ toggleAndMove: (ctx, event) => {
159
+ const toggled = toggle(ctx)
160
+ if (toggled.cursor === null) return toggled
161
+ const direction: 1 | -1 = event.delta < 0 ? -1 : 1
162
+ const cursor = resolveIndex(
163
+ toggled.cursor + event.delta,
164
+ direction,
165
+ toggled.rowKeys.length,
166
+ deps.isSelectable,
167
+ )
168
+ return cursor === null ? toggled : { ...toggled, cursor }
169
+ },
170
+ searchStarted: (ctx, event) => ({
171
+ ...ctx,
172
+ search: {
173
+ ...ctx.search,
174
+ status: "editing",
175
+ query: "",
176
+ matches: [],
177
+ currentMatchIndex: 0,
178
+ preSearchMode: event.mode,
179
+ },
180
+ }),
181
+ searchChanged: (ctx, event, enqueue) => {
182
+ const first = event.matches[0]
183
+ if (first !== undefined) enqueue.emit.jumped({ index: first })
184
+ return {
185
+ ...ctx,
186
+ cursor:
187
+ first === undefined
188
+ ? ctx.cursor
189
+ : resolveIndex(first, 1, ctx.rowKeys.length, deps.isSelectable),
190
+ search: {
191
+ ...ctx.search,
192
+ query: event.query,
193
+ matches: event.matches,
194
+ currentMatchIndex: 0,
195
+ },
196
+ }
197
+ },
198
+ searchSubmitted: (ctx, _event, enqueue) => {
199
+ const first = ctx.search.matches[0]
200
+ if (first !== undefined) enqueue.emit.jumped({ index: first })
201
+ enqueue.emit.restoreMode({ mode: ctx.search.preSearchMode })
202
+ return {
203
+ ...ctx,
204
+ cursor:
205
+ first === undefined
206
+ ? ctx.cursor
207
+ : resolveIndex(first, 1, ctx.rowKeys.length, deps.isSelectable),
208
+ search: {
209
+ ...ctx.search,
210
+ status: "committed",
211
+ committedQuery: ctx.search.query,
212
+ currentMatchIndex: 0,
213
+ },
214
+ }
215
+ },
216
+ searchCancelled: (ctx, _event, enqueue) => {
217
+ enqueue.emit.restoreMode({ mode: ctx.search.preSearchMode })
218
+ return {
219
+ ...ctx,
220
+ search: {
221
+ ...ctx.search,
222
+ status: "idle",
223
+ query: "",
224
+ committedQuery: "",
225
+ matches: [],
226
+ currentMatchIndex: 0,
227
+ },
228
+ }
229
+ },
230
+ searchNext: (ctx, _event, enqueue) => searchStep(ctx, 1, enqueue),
231
+ searchPrevious: (ctx, _event, enqueue) => searchStep(ctx, -1, enqueue),
232
+ },
233
+ })
234
+ }
235
+
236
+ function toggle(ctx: NavSearchContext): NavSearchContext {
237
+ if (ctx.cursor === null) return ctx
238
+ const key = ctx.rowKeys[ctx.cursor]
239
+ if (key === undefined) return ctx
240
+ const toggledKeys = new Set(ctx.toggledKeys)
241
+ if (toggledKeys.has(key)) toggledKeys.delete(key)
242
+ else toggledKeys.add(key)
243
+ return { ...ctx, toggledKeys }
244
+ }
245
+
246
+ function searchStep(
247
+ ctx: NavSearchContext,
248
+ delta: 1 | -1,
249
+ enqueue: { emit: { jumped: (event: { index: number }) => void } },
250
+ ): NavSearchContext {
251
+ if (ctx.search.matches.length === 0) return ctx
252
+ const currentMatchIndex =
253
+ (ctx.search.currentMatchIndex + delta + ctx.search.matches.length) % ctx.search.matches.length
254
+ const cursor = ctx.search.matches[currentMatchIndex]!
255
+ enqueue.emit.jumped({ index: cursor })
256
+ return { ...ctx, cursor, search: { ...ctx.search, currentMatchIndex } }
257
+ }
258
+
259
+ export type NavSearchStore = ReturnType<typeof createNavSearchStore>
260
+
261
+ export const selectCursor = (ctx: NavSearchContext) => ctx.cursor
262
+ export const selectRowKeys = (ctx: NavSearchContext) => ctx.rowKeys
263
+ export const selectSelectionAnchor = (ctx: NavSearchContext) => ctx.selectionAnchor
264
+ export const selectToggledKeys = (ctx: NavSearchContext) => ctx.toggledKeys
265
+ export const selectSearchStatus = (ctx: NavSearchContext) => ctx.search.status
266
+ export const selectSearchActive = (ctx: NavSearchContext) => ctx.search.status === "editing"
267
+ export const selectSearchQuery = (ctx: NavSearchContext) =>
268
+ ctx.search.status === "editing" ? ctx.search.query : ctx.search.committedQuery
269
+ export const selectMatches = (ctx: NavSearchContext) => ctx.search.matches
270
+ export const selectCurrentMatchIndex = (ctx: NavSearchContext) => ctx.search.currentMatchIndex
271
+ export function deriveSelection(ctx: NavSearchContext, mode: Mode) {
272
+ return mode === "select" && ctx.selectionAnchor !== null && ctx.cursor !== null
273
+ ? {
274
+ start: Math.min(ctx.selectionAnchor, ctx.cursor),
275
+ end: Math.max(ctx.selectionAnchor, ctx.cursor),
276
+ }
277
+ : null
278
+ }
@@ -1,20 +1,21 @@
1
- import { useCallback, useRef, useState } from "react"
1
+ import { useCallback, useEffect, useRef, useState } from "react"
2
+ import { useSelector } from "@xstate/store-react"
2
3
  import { useCommand, useMode, useSetMode, type Mode } from "@tooee/commands"
4
+ import {
5
+ createNavSearchStore,
6
+ selectCurrentMatchIndex,
7
+ selectMatches,
8
+ selectSearchActive,
9
+ selectSearchQuery,
10
+ type NavSearchDeps,
11
+ type NavSearchStore,
12
+ type RowKey,
13
+ } from "./nav-search-store.js"
3
14
 
4
15
  export interface UseSearchOptions {
5
16
  match: (query: string) => number[]
6
17
  onJump: (index: number) => void
7
- /**
8
- * Register the search commands (default true). `false` leaves the state
9
- * inert: no `/`, `n`, `shift+n` or cancel command exists.
10
- */
11
18
  enabled?: boolean
12
- /**
13
- * Values the searched content is derived from. Matches are memoized on the
14
- * query, so a committed query goes stale when the content changes underneath
15
- * it (streaming or reloaded documents). List the content here to re-match.
16
- * The array length must be stable across renders.
17
- */
18
19
  deps?: readonly unknown[]
19
20
  }
20
21
 
@@ -27,74 +28,87 @@ export interface SearchState {
27
28
  submitSearch: () => void
28
29
  }
29
30
 
30
- const EMPTY: number[] = []
31
- const EMPTY_DEPS: readonly unknown[] = []
32
-
33
- interface MatchCache {
34
- query: string
35
- deps: readonly unknown[]
36
- matches: number[]
37
- }
38
-
39
- function sameDeps(left: readonly unknown[], right: readonly unknown[]): boolean {
40
- if (left.length !== right.length) return false
41
- return left.every((value, index) => Object.is(value, right[index]))
42
- }
43
31
  const CURSOR_MODES: Mode[] = ["cursor"]
44
32
  const ALL_MODES: Mode[] = ["cursor", "select", "insert"]
45
33
 
46
- export function useSearch({
47
- match,
48
- onJump,
49
- enabled = true,
50
- deps = EMPTY_DEPS,
51
- }: UseSearchOptions): SearchState {
34
+ export function useNavSearchStore(options: {
35
+ keys: readonly RowKey[]
36
+ isSelectable?: (index: number) => boolean
37
+ preserveCursorByKey?: boolean
38
+ }): NavSearchStore {
39
+ const depsRef = useRef<NavSearchDeps>({ isSelectable: options.isSelectable ?? (() => true) })
40
+ depsRef.current.isSelectable = options.isSelectable ?? (() => true)
41
+ const [store] = useState(() =>
42
+ createNavSearchStore({ keys: options.keys, deps: depsRef.current }),
43
+ )
44
+ useEffect(() => {
45
+ const currentKeys = store.getSnapshot().context.rowKeys
46
+ const keysChanged =
47
+ currentKeys.length !== options.keys.length ||
48
+ currentKeys.some((key, index) => !Object.is(key, options.keys[index]))
49
+ if (keysChanged) {
50
+ store.trigger.rowsChanged({
51
+ keys: options.keys,
52
+ preserveCursorByKey: options.preserveCursorByKey,
53
+ })
54
+ }
55
+ }, [store, options.keys, options.preserveCursorByKey])
56
+ return store
57
+ }
58
+
59
+ export function useSearchBindings(
60
+ store: NavSearchStore,
61
+ {
62
+ match,
63
+ onJump,
64
+ enabled = true,
65
+ deps = [],
66
+ }: Omit<UseSearchOptions, "onJump"> & { onJump?: (index: number) => void },
67
+ ): SearchState {
52
68
  const mode = useMode()
53
69
  const setMode = useSetMode()
54
-
55
- const [searchQuery, setSearchQuery] = useState("")
56
- const [searchActive, setSearchActive] = useState(false)
57
- const [currentMatchIndex, setCurrentMatchIndex] = useState(0)
58
- const [committedQuery, setCommittedQuery] = useState("")
59
- const preSearchModeRef = useRef<Mode>("cursor")
60
-
61
70
  const matchRef = useRef(match)
62
71
  matchRef.current = match
63
-
64
72
  const onJumpRef = useRef(onJump)
65
73
  onJumpRef.current = onJump
66
74
 
67
- const activeQuery = searchActive ? searchQuery : committedQuery
68
-
69
- // Memoized on the query *and* the searched content: a committed query must
70
- // re-match when rows arrive or change. `deps` is caller-sized, so this is a
71
- // hand-rolled cache rather than a useMemo dependency array.
72
- const cacheRef = useRef<MatchCache | null>(null)
73
- let matchingLines: number[]
74
- if (!activeQuery) {
75
- matchingLines = EMPTY
76
- } else {
77
- const cache = cacheRef.current
78
- if (cache && cache.query === activeQuery && sameDeps(cache.deps, deps)) {
79
- matchingLines = cache.matches
80
- } else {
81
- matchingLines = matchRef.current(activeQuery)
82
- cacheRef.current = { query: activeQuery, deps: [...deps], matches: matchingLines }
83
- }
84
- }
85
-
86
- const matchingLinesRef = useRef(matchingLines)
87
- matchingLinesRef.current = matchingLines
88
-
89
- // Imperatively set search query, reset match index, and jump to first match.
90
- const updateSearchQuery = useCallback((query: string) => {
91
- setSearchQuery(query)
92
- setCurrentMatchIndex(0)
93
- const matches = query ? matchRef.current(query) : []
94
- if (matches[0] != null) {
95
- onJumpRef.current(matches[0])
96
- }
97
- }, [])
75
+ useEffect(() => {
76
+ const subscription = store.on("jumped", ({ index }) => onJumpRef.current?.(index))
77
+ return () => subscription.unsubscribe()
78
+ }, [store])
79
+ useEffect(() => {
80
+ const subscription = store.on("restoreMode", ({ mode: restored }) => setMode(restored))
81
+ return () => subscription.unsubscribe()
82
+ }, [store, setMode])
83
+
84
+ const searchQuery = useSelector(store, (snapshot) => selectSearchQuery(snapshot.context))
85
+ const searchActive = useSelector(store, (snapshot) => selectSearchActive(snapshot.context))
86
+ const matchingLines = useSelector(store, (snapshot) =>
87
+ selectMatches(snapshot.context),
88
+ ) as number[]
89
+ const currentMatchIndex = useSelector(store, (snapshot) =>
90
+ selectCurrentMatchIndex(snapshot.context),
91
+ )
92
+
93
+ const updateSearchQuery = useCallback(
94
+ (query: string) => {
95
+ store.trigger.searchChanged({ query, matches: query ? matchRef.current(query) : [] })
96
+ },
97
+ [store],
98
+ )
99
+
100
+ // Re-match a committed query once when the caller-declared content changes.
101
+ const depsRef = useRef(deps)
102
+ useEffect(() => {
103
+ if (
104
+ depsRef.current.every((value, index) => Object.is(value, deps[index])) &&
105
+ depsRef.current.length === deps.length
106
+ )
107
+ return
108
+ depsRef.current = deps
109
+ const query = selectSearchQuery(store.getSnapshot().context)
110
+ if (query) store.trigger.searchChanged({ query, matches: matchRef.current(query) })
111
+ }, [deps, store])
98
112
 
99
113
  useCommand({
100
114
  id: "cursor-search-start",
@@ -103,90 +117,49 @@ export function useSearch({
103
117
  modes: CURSOR_MODES,
104
118
  enabled,
105
119
  handler: () => {
106
- preSearchModeRef.current = mode
107
- setSearchActive(true)
108
- setSearchQuery("")
120
+ store.trigger.searchStarted({ mode })
109
121
  setMode("insert")
110
122
  },
111
123
  })
112
-
113
124
  useCommand({
114
125
  id: "cursor-search-next",
115
126
  title: "Next match",
116
127
  hotkey: "n",
117
128
  modes: CURSOR_MODES,
118
129
  enabled,
119
- when: () => !searchActive,
120
- handler: () => {
121
- const matches = matchingLinesRef.current
122
- if (matches.length === 0) return
123
-
124
- setCurrentMatchIndex((index) => {
125
- const nextIndex = (index + 1) % matches.length
126
- const nextMatch = matches[nextIndex]
127
- if (nextMatch != null) {
128
- onJumpRef.current(nextMatch)
129
- }
130
- return nextIndex
131
- })
132
- },
130
+ when: () => !selectSearchActive(store.getSnapshot().context),
131
+ handler: () => store.trigger.searchNext({}),
133
132
  })
134
-
135
133
  useCommand({
136
134
  id: "cursor-search-prev",
137
135
  title: "Previous match",
138
136
  hotkey: "shift+n",
139
137
  modes: CURSOR_MODES,
140
138
  enabled,
141
- when: () => !searchActive,
142
- handler: () => {
143
- const matches = matchingLinesRef.current
144
- if (matches.length === 0) return
145
-
146
- setCurrentMatchIndex((index) => {
147
- const nextIndex = (index - 1 + matches.length) % matches.length
148
- const nextMatch = matches[nextIndex]
149
- if (nextMatch != null) {
150
- onJumpRef.current(nextMatch)
151
- }
152
- return nextIndex
153
- })
154
- },
139
+ when: () => !selectSearchActive(store.getSnapshot().context),
140
+ handler: () => store.trigger.searchPrevious({}),
155
141
  })
156
-
157
142
  useCommand({
158
143
  id: "search-cancel",
159
144
  title: "Cancel search",
160
145
  hotkey: "escape",
161
146
  modes: ALL_MODES,
162
147
  enabled,
163
- when: () => searchActive,
164
- handler: () => {
165
- setSearchActive(false)
166
- setSearchQuery("")
167
- setCommittedQuery("")
168
- setCurrentMatchIndex(0)
169
- setMode(preSearchModeRef.current)
170
- },
148
+ when: () => selectSearchActive(store.getSnapshot().context),
149
+ handler: () => store.trigger.searchCancelled({}),
171
150
  })
172
151
 
173
- const submitSearch = useCallback(() => {
174
- setCommittedQuery(searchQuery)
175
- setSearchActive(false)
176
- setCurrentMatchIndex(0)
177
- const matches = searchQuery ? matchRef.current(searchQuery) : []
178
- if (matches[0] != null) {
179
- onJumpRef.current(matches[0])
180
- }
181
- setMode(preSearchModeRef.current)
182
- }, [searchQuery, setMode])
183
-
184
152
  return {
185
153
  searchQuery,
186
154
  searchActive,
187
155
  setSearchQuery: updateSearchQuery,
188
156
  matchingLines,
189
157
  currentMatchIndex,
190
- submitSearch,
158
+ submitSearch: () => store.trigger.searchSubmitted({}),
191
159
  }
192
160
  }
161
+
162
+ export function useSearch(options: UseSearchOptions): SearchState {
163
+ const store = useNavSearchStore({ keys: [] })
164
+ return useSearchBindings(store, options)
165
+ }