import { Text } from '@codemirror/state' import { RegExpCursor, SearchCursor, SearchQuery } from '@codemirror/search' import { ProjectSnapshot } from '@/infrastructure/project-snapshot' import { categorizer, regexpWordTest, stringWordTest } from './search' import { sendSearchEvent } from '@/features/event-tracking/search-events' export type Hit = { lineIndex: number matchIndex: number length: number } export type MatchedFile = { path: string lines: string[] hits: Hit[] } const toLowerCase = (string: string) => string.toLowerCase() export const searchSnapshot = async ( projectSnapshot: ProjectSnapshot, searchQuery: SearchQuery ) => { if (!searchQuery.search.trim().length) { return } const matchedFiles = new Map() const createCursor = (text: Text) => { if (searchQuery.regexp) { return new RegExpCursor(text, searchQuery.search, { ignoreCase: !searchQuery.caseSensitive, test: searchQuery.wholeWord ? regexpWordTest(categorizer) : undefined, }) } return new SearchCursor( text, searchQuery.search, undefined, undefined, searchQuery.caseSensitive ? undefined : toLowerCase, searchQuery.wholeWord ? stringWordTest(text, categorizer) : undefined ) } const docPaths = projectSnapshot.getDocPaths() for (const path of docPaths) { const content = projectSnapshot.getDocContents(path) if (content) { const lines = content.split('\n') const text = Text.of(lines) const cursor = createCursor(text) while (!cursor.next().done) { const { from, to } = cursor.value const matchedFile: MatchedFile = matchedFiles.get(path) ?? { path, lines, hits: [], } const line = text.lineAt(from) matchedFile.hits.push({ lineIndex: line.number - 1, matchIndex: from - line.from, length: to - from, }) matchedFiles.set(path, matchedFile) } } } const results = [...matchedFiles.values()].sort((a, b) => a.path.localeCompare(b.path) ) sendSearchEvent('search-execute', { searchType: 'full-project', totalDocs: docPaths.length, totalResults: results.flatMap(file => file.hits).length, }) return results }