import type { EntityID } from '@wovin/core/applog' import type { SearchGrammarType } from '../search/grammar-types' import { joinThreads } from '@wovin/core/applog' import { Logger } from 'besonders-logger' import { Grammar, Parser } from 'nearley' import searchGrammar from '../search/grammar-compiled' import { useAllTags, useBlocksWithTags, useRootsOfMaybeNested, useThreadFromContext, withDS } from '../ui/reactive' import { useBlocksMatchingQuery } from './search' import { useBlk } from './VMs/BlockVM' const { WARN, LOG, DEBUG, VERBOSE, ERROR } = Logger.setup(Logger.VERBOSE) // eslint-disable-line unused-imports/no-unused-vars export interface SmartQuery { source: string ast: SearchGrammarType } export interface SmartQueryError { source: string error: string } export type SmartQueryOrError = SmartQuery | SmartQueryError export function smartQueryIsError(s: SmartQueryOrError): s is SmartQueryError { return (s as any).error } export interface SmartList { query: SmartQuery title?: string blocks?: readonly EntityID[] } export function parseSmartQuery(queryStr: string): SmartQueryOrError { // if (queryStr.length < 3) return null // HACK: safequard against querying the whole world while starting to type (should be done on UI level I guess) const parser = new Parser(Grammar.fromCompiled(searchGrammar)) // TODO: optimize by tokenizing ? - https://nearley.js.org/docs/tokenizers try { const parsed = parser.feed(queryStr.trim()) DEBUG(`Parsed query:`, parsed.results) if (parsed.results.length < 1) { WARN(`Parsed query is empty`, { queryStr, parsed }) throw new SyntaxError('Parsing syntax yields no result') } if (parsed.results.length > 1) WARN(`Parsed query is ambiguous - send manu back to the grammar playground`, { queryStr, parsed }) // const tokens = queryStr.trim().split(/\s+/) // const tags = tokens.filter(tag => RE_ANY_TAG_ONLY.exec(tag)) // const textTokens = tokens.filter(token => !tags.includes(token)) return { source: queryStr, ast: parsed.results[0], } satisfies SmartQuery } catch (error) { WARN(`SmartQuery error`, error) return { source: queryStr, error: (error as any).message || error.toString() } satisfies SmartQueryError } } export function getSmartLists(query: SmartQueryOrError, { excludeBlocks, exclusiveGroups }: { excludeBlocks?: Set exclusiveGroups?: boolean } = {}) { VERBOSE(`[getSmartLists]`, { tags: query, excludeBlocks }) if (smartQueryIsError(query)) return null let blocksMatchingQuery = useBlocksMatchingQuery(query.ast) if (excludeBlocks) { blocksMatchingQuery = blocksMatchingQuery.filter(b => !excludeBlocks.has(b)) } blocksMatchingQuery = useRootsOfMaybeNested(blocksMatchingQuery) // ? redundant - or less work for the next one: // const threadOfAllBlocksMatchingQuery = joinThreads(blocksMatchingQuery.map(blockID => useBlk(blockID).threadWithRecursiveKids)) const threadOfAllBlocksMatchingQuery = joinThreads( blocksMatchingQuery.map(blockID => useBlk(blockID).entityThread), ) const otherTagsOfMatchedBlocks = withDS( threadOfAllBlocksMatchingQuery, () => useAllTags(), ) const categorizedBlocks = new Set() const lists = [...otherTagsOfMatchedBlocks.entries()] .sort(([, a], [, b]) => b - a) // biggest count first .filter(([tag]) => !query.source.includes(tag)) // exclude self //HACK: how to do this with new complexity .map(([tag, count]) => { let blocks = withDS(threadOfAllBlocksMatchingQuery, () => useBlocksWithTags([tag])) blocks = blocks.filter((b) => { if (excludeBlocks && excludeBlocks.has(b)) { return false } if (exclusiveGroups && categorizedBlocks.has(b)) { return false } return true }) blocks.forEach(block => categorizedBlocks.add(block)) return { title: tag, query: parseSmartQuery(tag), blocks, // count, // TODO: thread: ? } satisfies SmartList }) const uncategorizedBlocks = blocksMatchingQuery.filter(b => !categorizedBlocks.has(b)) if (uncategorizedBlocks.length) { lists.push( { title: 'Other', tag: null, blocks: uncategorizedBlocks }, ) } DEBUG(`[getSmartLists]`, query, { excludeBlocks, threadOfThisTag: threadOfAllBlocksMatchingQuery, kidTags: otherTagsOfMatchedBlocks, lists, threadContext: useThreadFromContext(), }) return lists }