Files
hapi/shared/src/search.ts
T
AnanovoandGitHub 7909c46fff feat(search): support wildcard patterns across search fields (#1571)
* feat(search): add wildcard matching to search fields

* fix(search): harden wildcard matching and file globs

* fix(search): align file matching with shared wildcard semantics

* fix(search): bound file wildcard search in runner

* fix(search): normalize outline queries through shared matcher

* fix(web): remove duplicate markdown test context field
2026-08-16 22:41:24 +08:00

68 lines
2.2 KiB
TypeScript

/** Whether a search query should be interpreted as a wildcard pattern. */
export function isWildcardSearch(query: string): boolean {
return query.includes('*') || query.includes('?')
}
function matchesWildcard(value: string, pattern: string): boolean {
const input = Array.from(value.toLowerCase())
const tokens = Array.from(pattern.toLowerCase())
let inputIndex = 0
let tokenIndex = 0
let starIndex = -1
let retryInputIndex = 0
while (inputIndex < input.length) {
const token = tokens[tokenIndex]
if (token !== undefined && (token === '?' || token === input[inputIndex])) {
inputIndex++
tokenIndex++
} else if (token === '*') {
starIndex = tokenIndex++
retryInputIndex = inputIndex
} else if (starIndex >= 0) {
tokenIndex = starIndex + 1
inputIndex = ++retryInputIndex
} else {
return false
}
}
while (tokens[tokenIndex] === '*') {
tokenIndex++
}
return tokenIndex === tokens.length
}
/** Match a value using the existing substring behavior or supported wildcards. */
export function matchesSearchQuery(value: string, query: string): boolean {
const normalizedQuery = query.trim()
if (!normalizedQuery) return true
if (!isWildcardSearch(normalizedQuery)) {
return value.toLowerCase().includes(normalizedQuery.toLowerCase())
}
return matchesWildcard(value, normalizedQuery)
}
function escapeRipgrepGlobSyntax(pattern: string): string {
let escaped = ''
for (const [index, character] of Array.from(pattern).entries()) {
if (character === '\\' || character === '[' || character === ']' || character === '{' || character === '}') {
escaped += `\\${character}`
} else if (index === 0 && character === '!') {
escaped += '\\!'
} else {
escaped += character
}
}
return escaped
}
/** Build a conservative ripgrep glob for plain-text file-search prefiltering. */
export function toSearchGlob(query: string): string {
const normalizedQuery = query.trim()
const pattern = isWildcardSearch(normalizedQuery) ? normalizedQuery : `*${normalizedQuery}*`
return escapeRipgrepGlobSyntax(pattern)
}