126 lines
3.0 KiB
TypeScript
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)
|
|
))
|
|
}
|