Skip to content
This repository was archived by the owner on Jan 19, 2025. It is now read-only.

Commit 49606af

Browse files
authored
feat(gui): suggest replacement for wrong filter tokens (#907)
* feat(gui): suggest replacement for wrong filter tokens * style: apply automatic fixes of linters * fix(gui): convert token to lowercase Co-authored-by: lars-reimann <[email protected]>
1 parent 728bf87 commit 49606af

File tree

4 files changed

+107
-71
lines changed

4 files changed

+107
-71
lines changed

api-editor/gui/package-lock.json

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api-editor/gui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"@emotion/styled": "^11.9.3",
2222
"@reduxjs/toolkit": "^1.8.3",
2323
"chart.js": "^3.8.0",
24+
"fastest-levenshtein": "^1.0.12",
2425
"framer-motion": "^6.3.16",
2526
"idb-keyval": "^6.2.0",
2627
"katex": "^0.16.0",

api-editor/gui/src/features/filter/FilterInput.tsx

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ import {
99
PopoverContent,
1010
PopoverHeader,
1111
PopoverTrigger,
12+
Text as ChakraText,
1213
UnorderedList,
1314
} from '@chakra-ui/react';
15+
import { closest, distance } from 'fastest-levenshtein';
1416
import React from 'react';
1517
import { useAppDispatch, useAppSelector } from '../../app/hooks';
1618
import { selectFilterString, setFilterString } from '../ui/uiSlice';
17-
import { isValidFilterToken } from './model/filterFactory';
19+
import { getFixedFilterNames, isValidFilterToken } from './model/filterFactory';
1820

1921
export const FilterInput: React.FC = function () {
2022
const dispatch = useAppDispatch();
@@ -51,7 +53,7 @@ export const FilterInput: React.FC = function () {
5153
<PopoverBody>
5254
<UnorderedList spacing={2}>
5355
{invalidTokens.map((token) => (
54-
<ListItem key={token}>{token}</ListItem>
56+
<InvalidFilterToken key={token} token={token} />
5557
))}
5658
</UnorderedList>
5759
</PopoverBody>
@@ -61,3 +63,41 @@ export const FilterInput: React.FC = function () {
6163
</Box>
6264
);
6365
};
66+
67+
interface InvalidFilterTokenProps {
68+
token: string;
69+
}
70+
71+
const InvalidFilterToken: React.FC<InvalidFilterTokenProps> = function ({ token }) {
72+
const dispatch = useAppDispatch();
73+
74+
const alternatives = getFixedFilterNames();
75+
const closestAlternative = closest(token.toLowerCase(), alternatives);
76+
const closestDistance = distance(token.toLowerCase(), closestAlternative);
77+
78+
const filterString = useAppSelector(selectFilterString);
79+
80+
const onClick = () => {
81+
dispatch(setFilterString(filterString.replace(token, closestAlternative)));
82+
};
83+
84+
return (
85+
<ListItem>
86+
<ChakraText>
87+
<ChakraText display="inline" fontWeight="bold">
88+
{token}
89+
</ChakraText>
90+
{closestDistance <= 3 && (
91+
<>
92+
{'. '}
93+
Did you mean{' '}
94+
<ChakraText display="inline" textDecoration="underline" cursor="pointer" onClick={onClick}>
95+
{closestAlternative}
96+
</ChakraText>
97+
?
98+
</>
99+
)}
100+
</ChakraText>
101+
</ListItem>
102+
);
103+
};

api-editor/gui/src/features/filter/model/filterFactory.ts

Lines changed: 53 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -52,81 +52,58 @@ const parsePotentiallyNegatedToken = function (token: string): Optional<Abstract
5252
}
5353
};
5454

