Skip to content
This repository was archived by the owner on Apr 15, 2026. It is now read-only.

Commit d554699

Browse files
heymanmarijnh
authored andcommitted
Add a test field to search query objects
FEATURE: Search queries now support a generic `test` field that can be used to implement custom tests on matches.
1 parent 9a54e64 commit d554699

File tree

2 files changed

+64
-3
lines changed

2 files changed

+64
-3
lines changed

src/search.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ export class SearchQuery {
105105
/// @internal
106106
readonly unquoted: string
107107

108+
/// Optional test function used to filter matches.
109+
readonly test: ((match: string, state: EditorState, from: number, to: number) => boolean) | undefined
110+
108111
/// Create a query object.
109112
constructor(config: {
110113
/// The search string.
@@ -121,6 +124,10 @@ export class SearchQuery {
121124
replace?: string,
122125
/// Enable whole-word matching.
123126
wholeWord?: boolean
127+
128+
/// Optional function to filter matches. It is passed the matched
129+
/// string, the editor state, and the match range.
130+
test?: (match: string, state: EditorState, from: number, to: number) => boolean
124131
}) {
125132
this.search = config.search
126133
this.caseSensitive = !!config.caseSensitive
@@ -130,6 +137,7 @@ export class SearchQuery {
130137
this.valid = !!this.search && (!this.regexp || validRegExp(this.search))
131138
this.unquoted = this.unquote(this.search)
132139
this.wholeWord = !!config.wholeWord
140+
this.test = config.test
133141
}
134142

135143
/// @internal
@@ -142,7 +150,7 @@ export class SearchQuery {
142150
eq(other: SearchQuery) {
143151
return this.search == other.search && this.replace == other.replace &&
144152
this.caseSensitive == other.caseSensitive && this.regexp == other.regexp &&
145-
this.wholeWord == other.wholeWord
153+
this.wholeWord == other.wholeWord && this.test == other.test
146154
}
147155

148156
/// @internal
@@ -178,9 +186,22 @@ abstract class QueryType<Result extends SearchResult = SearchResult> {
178186
const enum FindPrev { ChunkSize = 10000 }
179187

180188
function stringCursor(spec: SearchQuery, state: EditorState, from: number, to: number) {
189+
const wordTest = spec.wholeWord ? stringWordTest(state.doc, state.charCategorizer(state.selection.main.head)) : undefined
190+
const test = (from: number, to: number, buffer: string, bufferPos: number) => {
191+
return (
192+
(wordTest ? wordTest(from, to, buffer, bufferPos) : true) &&
193+
(spec.test ? spec.test(matchFromBuffer(state, from, to, buffer, bufferPos), state, from, to) : true)
194+
)
195+
}
181196
return new SearchCursor(
182197
state.doc, spec.unquoted, from, to, spec.caseSensitive ? undefined : x => x.toLowerCase(),
183-
spec.wholeWord ? stringWordTest(state.doc, state.charCategorizer(state.selection.main.head)) : undefined)
198+
wordTest || spec.test ? test : undefined)
199+
}
200+
201+
function matchFromBuffer(state: EditorState, from: number, to: number, buffer: string, bufferPos: number) {
202+
if (from >= bufferPos && to <= bufferPos + buffer.length)
203+
return buffer.slice(from - bufferPos, to - bufferPos)
204+
return state.doc.sliceString(from, to)
184205
}
185206

186207
function stringWordTest(doc: Text, categorizer: (ch: string) => CharCategory) {
@@ -253,9 +274,17 @@ const enum RegExp { HighlightMargin = 250 }
253274
type RegExpResult = typeof RegExpCursor.prototype.value
254275

255276
function regexpCursor(spec: SearchQuery, state: EditorState, from: number, to: number) {
277+
const wordTest = spec.wholeWord ? regexpWordTest(state.charCategorizer(state.selection.main.head)) : undefined
278+
const test = (from: number, to: number, match: RegExpExecArray) => {
279+
return (
280+
(wordTest ? wordTest(from, to, match) : true) &&
281+
(spec.test ? spec.test(match[0], state, from, to) : true)
282+
)
283+
}
284+
256285
return new RegExpCursor(state.doc, spec.search, {
257286
ignoreCase: !spec.caseSensitive,
258-
test: spec.wholeWord ? regexpWordTest(state.charCategorizer(state.selection.main.head)) : undefined
287+
test: wordTest || spec.test ? test : undefined
259288
}, from, to)
260289
}
261290

test/test-query.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,36 @@ describe("SearchQuery", () => {
5858
it("can match regular expressions by word", () => {
5959
test(new SearchQuery({search: "a..", regexp: true, wholeWord: true}), "[aap] baap aapje [a--]w")
6060
})
61+
62+
it("can filter string matches with a test function", () => {
63+
test(new SearchQuery({
64+
search: "ab",
65+
test(match, state, from, to) {
66+
ist(match, state.doc.sliceString(from, to))
67+
return from > 0
68+
}
69+
}), "ab a[ab] [ab]")
70+
})
71+
72+
it("combines whole-word matches with a test function", () => {
73+
test(new SearchQuery({
74+
search: "ab",
75+
wholeWord: true,
76+
test(match, state, from, to) {
77+
ist(match, state.doc.sliceString(from, to))
78+
return from > 0
79+
}
80+
}), "ab xab [ab]")
81+
})
82+
83+
it("can filter regexp matches with a test function", () => {
84+
test(new SearchQuery({
85+
search: "a.",
86+
regexp: true,
87+
test(match, state, from, to) {
88+
ist(match, state.doc.sliceString(from, to))
89+
return match != "aa"
90+
}
91+
}), "[ab] aa [ac]")
92+
})
6193
})

0 commit comments

Comments
 (0)