diff --git a/website/components/NixFunctions/nixFunctions.tsx b/website/components/NixFunctions/nixFunctions.tsx index b8467fe..2461abe 100644 --- a/website/components/NixFunctions/nixFunctions.tsx +++ b/website/components/NixFunctions/nixFunctions.tsx @@ -1,50 +1,62 @@ import { Box } from "@mui/system"; import { useEffect, useMemo } from "react"; -import { PageState } from "../../models/internals"; +import { PageState, normalizePath } from "../../models/internals"; import { byType, pipe } from "../../queries"; import { DocItem } from "../../models/nix"; import { BasicList, BasicListItem } from "../basicList"; import FunctionItem from "../functionItem/functionItem"; import { SetPageStateVariable } from "../pageContext"; import { useMiniSearch } from "react-minisearch"; - interface FunctionsProps { pageState: PageState; setPageStateVariable: SetPageStateVariable; } +export const commonSearchOptions = { + // allow 22% levenshtein distance (e.g. 2.2 of 10 characters don't match) + fuzzy: 0.22, + // prefer to show builtins first + boostDocument: (id: string, term: string) => { + let boost = 1; + boost *= id.includes("builtins") ? 10 : 1; + boost *= id.includes(term) ? 100 : 1; + return boost; + }, + boost: { + id: 1000, + name: 100, + category: 10, + example: 0.5, + fn_type: 10, + description: 1, + }, +}; + export function NixFunctions(props: FunctionsProps) { const { pageState, setPageStateVariable } = props; const { data, selected, term, filter } = pageState; const setSelected = setPageStateVariable("selected"); - const { search, searchResults, rawResults } = useMiniSearch(data, { + const minisearch = useMiniSearch(data, { fields: ["id", "name", "category", "description", "example", "fn_type"], - searchOptions: { - // allow 22% levenshtein distance (e.g. 2.2 of 10 characters don't match) - fuzzy: 0.22, - // prefer to show builtins first - boostDocument: (id, term) => { - let boost = 1; - boost *= id.includes("builtins") ? 10 : 1; - boost *= id.includes(term) ? 10 : 1; - return boost; - }, - boost: { - id: 1000, - name: 100, - category: 10, - example: 0.5, - fn_type: 10, - description: 1, - }, - }, + searchOptions: commonSearchOptions, + idField: "id", tokenize: (text: string, fieldName): string[] => { + let normalizedId = normalizePath(text); + const tokens = []; + + if (fieldName === "id") { + tokens.push(text); + tokens.push(normalizedId); + tokens.push(...normalizedId.split(".")); + } + //split the text into words const wordTokens = text.split(/\W/); const containsUpper = (w: string) => Boolean(w.match(/[A-Z]/)?.length); - const tokens = [ + tokens.push( + text, // include the words itself if they contain upperCharacters // mapAttrs -> mapAttrs ...wordTokens.filter(containsUpper), @@ -56,15 +68,17 @@ export function NixFunctions(props: FunctionsProps) { .flat(), // just include lowercase words without further tokenizing // map -> map - ...wordTokens.filter((w) => !containsUpper(w)), - ]; - return tokens; + ...wordTokens.filter((w) => !containsUpper(w)) + ); + return tokens.filter(Boolean); }, }); + const { search, searchResults, rawResults } = minisearch; //initial site-load is safe to call useEffect(() => { search(term); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -103,7 +117,7 @@ export function NixFunctions(props: FunctionsProps) { return ( - + ); } diff --git a/website/components/basicList/basicList.tsx b/website/components/basicList/basicList.tsx index 0562316..01e0c81 100644 --- a/website/components/basicList/basicList.tsx +++ b/website/components/basicList/basicList.tsx @@ -8,8 +8,9 @@ import { usePageContext } from "../pageContext"; import { useMobile } from "../layout/layout"; import { EmptyRecordsPlaceholder } from "../emptyRecordsPlaceholder"; import { FunctionOfTheDay } from "../functionOfTheDay"; -import { Query, SearchOptions } from "minisearch"; import { ViewMode } from "../../models/internals"; +import { UseMiniSearch } from "react-minisearch"; +import { DocItem } from "../../models/nix"; export type BasicListItem = { item: React.ReactNode; @@ -17,11 +18,12 @@ export type BasicListItem = { }; export type BasicListProps = BasicDataViewProps & { selected?: string | null; - search: (query: Query, options?: SearchOptions) => void; + minisearch: UseMiniSearch; }; export function BasicList(props: BasicListProps) { - const { items, search } = props; + const { items, minisearch } = props; + const { search, suggestions, autoSuggest, clearSuggestions } = minisearch; const { pageState, setPageStateVariable, resetQueries } = usePageContext(); const isMobile = useMobile(); const { page, itemsPerPage, FOTD: showFunctionOfTheDay, data } = pageState; @@ -72,6 +74,9 @@ export function BasicList(props: BasicListProps) { handleClear={handleClear} placeholder="search nix functions" handleSearch={handleSearch} + suggestions={suggestions || []} + autoSuggest={autoSuggest} + clearSuggestions={clearSuggestions} /> {showFunctionOfTheDay && ( void; handleFilter: (filter: Filter | ((curr: Filter) => Filter)) => void; placeholder: string; + suggestions: Suggestion[]; + autoSuggest: (query: string, options?: SearchOptions) => void; + clearSuggestions: () => void; } export function SearchInput(props: SearchInputProps) { - const { handleSearch, placeholder, handleFilter, handleClear } = props; + const { + handleSearch, + placeholder, + handleFilter, + handleClear, + suggestions, + autoSuggest, + clearSuggestions, + } = props; const { pageState } = usePageContext(); const { filter, term } = pageState; const [_term, _setTerm] = useState(term); @@ -37,14 +49,28 @@ export function SearchInput(props: SearchInputProps) { const _handleClear = () => { _setTerm(""); handleClear(); + clearSuggestions(); }; const handleType = ( e: React.ChangeEvent ) => { _setTerm(e.target.value); + autoSuggest(e.target.value, { + fuzzy: 0.25, + fields: ["id", "name", "category"], + }); debouncedSubmit(e.target.value); }; + const autoCompleteOptions = useMemo(() => { + const options = suggestions + .slice(0, 5) + .map((s) => s.terms) + .flat(); + const sorted = options.sort((a, b) => -b.localeCompare(a)); + return [...new Set(sorted)]; + }, [suggestions]); + return ( <> - { + handleType({ + target: { value: value || "" }, + } as React.ChangeEvent); }} - placeholder={placeholder} - inputProps={{ "aria-label": "search-input" }} value={_term} - onChange={(e) => handleType(e)} + renderInput={(params) => ( + // + handleType(e)} + {...params} + {...params.InputProps} + endAdornment={undefined} + /> + )} /> + { const start = substr.slice(0, 1); const end = substr.slice(1); - console.log({ start, end }); return start.toUpperCase() + end; }) .join(""); diff --git a/website/queries/byType.ts b/website/queries/byType.ts index 13e7373..c83d756 100644 --- a/website/queries/byType.ts +++ b/website/queries/byType.ts @@ -2,26 +2,23 @@ import { MetaData, NixType } from "../models/nix"; import { getTypes } from "./lib"; export const byType = -({ to, from }: { to: NixType; from: NixType }) => -(data: MetaData): MetaData => { - if (to === "any" && from === "any") { - return data; - } else { - return data.filter( - // TODO: Implement proper type matching - ({ name, fn_type }) => { - if (fn_type) { - const parsedType = getTypes(name, fn_type); - // if(name === "derivation"){ - // console.log({name,parsedType,fn_type}); - // } - return ( - parsedType.args.includes(from) && parsedType.types.includes(to) - ); - } else { - return to === "any" && from === "any"; + ({ to, from }: { to: NixType; from: NixType }) => + (data: MetaData): MetaData => { + if (to === "any" && from === "any") { + return data; + } else { + return data.filter( + // TODO: Implement proper type matching + ({ name, fn_type }) => { + if (fn_type) { + const parsedType = getTypes(name, fn_type); + return ( + parsedType.args.includes(from) && parsedType.types.includes(to) + ); + } else { + return to === "any" && from === "any"; + } } - } - ); - } -}; \ No newline at end of file + ); + } + };