mirror of
https://github.com/hsjobeki/noogle.git
synced 2024-12-26 15:34:25 +03:00
Merge pull request #44 from nix-community/feat/autocomplete-suggestions
autocomplete: init
This commit is contained in:
commit
5913852836
@ -1,34 +1,25 @@
|
|||||||
import { Box } from "@mui/system";
|
import { Box } from "@mui/system";
|
||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import { PageState } from "../../models/internals";
|
import { PageState, normalizePath } from "../../models/internals";
|
||||||
import { byType, pipe } from "../../queries";
|
import { byType, pipe } from "../../queries";
|
||||||
import { DocItem } from "../../models/nix";
|
import { DocItem } from "../../models/nix";
|
||||||
import { BasicList, BasicListItem } from "../basicList";
|
import { BasicList, BasicListItem } from "../basicList";
|
||||||
import FunctionItem from "../functionItem/functionItem";
|
import FunctionItem from "../functionItem/functionItem";
|
||||||
import { SetPageStateVariable } from "../pageContext";
|
import { SetPageStateVariable } from "../pageContext";
|
||||||
import { useMiniSearch } from "react-minisearch";
|
import { useMiniSearch } from "react-minisearch";
|
||||||
|
|
||||||
interface FunctionsProps {
|
interface FunctionsProps {
|
||||||
pageState: PageState;
|
pageState: PageState;
|
||||||
setPageStateVariable: SetPageStateVariable;
|
setPageStateVariable: SetPageStateVariable;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NixFunctions(props: FunctionsProps) {
|
export const commonSearchOptions = {
|
||||||
const { pageState, setPageStateVariable } = props;
|
|
||||||
const { data, selected, term, filter } = pageState;
|
|
||||||
|
|
||||||
const setSelected = setPageStateVariable<string | null>("selected");
|
|
||||||
|
|
||||||
const { search, searchResults, rawResults } = useMiniSearch<DocItem>(data, {
|
|
||||||
fields: ["id", "name", "category", "description", "example", "fn_type"],
|
|
||||||
searchOptions: {
|
|
||||||
// allow 22% levenshtein distance (e.g. 2.2 of 10 characters don't match)
|
// allow 22% levenshtein distance (e.g. 2.2 of 10 characters don't match)
|
||||||
fuzzy: 0.22,
|
fuzzy: 0.22,
|
||||||
// prefer to show builtins first
|
// prefer to show builtins first
|
||||||
boostDocument: (id, term) => {
|
boostDocument: (id: string, term: string) => {
|
||||||
let boost = 1;
|
let boost = 1;
|
||||||
boost *= id.includes("builtins") ? 10 : 1;
|
boost *= id.includes("builtins") ? 10 : 1;
|
||||||
boost *= id.includes(term) ? 10 : 1;
|
boost *= id.includes(term) ? 100 : 1;
|
||||||
return boost;
|
return boost;
|
||||||
},
|
},
|
||||||
boost: {
|
boost: {
|
||||||
@ -39,12 +30,33 @@ export function NixFunctions(props: FunctionsProps) {
|
|||||||
fn_type: 10,
|
fn_type: 10,
|
||||||
description: 1,
|
description: 1,
|
||||||
},
|
},
|
||||||
},
|
};
|
||||||
|
|
||||||
|
export function NixFunctions(props: FunctionsProps) {
|
||||||
|
const { pageState, setPageStateVariable } = props;
|
||||||
|
const { data, selected, term, filter } = pageState;
|
||||||
|
|
||||||
|
const setSelected = setPageStateVariable<string | null>("selected");
|
||||||
|
|
||||||
|
const minisearch = useMiniSearch<DocItem>(data, {
|
||||||
|
fields: ["id", "name", "category", "description", "example", "fn_type"],
|
||||||
|
searchOptions: commonSearchOptions,
|
||||||
|
idField: "id",
|
||||||
tokenize: (text: string, fieldName): string[] => {
|
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
|
//split the text into words
|
||||||
const wordTokens = text.split(/\W/);
|
const wordTokens = text.split(/\W/);
|
||||||
const containsUpper = (w: string) => Boolean(w.match(/[A-Z]/)?.length);
|
const containsUpper = (w: string) => Boolean(w.match(/[A-Z]/)?.length);
|
||||||
const tokens = [
|
tokens.push(
|
||||||
|
text,
|
||||||
// include the words itself if they contain upperCharacters
|
// include the words itself if they contain upperCharacters
|
||||||
// mapAttrs -> mapAttrs
|
// mapAttrs -> mapAttrs
|
||||||
...wordTokens.filter(containsUpper),
|
...wordTokens.filter(containsUpper),
|
||||||
@ -56,15 +68,17 @@ export function NixFunctions(props: FunctionsProps) {
|
|||||||
.flat(),
|
.flat(),
|
||||||
// just include lowercase words without further tokenizing
|
// just include lowercase words without further tokenizing
|
||||||
// map -> map
|
// map -> map
|
||||||
...wordTokens.filter((w) => !containsUpper(w)),
|
...wordTokens.filter((w) => !containsUpper(w))
|
||||||
];
|
);
|
||||||
return tokens;
|
return tokens.filter(Boolean);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { search, searchResults, rawResults } = minisearch;
|
||||||
//initial site-load is safe to call
|
//initial site-load is safe to call
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
search(term);
|
search(term);
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -103,7 +117,7 @@ export function NixFunctions(props: FunctionsProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ ml: { xs: 0, md: 2 } }}>
|
<Box sx={{ ml: { xs: 0, md: 2 } }}>
|
||||||
<BasicList items={preRenderedItems} search={search} />
|
<BasicList items={preRenderedItems} minisearch={minisearch} />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -8,8 +8,9 @@ import { usePageContext } from "../pageContext";
|
|||||||
import { useMobile } from "../layout/layout";
|
import { useMobile } from "../layout/layout";
|
||||||
import { EmptyRecordsPlaceholder } from "../emptyRecordsPlaceholder";
|
import { EmptyRecordsPlaceholder } from "../emptyRecordsPlaceholder";
|
||||||
import { FunctionOfTheDay } from "../functionOfTheDay";
|
import { FunctionOfTheDay } from "../functionOfTheDay";
|
||||||
import { Query, SearchOptions } from "minisearch";
|
|
||||||
import { ViewMode } from "../../models/internals";
|
import { ViewMode } from "../../models/internals";
|
||||||
|
import { UseMiniSearch } from "react-minisearch";
|
||||||
|
import { DocItem } from "../../models/nix";
|
||||||
|
|
||||||
export type BasicListItem = {
|
export type BasicListItem = {
|
||||||
item: React.ReactNode;
|
item: React.ReactNode;
|
||||||
@ -17,11 +18,12 @@ export type BasicListItem = {
|
|||||||
};
|
};
|
||||||
export type BasicListProps = BasicDataViewProps & {
|
export type BasicListProps = BasicDataViewProps & {
|
||||||
selected?: string | null;
|
selected?: string | null;
|
||||||
search: (query: Query, options?: SearchOptions) => void;
|
minisearch: UseMiniSearch<DocItem>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function BasicList(props: BasicListProps) {
|
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 { pageState, setPageStateVariable, resetQueries } = usePageContext();
|
||||||
const isMobile = useMobile();
|
const isMobile = useMobile();
|
||||||
const { page, itemsPerPage, FOTD: showFunctionOfTheDay, data } = pageState;
|
const { page, itemsPerPage, FOTD: showFunctionOfTheDay, data } = pageState;
|
||||||
@ -72,6 +74,9 @@ export function BasicList(props: BasicListProps) {
|
|||||||
handleClear={handleClear}
|
handleClear={handleClear}
|
||||||
placeholder="search nix functions"
|
placeholder="search nix functions"
|
||||||
handleSearch={handleSearch}
|
handleSearch={handleSearch}
|
||||||
|
suggestions={suggestions || []}
|
||||||
|
autoSuggest={autoSuggest}
|
||||||
|
clearSuggestions={clearSuggestions}
|
||||||
/>
|
/>
|
||||||
{showFunctionOfTheDay && (
|
{showFunctionOfTheDay && (
|
||||||
<FunctionOfTheDay
|
<FunctionOfTheDay
|
||||||
|
@ -3,12 +3,13 @@ import Paper from "@mui/material/Paper";
|
|||||||
import InputBase from "@mui/material/InputBase";
|
import InputBase from "@mui/material/InputBase";
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
import ClearIcon from "@mui/icons-material/Clear";
|
import ClearIcon from "@mui/icons-material/Clear";
|
||||||
import { Box, debounce, Grid, Typography } from "@mui/material";
|
import { Autocomplete, Box, debounce, Grid, Typography } from "@mui/material";
|
||||||
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
||||||
import { NixType, nixTypes } from "../../models/nix";
|
import { NixType, nixTypes } from "../../models/nix";
|
||||||
import SearchIcon from "@mui/icons-material/Search";
|
import SearchIcon from "@mui/icons-material/Search";
|
||||||
import { usePageContext } from "../pageContext";
|
import { usePageContext } from "../pageContext";
|
||||||
import { SelectOption } from "../selectOption";
|
import { SelectOption } from "../selectOption";
|
||||||
|
import { SearchOptions, Suggestion } from "minisearch";
|
||||||
|
|
||||||
export type Filter = { from: NixType; to: NixType };
|
export type Filter = { from: NixType; to: NixType };
|
||||||
|
|
||||||
@ -17,10 +18,21 @@ export interface SearchInputProps {
|
|||||||
handleClear: () => void;
|
handleClear: () => void;
|
||||||
handleFilter: (filter: Filter | ((curr: Filter) => Filter)) => void;
|
handleFilter: (filter: Filter | ((curr: Filter) => Filter)) => void;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
|
suggestions: Suggestion[];
|
||||||
|
autoSuggest: (query: string, options?: SearchOptions) => void;
|
||||||
|
clearSuggestions: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SearchInput(props: SearchInputProps) {
|
export function SearchInput(props: SearchInputProps) {
|
||||||
const { handleSearch, placeholder, handleFilter, handleClear } = props;
|
const {
|
||||||
|
handleSearch,
|
||||||
|
placeholder,
|
||||||
|
handleFilter,
|
||||||
|
handleClear,
|
||||||
|
suggestions,
|
||||||
|
autoSuggest,
|
||||||
|
clearSuggestions,
|
||||||
|
} = props;
|
||||||
const { pageState } = usePageContext();
|
const { pageState } = usePageContext();
|
||||||
const { filter, term } = pageState;
|
const { filter, term } = pageState;
|
||||||
const [_term, _setTerm] = useState(term);
|
const [_term, _setTerm] = useState(term);
|
||||||
@ -37,14 +49,28 @@ export function SearchInput(props: SearchInputProps) {
|
|||||||
const _handleClear = () => {
|
const _handleClear = () => {
|
||||||
_setTerm("");
|
_setTerm("");
|
||||||
handleClear();
|
handleClear();
|
||||||
|
clearSuggestions();
|
||||||
};
|
};
|
||||||
const handleType = (
|
const handleType = (
|
||||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||||
) => {
|
) => {
|
||||||
_setTerm(e.target.value);
|
_setTerm(e.target.value);
|
||||||
|
autoSuggest(e.target.value, {
|
||||||
|
fuzzy: 0.25,
|
||||||
|
fields: ["id", "name", "category"],
|
||||||
|
});
|
||||||
debouncedSubmit(e.target.value);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Paper
|
<Paper
|
||||||
@ -65,6 +91,20 @@ export function SearchInput(props: SearchInputProps) {
|
|||||||
<IconButton aria-label="clear-button" onClick={_handleClear}>
|
<IconButton aria-label="clear-button" onClick={_handleClear}>
|
||||||
<ClearIcon />
|
<ClearIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
|
<Autocomplete
|
||||||
|
// disablePortal
|
||||||
|
// id="combo-box-demo"
|
||||||
|
options={autoCompleteOptions}
|
||||||
|
sx={{ width: "100%" }}
|
||||||
|
onChange={(e, value) => {
|
||||||
|
handleType({
|
||||||
|
target: { value: value || "" },
|
||||||
|
} as React.ChangeEvent<HTMLInputElement>);
|
||||||
|
}}
|
||||||
|
value={_term}
|
||||||
|
renderInput={(params) => (
|
||||||
|
// <InputBase {...params} {...params.InputProps} />
|
||||||
<InputBase
|
<InputBase
|
||||||
autoFocus
|
autoFocus
|
||||||
sx={{
|
sx={{
|
||||||
@ -74,10 +114,16 @@ export function SearchInput(props: SearchInputProps) {
|
|||||||
p: 1,
|
p: 1,
|
||||||
}}
|
}}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
inputProps={{ "aria-label": "search-input" }}
|
// inputProps={{ "aria-label": "search-input" }}
|
||||||
value={_term}
|
value={_term}
|
||||||
onChange={(e) => handleType(e)}
|
onChange={(e) => handleType(e)}
|
||||||
|
{...params}
|
||||||
|
{...params.InputProps}
|
||||||
|
endAdornment={undefined}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
sx={{
|
sx={{
|
||||||
p: 1,
|
p: 1,
|
||||||
|
@ -40,7 +40,6 @@ export const normalizePath = (id: string) => {
|
|||||||
|
|
||||||
const start = substr.slice(0, 1);
|
const start = substr.slice(0, 1);
|
||||||
const end = substr.slice(1);
|
const end = substr.slice(1);
|
||||||
console.log({ start, end });
|
|
||||||
return start.toUpperCase() + end;
|
return start.toUpperCase() + end;
|
||||||
})
|
})
|
||||||
.join("");
|
.join("");
|
||||||
|
@ -2,8 +2,8 @@ import { MetaData, NixType } from "../models/nix";
|
|||||||
import { getTypes } from "./lib";
|
import { getTypes } from "./lib";
|
||||||
|
|
||||||
export const byType =
|
export const byType =
|
||||||
({ to, from }: { to: NixType; from: NixType }) =>
|
({ to, from }: { to: NixType; from: NixType }) =>
|
||||||
(data: MetaData): MetaData => {
|
(data: MetaData): MetaData => {
|
||||||
if (to === "any" && from === "any") {
|
if (to === "any" && from === "any") {
|
||||||
return data;
|
return data;
|
||||||
} else {
|
} else {
|
||||||
@ -12,9 +12,6 @@ export const byType =
|
|||||||
({ name, fn_type }) => {
|
({ name, fn_type }) => {
|
||||||
if (fn_type) {
|
if (fn_type) {
|
||||||
const parsedType = getTypes(name, fn_type);
|
const parsedType = getTypes(name, fn_type);
|
||||||
// if(name === "derivation"){
|
|
||||||
// console.log({name,parsedType,fn_type});
|
|
||||||
// }
|
|
||||||
return (
|
return (
|
||||||
parsedType.args.includes(from) && parsedType.types.includes(to)
|
parsedType.args.includes(from) && parsedType.types.includes(to)
|
||||||
);
|
);
|
||||||
@ -24,4 +21,4 @@ export const byType =
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user