55+
const fixedFilters: { [name: string]: AbstractPythonFilter } = {
56+
// Declaration type
57+
'is:module': new DeclarationTypeFilter(DeclarationType.Module),
58+
'is:class': new DeclarationTypeFilter(DeclarationType.Class),
59+
'is:function': new DeclarationTypeFilter(DeclarationType.Function),
60+
'is:parameter': new DeclarationTypeFilter(DeclarationType.Parameter),
61+
62+
// Visibility
63+
'is:public': new VisibilityFilter(Visibility.Public),
64+
'is:internal': new VisibilityFilter(Visibility.Internal),
65+
66+
// Parameter required or optional
67+
'is:required': new RequiredOrOptionalFilter(RequiredOrOptional.Required),
68+
'is:optional': new RequiredOrOptionalFilter(RequiredOrOptional.Optional),
69+
70+
// Parameter assignment
71+
'is:implicit': new ParameterAssignmentFilter(PythonParameterAssignment.IMPLICIT),
72+
'is:positiononly': new ParameterAssignmentFilter(PythonParameterAssignment.POSITION_ONLY),
73+
'is:positionorname': new ParameterAssignmentFilter(PythonParameterAssignment.POSITION_OR_NAME),
74+
'is:positionalvararg': new ParameterAssignmentFilter(PythonParameterAssignment.POSITIONAL_VARARG),
75+
'is:nameonly': new ParameterAssignmentFilter(PythonParameterAssignment.NAME_ONLY),
76+
'is:namedvararg': new ParameterAssignmentFilter(PythonParameterAssignment.NAMED_VARARG),
77+
78+
// Done
79+
'is:done': new DoneFilter(),
80+
81+
// Annotations
82+
'annotation:any': new AnnotationFilter(AnnotationType.Any),
83+
'annotation:@boundary': new AnnotationFilter(AnnotationType.Boundary),
84+
'annotation:@calledafter': new AnnotationFilter(AnnotationType.CalledAfter),
85+
'is:complete': new AnnotationFilter(AnnotationType.Complete), // Deliberate special case. It should be transparent to users it's an annotation.
86+
'annotation:@description': new AnnotationFilter(AnnotationType.Description),
87+
'annotation:@enum': new AnnotationFilter(AnnotationType.Enum),
88+
'annotation:@group': new AnnotationFilter(AnnotationType.Group),
89+
'annotation:@move': new AnnotationFilter(AnnotationType.Move),
90+
'annotation:@pure': new AnnotationFilter(AnnotationType.Pure),
91+
'annotation:@remove': new AnnotationFilter(AnnotationType.Remove),
92+
'annotation:@rename': new AnnotationFilter(AnnotationType.Rename),
93+
'annotation:@todo': new AnnotationFilter(AnnotationType.Todo),
94+
'annotation:@value': new AnnotationFilter(AnnotationType.Value),
95+
};
96+
5597
/**
5698
* Handles a singe non-negated token.
5799
*
58100
* @param token The text that describes the filter.
59101
*/
60102
const parsePositiveToken = function (token: string): Optional<AbstractPythonFilter> {
61-
// Filters with fixed text
62-
switch (token.toLowerCase()) {
63-
// Declaration type
64-
case 'is:module':
65-
return new DeclarationTypeFilter(DeclarationType.Module);
66-
case 'is:class':
67-
return new DeclarationTypeFilter(DeclarationType.Class);
68-
case 'is:function':
69-
return new DeclarationTypeFilter(DeclarationType.Function);
70-
case 'is:parameter':
71-
return new DeclarationTypeFilter(DeclarationType.Parameter);
72-
73-
// Visibility
74-
case 'is:public':
75-
return new VisibilityFilter(Visibility.Public);
76-
case 'is:internal':
77-
return new VisibilityFilter(Visibility.Internal);
78-
79-
// Parameter required or optional
80-
case 'is:required':
81-
return new RequiredOrOptionalFilter(RequiredOrOptional.Required);
82-
case 'is:optional':
83-
return new RequiredOrOptionalFilter(RequiredOrOptional.Optional);
84-
85-
// Parameter assignment
86-
case 'is:implicit':
87-
return new ParameterAssignmentFilter(PythonParameterAssignment.IMPLICIT);
88-
case 'is:positiononly':
89-
return new ParameterAssignmentFilter(PythonParameterAssignment.POSITION_ONLY);
90-
case 'is:positionorname':
91-
return new ParameterAssignmentFilter(PythonParameterAssignment.POSITION_OR_NAME);
92-
case 'is:positionalvararg':
93-
return new ParameterAssignmentFilter(PythonParameterAssignment.POSITIONAL_VARARG);
94-
case 'is:nameonly':
95-
return new ParameterAssignmentFilter(PythonParameterAssignment.NAME_ONLY);
96-
case 'is:namedvararg':
97-
return new ParameterAssignmentFilter(PythonParameterAssignment.NAMED_VARARG);
98-
99-
// Done
100-
case 'is:done':
101-
return new DoneFilter();
102-
103-
// Annotations
104-
case 'annotation:any':
105-
return new AnnotationFilter(AnnotationType.Any);
106-
case 'annotation:@boundary':
107-
return new AnnotationFilter(AnnotationType.Boundary);
108-
case 'annotation:@calledafter':
109-
return new AnnotationFilter(AnnotationType.CalledAfter);
110-
case 'is:complete': // Deliberate special case. It should be transparent to users it's an annotation.
111-
return new AnnotationFilter(AnnotationType.Complete);
112-
case 'annotation:@description':
113-
return new AnnotationFilter(AnnotationType.Description);
114-
case 'annotation:@enum':
115-
return new AnnotationFilter(AnnotationType.Enum);
116-
case 'annotation:@group':
117-
return new AnnotationFilter(AnnotationType.Group);
118-
case 'annotation:@move':
119-
return new AnnotationFilter(AnnotationType.Move);
120-
case 'annotation:@pure':
121-
return new AnnotationFilter(AnnotationType.Pure);
122-
case 'annotation:@remove':
123-
return new AnnotationFilter(AnnotationType.Remove);
124-
case 'annotation:@rename':
125-
return new AnnotationFilter(AnnotationType.Rename);
126-
case 'annotation:@todo':
127-
return new AnnotationFilter(AnnotationType.Todo);
128-
case 'annotation:@value':
129-
return new AnnotationFilter(AnnotationType.Value);
103+
// Fixed filters
104+
const fixedFilter = fixedFilters[token.toLowerCase()];
105+
if (fixedFilter) {
106+
return fixedFilter;
130107
}
131108

132109
// Name
@@ -219,3 +196,10 @@ const comparisonFunction = function (comparisonOperator: string): ((a: number, b
219196
export const isValidFilterToken = function (token: string): boolean {
220197
return Boolean(parsePotentiallyNegatedToken(token));
221198
};
199+
200+
/**
201+
* Returns the names of all fixed filter like "annotation:any".
202+
*/
203+
export const getFixedFilterNames = function (): string[] {
204+
return Object.keys(fixedFilters);
205+
};

0 commit comments

Comments
 (0)