diff --git a/api-editor/gui/package-lock.json b/api-editor/gui/package-lock.json index e18a001ed..0370de2ef 100644 --- a/api-editor/gui/package-lock.json +++ b/api-editor/gui/package-lock.json @@ -17,6 +17,7 @@ "@emotion/styled": "^11.9.3", "@reduxjs/toolkit": "^1.8.3", "chart.js": "^3.8.0", + "fastest-levenshtein": "^1.0.12", "framer-motion": "^6.3.16", "idb-keyval": "^6.2.0", "katex": "^0.16.0", @@ -5136,6 +5137,11 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, + "node_modules/fastest-levenshtein": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", + "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==" + }, "node_modules/fault": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", @@ -15822,6 +15828,11 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, + "fastest-levenshtein": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", + "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==" + }, "fault": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", diff --git a/api-editor/gui/package.json b/api-editor/gui/package.json index bc97f758e..2586fd303 100644 --- a/api-editor/gui/package.json +++ b/api-editor/gui/package.json @@ -21,6 +21,7 @@ "@emotion/styled": "^11.9.3", "@reduxjs/toolkit": "^1.8.3", "chart.js": "^3.8.0", + "fastest-levenshtein": "^1.0.12", "framer-motion": "^6.3.16", "idb-keyval": "^6.2.0", "katex": "^0.16.0", diff --git a/api-editor/gui/src/features/filter/FilterInput.tsx b/api-editor/gui/src/features/filter/FilterInput.tsx index 9ab88f830..6249d6d33 100644 --- a/api-editor/gui/src/features/filter/FilterInput.tsx +++ b/api-editor/gui/src/features/filter/FilterInput.tsx @@ -9,12 +9,14 @@ import { PopoverContent, PopoverHeader, PopoverTrigger, + Text as ChakraText, UnorderedList, } from '@chakra-ui/react'; +import { closest, distance } from 'fastest-levenshtein'; import React from 'react'; import { useAppDispatch, useAppSelector } from '../../app/hooks'; import { selectFilterString, setFilterString } from '../ui/uiSlice'; -import { isValidFilterToken } from './model/filterFactory'; +import { getFixedFilterNames, isValidFilterToken } from './model/filterFactory'; export const FilterInput: React.FC = function () { const dispatch = useAppDispatch(); @@ -51,7 +53,7 @@ export const FilterInput: React.FC = function () { {invalidTokens.map((token) => ( - {token} + ))} @@ -61,3 +63,41 @@ export const FilterInput: React.FC = function () { ); }; + +interface InvalidFilterTokenProps { + token: string; +} + +const InvalidFilterToken: React.FC = function ({ token }) { + const dispatch = useAppDispatch(); + + const alternatives = getFixedFilterNames(); + const closestAlternative = closest(token.toLowerCase(), alternatives); + const closestDistance = distance(token.toLowerCase(), closestAlternative); + + const filterString = useAppSelector(selectFilterString); + + const onClick = () => { + dispatch(setFilterString(filterString.replace(token, closestAlternative))); + }; + + return ( + + + + {token} + + {closestDistance <= 3 && ( + <> + {'. '} + Did you mean{' '} + + {closestAlternative} + + ? + + )} + + + ); +}; diff --git a/api-editor/gui/src/features/filter/model/filterFactory.ts b/api-editor/gui/src/features/filter/model/filterFactory.ts index c108a3ce0..bfaa5b44c 100644 --- a/api-editor/gui/src/features/filter/model/filterFactory.ts +++ b/api-editor/gui/src/features/filter/model/filterFactory.ts @@ -52,81 +52,58 @@ const parsePotentiallyNegatedToken = function (token: string): Optional { - // Filters with fixed text - switch (token.toLowerCase()) { - // Declaration type - case 'is:module': - return new DeclarationTypeFilter(DeclarationType.Module); - case 'is:class': - return new DeclarationTypeFilter(DeclarationType.Class); - case 'is:function': - return new DeclarationTypeFilter(DeclarationType.Function); - case 'is:parameter': - return new DeclarationTypeFilter(DeclarationType.Parameter); - - // Visibility - case 'is:public': - return new VisibilityFilter(Visibility.Public); - case 'is:internal': - return new VisibilityFilter(Visibility.Internal); - - // Parameter required or optional - case 'is:required': - return new RequiredOrOptionalFilter(RequiredOrOptional.Required); - case 'is:optional': - return new RequiredOrOptionalFilter(RequiredOrOptional.Optional); - - // Parameter assignment - case 'is:implicit': - return new ParameterAssignmentFilter(PythonParameterAssignment.IMPLICIT); - case 'is:positiononly': - return new ParameterAssignmentFilter(PythonParameterAssignment.POSITION_ONLY); - case 'is:positionorname': - return new ParameterAssignmentFilter(PythonParameterAssignment.POSITION_OR_NAME); - case 'is:positionalvararg': - return new ParameterAssignmentFilter(PythonParameterAssignment.POSITIONAL_VARARG); - case 'is:nameonly': - return new ParameterAssignmentFilter(PythonParameterAssignment.NAME_ONLY); - case 'is:namedvararg': - return new ParameterAssignmentFilter(PythonParameterAssignment.NAMED_VARARG); - - // Done - case 'is:done': - return new DoneFilter(); - - // Annotations - case 'annotation:any': - return new AnnotationFilter(AnnotationType.Any); - case 'annotation:@boundary': - return new AnnotationFilter(AnnotationType.Boundary); - case 'annotation:@calledafter': - return new AnnotationFilter(AnnotationType.CalledAfter); - case 'is:complete': // Deliberate special case. It should be transparent to users it's an annotation. - return new AnnotationFilter(AnnotationType.Complete); - case 'annotation:@description': - return new AnnotationFilter(AnnotationType.Description); - case 'annotation:@enum': - return new AnnotationFilter(AnnotationType.Enum); - case 'annotation:@group': - return new AnnotationFilter(AnnotationType.Group); - case 'annotation:@move': - return new AnnotationFilter(AnnotationType.Move); - case 'annotation:@pure': - return new AnnotationFilter(AnnotationType.Pure); - case 'annotation:@remove': - return new AnnotationFilter(AnnotationType.Remove); - case 'annotation:@rename': - return new AnnotationFilter(AnnotationType.Rename); - case 'annotation:@todo': - return new AnnotationFilter(AnnotationType.Todo); - case 'annotation:@value': - return new AnnotationFilter(AnnotationType.Value); + // Fixed filters + const fixedFilter = fixedFilters[token.toLowerCase()]; + if (fixedFilter) { + return fixedFilter; } // Name @@ -219,3 +196,10 @@ const comparisonFunction = function (comparisonOperator: string): ((a: number, b export const isValidFilterToken = function (token: string): boolean { return Boolean(parsePotentiallyNegatedToken(token)); }; + +/** + * Returns the names of all fixed filter like "annotation:any". + */ +export const getFixedFilterNames = function (): string[] { + return Object.keys(fixedFilters); +};