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);
+};