Files
tolaria/src/utils/editorFind.ts
2026-04-27 10:57:58 +02:00

126 lines
3.0 KiB
TypeScript

export interface EditorFindOptions {
caseSensitive: boolean
regex: boolean
}
export interface EditorFindMatch {
from: number
text: string
to: number
}
export interface EditorFindResult {
error: string | null
matches: EditorFindMatch[]
}
export interface EditorFindChange {
from: number
insert: string
to: number
}
type CompiledPattern =
| { error: string; pattern: null }
| { error: null; pattern: RegExp | null }
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
function compileFindPattern(
query: string,
options: EditorFindOptions,
global: boolean,
): CompiledPattern {
if (query.length === 0) return { error: null, pattern: null }
const source = options.regex ? query : escapeRegExp(query)
const flags = `${global ? 'g' : ''}${options.caseSensitive ? '' : 'i'}`
try {
return { error: null, pattern: new RegExp(source, flags) }
} catch {
return { error: 'Invalid regex', pattern: null }
}
}
export function findEditorMatches(
documentText: string,
query: string,
options: EditorFindOptions,
): EditorFindResult {
const compiled = compileFindPattern(query, options, true)
if (compiled.error) return { error: compiled.error, matches: [] }
if (!compiled.pattern) return { error: null, matches: [] }
const matches: EditorFindMatch[] = []
let match: RegExpExecArray | null
while ((match = compiled.pattern.exec(documentText)) !== null) {
if (match[0].length === 0) {
return { error: 'Regex must match text', matches: [] }
}
matches.push({
from: match.index,
text: match[0],
to: match.index + match[0].length,
})
}
return { error: null, matches }
}
export function clampEditorFindIndex(index: number, matchCount: number): number {
if (matchCount <= 0) return 0
return Math.min(Math.max(index, 0), matchCount - 1)
}
export function nextEditorFindIndex(
index: number,
matchCount: number,
direction: 1 | -1,
): number {
if (matchCount <= 0) return 0
return (clampEditorFindIndex(index, matchCount) + direction + matchCount) % matchCount
}
export function replacementForEditorFindMatch(
match: EditorFindMatch,
query: string,
replacement: string,
options: EditorFindOptions,
): string {
if (!options.regex) return replacement
const compiled = compileFindPattern(query, options, false)
if (!compiled.pattern) return replacement
return match.text.replace(compiled.pattern, replacement)
}
export function buildEditorFindReplacementChange(
match: EditorFindMatch,
query: string,
replacement: string,
options: EditorFindOptions,
): EditorFindChange {
return {
from: match.from,
insert: replacementForEditorFindMatch(match, query, replacement, options),
to: match.to,
}
}
export function buildEditorFindReplacementChanges(
matches: readonly EditorFindMatch[],
query: string,
replacement: string,
options: EditorFindOptions,
): EditorFindChange[] {
return matches.map((match) => (
buildEditorFindReplacementChange(match, query, replacement, options)
))
}