Skip to content

Commit 39c1729

Browse files
committed
Add diagnostics to suggest box
This adds the ability for consumers of the suggest box to add diagnostics to the suggest box. When a diagnostic is returned for a value, the input will be shown with a red border.
1 parent 6ec727a commit 39c1729

File tree

3 files changed

+82
-7
lines changed

3 files changed

+82
-7
lines changed

extensions/ql-vscode/src/stories/common/SuggestBox.stories.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { styled } from "styled-components";
55
import { Codicon } from "../../view/common";
66
import { SuggestBox as SuggestBoxComponent } from "../../view/common/SuggestBox/SuggestBox";
77
import { useCallback, useState } from "react";
8+
import type { Diagnostic } from "../../view/common/SuggestBox/diagnostics";
89

910
export default {
1011
title: "Suggest Box",
@@ -141,6 +142,25 @@ export const AccessPath = Template.bind({});
141142
AccessPath.args = {
142143
options: suggestedOptions,
143144
parseValueToTokens: (value: string) => value.split("."),
145+
validateValue: (value: string) => {
146+
let index = value.indexOf("|");
147+
148+
const diagnostics: Diagnostic[] = [];
149+
150+
while (index !== -1) {
151+
index = value.indexOf("|", index + 1);
152+
153+
diagnostics.push({
154+
message: "This cannot contain |",
155+
range: {
156+
start: index,
157+
end: index + 1,
158+
},
159+
});
160+
}
161+
162+
return diagnostics;
163+
},
144164
getIcon: (option: StoryOption) => <Icon name={option.icon} />,
145165
getDetails: (option: StoryOption) => option.details,
146166
};

extensions/ql-vscode/src/view/common/SuggestBox/SuggestBox.tsx

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,24 @@ import {
1313
useListNavigation,
1414
useRole,
1515
} from "@floating-ui/react";
16-
import { styled } from "styled-components";
16+
import { css, styled } from "styled-components";
1717
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react";
1818
import type { Option } from "./options";
1919
import { findMatchingOptions } from "./options";
2020
import { SuggestBoxItem } from "./SuggestBoxItem";
21+
import type { Diagnostic } from "./diagnostics";
2122

22-
const Input = styled(VSCodeTextField)`
23+
const Input = styled(VSCodeTextField)<{ $error: boolean }>`
2324
width: 430px;
2425
2526
font-family: var(--vscode-editor-font-family);
27+
28+
${(props) =>
29+
props.$error &&
30+
css`
31+
--dropdown-border: var(--vscode-inputValidation-errorBorder);
32+
--focus-border: var(--vscode-inputValidation-errorBorder);
33+
`}
2634
`;
2735

2836
const Container = styled.div`
@@ -50,7 +58,10 @@ const NoSuggestionsText = styled.div`
5058
padding-left: 22px;
5159
`;
5260

53-
export type SuggestBoxProps<T extends Option<T>> = {
61+
export type SuggestBoxProps<
62+
T extends Option<T>,
63+
D extends Diagnostic = Diagnostic,
64+
> = {
5465
value?: string;
5566
onChange: (value: string) => void;
5667
options: T[];
@@ -62,6 +73,12 @@ export type SuggestBoxProps<T extends Option<T>> = {
6273
*/
6374
parseValueToTokens: (value: string) => string[];
6475

76+
/**
77+
* Validate the value. This is used to show syntax errors in the input.
78+
* @param value The user-entered value to validate.
79+
*/
80+
validateValue?: (value: string) => D[];
81+
6582
/**
6683
* Get the icon to display for an option.
6784
* @param option The option to get the icon for.
@@ -83,20 +100,29 @@ export type SuggestBoxProps<T extends Option<T>> = {
83100
* for easier testing.
84101
* @param props The props returned by `getReferenceProps` of {@link useInteractions}
85102
*/
86-
renderInputComponent?: (props: Record<string, unknown>) => ReactNode;
103+
renderInputComponent?: (
104+
props: Record<string, unknown>,
105+
hasError: boolean,
106+
) => ReactNode;
87107
};
88108

89-
export const SuggestBox = <T extends Option<T>>({
109+
export const SuggestBox = <
110+
T extends Option<T>,
111+
D extends Diagnostic = Diagnostic,
112+
>({
90113
value = "",
91114
onChange,
92115
options,
93116
parseValueToTokens,
117+
validateValue,
94118
getIcon,
95119
getDetails,
96120
disabled,
97121
"aria-label": ariaLabel,
98-
renderInputComponent = (props) => <Input {...props} />,
99-
}: SuggestBoxProps<T>) => {
122+
renderInputComponent = (props, hasError) => (
123+
<Input {...props} $error={hasError} />
124+
),
125+
}: SuggestBoxProps<T, D>) => {
100126
const [isOpen, setIsOpen] = useState(false);
101127
const [activeIndex, setActiveIndex] = useState<number | null>(null);
102128

@@ -151,6 +177,13 @@ export const SuggestBox = <T extends Option<T>>({
151177
return findMatchingOptions(options, parseValueToTokens(value));
152178
}, [options, value, parseValueToTokens]);
153179

180+
const diagnostics = useMemo(
181+
() => validateValue?.(value) ?? [],
182+
[validateValue, value],
183+
);
184+
185+
const hasSyntaxError = diagnostics.length > 0;
186+
154187
useEffect(() => {
155188
if (disabled) {
156189
setIsOpen(false);
@@ -180,6 +213,7 @@ export const SuggestBox = <T extends Option<T>>({
180213
},
181214
disabled,
182215
}),
216+
hasSyntaxError,
183217
)}
184218
{isOpen && (
185219
<FloatingPortal>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* A range of characters in a value. The start position is inclusive, the end position is exclusive.
3+
*/
4+
type DiagnosticRange = {
5+
/**
6+
* Zero-based index of the first character of the token.
7+
*/
8+
start: number;
9+
/**
10+
* Zero-based index of the character after the last character of the token.
11+
*/
12+
end: number;
13+
};
14+
15+
/**
16+
* A diagnostic message.
17+
*/
18+
export type Diagnostic = {
19+
range: DiagnosticRange;
20+
message: string;
21+
};

0 commit comments

Comments
 (0)