Skip to content

Commit 7c233db

Browse files
authored
CodeQL model editor: Show access path suggestions in the webview (#3305)
1 parent ad5ae27 commit 7c233db

File tree

10 files changed

+264
-19
lines changed

10 files changed

+264
-19
lines changed

extensions/ql-vscode/src/model-editor/shared/access-paths.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export function parseAccessPathTokens(path: string): AccessPartToken[] {
8989
// Regex for a single part of the access path
9090
const tokenRegex = /^(\w+)(?:\[([^\]]*)])?$/;
9191

92-
type AccessPathDiagnostic = {
92+
export type AccessPathDiagnostic = {
9393
range: AccessPathRange;
9494
message: string;
9595
};

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@ import type { Diagnostic } from "./diagnostics";
2323
import { useOpenKey } from "./useOpenKey";
2424

2525
const Input = styled(VSCodeTextField)<{ $error: boolean }>`
26-
width: 430px;
27-
26+
width: 100%;
2827
font-family: var(--vscode-editor-font-family);
2928
3029
${(props) =>
@@ -36,7 +35,6 @@ const Input = styled(VSCodeTextField)<{ $error: boolean }>`
3635
`;
3736

3837
const Container = styled.div`
39-
width: 430px;
4038
display: flex;
4139
flex-direction: column;
4240
border-radius: 3px;

extensions/ql-vscode/src/view/model-editor/LibraryRow.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
VSCodeTag,
1414
} from "@vscode/webview-ui-toolkit/react";
1515
import type { ModelEditorViewState } from "../../model-editor/shared/view-state";
16+
import type { AccessPathSuggestionOptions } from "../../model-editor/suggestions";
1617

1718
const LibraryContainer = styled.div`
1819
background-color: var(--vscode-peekViewResult-background);
@@ -76,6 +77,7 @@ export type LibraryRowProps = {
7677
viewState: ModelEditorViewState;
7778
hideModeledMethods: boolean;
7879
revealedMethodSignature: string | null;
80+
accessPathSuggestions?: AccessPathSuggestionOptions;
7981
onChange: (methodSignature: string, modeledMethods: ModeledMethod[]) => void;
8082
onMethodClick: (methodSignature: string) => void;
8183
onSaveModelClick: (methodSignatures: string[]) => void;
@@ -99,6 +101,7 @@ export const LibraryRow = ({
99101
viewState,
100102
hideModeledMethods,
101103
revealedMethodSignature,
104+
accessPathSuggestions,
102105
onChange,
103106
onMethodClick,
104107
onSaveModelClick,
@@ -237,6 +240,7 @@ export const LibraryRow = ({
237240
viewState={viewState}
238241
hideModeledMethods={hideModeledMethods}
239242
revealedMethodSignature={revealedMethodSignature}
243+
accessPathSuggestions={accessPathSuggestions}
240244
onChange={onChange}
241245
onMethodClick={onMethodClick}
242246
/>

extensions/ql-vscode/src/view/model-editor/MethodRow.tsx

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ import { DataGridCell, DataGridRow } from "../common/DataGrid";
3333
import { validateModeledMethods } from "../../model-editor/shared/validation";
3434
import { ModeledMethodAlert } from "../method-modeling/ModeledMethodAlert";
3535
import { createEmptyModeledMethod } from "../../model-editor/modeled-method-empty";
36+
import type { AccessPathOption } from "../../model-editor/suggestions";
37+
import { ModelInputSuggestBox } from "./ModelInputSuggestBox";
38+
import { ModelOutputSuggestBox } from "./ModelOutputSuggestBox";
3639

3740
const ApiOrMethodRow = styled.div`
3841
min-height: calc(var(--input-height) * 1px);
@@ -74,6 +77,8 @@ export type MethodRowProps = {
7477
modelingInProgress: boolean;
7578
viewState: ModelEditorViewState;
7679
revealedMethodSignature: string | null;
80+
inputAccessPathSuggestions?: AccessPathOption[];
81+
outputAccessPathSuggestions?: AccessPathOption[];
7782
onChange: (methodSignature: string, modeledMethods: ModeledMethod[]) => void;
7883
onMethodClick: (methodSignature: string) => void;
7984
};
@@ -108,6 +113,8 @@ const ModelableMethodRow = forwardRef<HTMLElement | undefined, MethodRowProps>(
108113
methodIsSelected,
109114
viewState,
110115
revealedMethodSignature,
116+
inputAccessPathSuggestions,
117+
outputAccessPathSuggestions,
111118
onChange,
112119
onMethodClick,
113120
} = props;
@@ -259,22 +266,38 @@ const ModelableMethodRow = forwardRef<HTMLElement | undefined, MethodRowProps>(
259266
/>
260267
</DataGridCell>
261268
<DataGridCell>
262-
<ModelInputDropdown
263-
language={viewState.language}
264-
method={method}
265-
modeledMethod={modeledMethod}
266-
modelingStatus={modelingStatus}
267-
onChange={modeledMethodChangedHandlers[index]}
268-
/>
269+
{inputAccessPathSuggestions === undefined ? (
270+
<ModelInputDropdown
271+
language={viewState.language}
272+
method={method}
273+
modeledMethod={modeledMethod}
274+
modelingStatus={modelingStatus}
275+
onChange={modeledMethodChangedHandlers[index]}
276+
/>
277+
) : (
278+
<ModelInputSuggestBox
279+
modeledMethod={modeledMethod}
280+
suggestions={inputAccessPathSuggestions}
281+
onChange={modeledMethodChangedHandlers[index]}
282+
/>
283+
)}
269284
</DataGridCell>
270285
<DataGridCell>
271-
<ModelOutputDropdown
272-
language={viewState.language}
273-
method={method}
274-
modeledMethod={modeledMethod}
275-
modelingStatus={modelingStatus}
276-
onChange={modeledMethodChangedHandlers[index]}
277-
/>
286+
{outputAccessPathSuggestions === undefined ? (
287+
<ModelOutputDropdown
288+
language={viewState.language}
289+
method={method}
290+
modeledMethod={modeledMethod}
291+
modelingStatus={modelingStatus}
292+
onChange={modeledMethodChangedHandlers[index]}
293+
/>
294+
) : (
295+
<ModelOutputSuggestBox
296+
modeledMethod={modeledMethod}
297+
suggestions={outputAccessPathSuggestions}
298+
onChange={modeledMethodChangedHandlers[index]}
299+
/>
300+
)}
278301
</DataGridCell>
279302
<DataGridCell>
280303
<ModelKindDropdown

extensions/ql-vscode/src/view/model-editor/ModelEditor.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { percentFormatter } from "./formatters";
1818
import { Mode } from "../../model-editor/shared/mode";
1919
import { getLanguageDisplayName } from "../../common/query-language";
2020
import { INITIAL_HIDE_MODELED_METHODS_VALUE } from "../../model-editor/shared/hide-modeled-methods";
21+
import type { AccessPathSuggestionOptions } from "../../model-editor/suggestions";
2122

2223
const LoadingContainer = styled.div`
2324
text-align: center;
@@ -122,6 +123,10 @@ export function ModelEditor({
122123
Record<string, ModeledMethod[]>
123124
>(initialModeledMethods);
124125

126+
const [accessPathSuggestions, setAccessPathSuggestions] = useState<
127+
AccessPathSuggestionOptions | undefined
128+
>(undefined);
129+
125130
useEffect(() => {
126131
const listener = (evt: MessageEvent) => {
127132
if (evt.origin === window.origin) {
@@ -147,7 +152,7 @@ export function ModelEditor({
147152
setRevealedMethodSignature(msg.methodSignature);
148153
break;
149154
case "setAccessPathSuggestions":
150-
// TODO
155+
setAccessPathSuggestions(msg.accessPathSuggestions);
151156
break;
152157
default:
153158
assertNever(msg);
@@ -386,6 +391,7 @@ export function ModelEditor({
386391
viewState={viewState}
387392
hideModeledMethods={hideModeledMethods}
388393
revealedMethodSignature={revealedMethodSignature}
394+
accessPathSuggestions={accessPathSuggestions}
389395
onChange={onChange}
390396
onMethodClick={onMethodClick}
391397
onSaveModelClick={onSaveModelClick}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { useEffect, useMemo, useState } from "react";
2+
import type { ModeledMethod } from "../../model-editor/modeled-method";
3+
import {
4+
calculateNewProvenance,
5+
modeledMethodSupportsInput,
6+
} from "../../model-editor/modeled-method";
7+
import { ReadonlyDropdown } from "../common/ReadonlyDropdown";
8+
import type { AccessPathOption } from "../../model-editor/suggestions";
9+
import { SuggestBox } from "../common/SuggestBox";
10+
import { useDebounceCallback } from "../common/useDebounceCallback";
11+
import type { AccessPathDiagnostic } from "../../model-editor/shared/access-paths";
12+
import {
13+
parseAccessPathTokens,
14+
validateAccessPath,
15+
} from "../../model-editor/shared/access-paths";
16+
import { ModelSuggestionIcon } from "./ModelSuggestionIcon";
17+
18+
type Props = {
19+
modeledMethod: ModeledMethod | undefined;
20+
suggestions: AccessPathOption[];
21+
onChange: (modeledMethod: ModeledMethod) => void;
22+
};
23+
24+
const parseValueToTokens = (value: string) =>
25+
parseAccessPathTokens(value).map((t) => t.text);
26+
27+
const getIcon = (option: AccessPathOption) => (
28+
<ModelSuggestionIcon name={option.icon} />
29+
);
30+
31+
const getDetails = (option: AccessPathOption) => option.details;
32+
33+
export const ModelInputSuggestBox = ({
34+
modeledMethod,
35+
suggestions,
36+
onChange,
37+
}: Props) => {
38+
const [value, setValue] = useState<string | undefined>(
39+
modeledMethod && modeledMethodSupportsInput(modeledMethod)
40+
? modeledMethod.input
41+
: undefined,
42+
);
43+
44+
useEffect(() => {
45+
if (modeledMethod && modeledMethodSupportsInput(modeledMethod)) {
46+
setValue(modeledMethod.input);
47+
}
48+
}, [modeledMethod]);
49+
50+
// Debounce the callback to avoid updating the model too often.
51+
// Not doing this results in a lot of lag when typing.
52+
useDebounceCallback(
53+
value,
54+
(input: string | undefined) => {
55+
if (
56+
!modeledMethod ||
57+
!modeledMethodSupportsInput(modeledMethod) ||
58+
input === undefined
59+
) {
60+
return;
61+
}
62+
63+
onChange({
64+
...modeledMethod,
65+
provenance: calculateNewProvenance(modeledMethod),
66+
input,
67+
});
68+
},
69+
500,
70+
);
71+
72+
const enabled = useMemo(
73+
() => modeledMethod && modeledMethodSupportsInput(modeledMethod),
74+
[modeledMethod],
75+
);
76+
77+
if (modeledMethod?.type === "type") {
78+
return <ReadonlyDropdown value={modeledMethod.path} aria-label="Path" />;
79+
}
80+
81+
return (
82+
<SuggestBox<AccessPathOption, AccessPathDiagnostic>
83+
value={value}
84+
onChange={setValue}
85+
options={suggestions}
86+
parseValueToTokens={parseValueToTokens}
87+
validateValue={validateAccessPath}
88+
getIcon={getIcon}
89+
getDetails={getDetails}
90+
disabled={!enabled}
91+
aria-label="Input"
92+
/>
93+
);
94+
};
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { useEffect, useMemo, useState } from "react";
2+
import type { ModeledMethod } from "../../model-editor/modeled-method";
3+
import {
4+
calculateNewProvenance,
5+
modeledMethodSupportsOutput,
6+
} from "../../model-editor/modeled-method";
7+
import { ReadonlyDropdown } from "../common/ReadonlyDropdown";
8+
import type { AccessPathOption } from "../../model-editor/suggestions";
9+
import { SuggestBox } from "../common/SuggestBox";
10+
import { useDebounceCallback } from "../common/useDebounceCallback";
11+
import type { AccessPathDiagnostic } from "../../model-editor/shared/access-paths";
12+
import {
13+
parseAccessPathTokens,
14+
validateAccessPath,
15+
} from "../../model-editor/shared/access-paths";
16+
import { ModelSuggestionIcon } from "./ModelSuggestionIcon";
17+
18+
type Props = {
19+
modeledMethod: ModeledMethod | undefined;
20+
suggestions: AccessPathOption[];
21+
onChange: (modeledMethod: ModeledMethod) => void;
22+
};
23+
24+
const parseValueToTokens = (value: string) =>
25+
parseAccessPathTokens(value).map((t) => t.text);
26+
27+
const getIcon = (option: AccessPathOption) => (
28+
<ModelSuggestionIcon name={option.icon} />
29+
);
30+
31+
const getDetails = (option: AccessPathOption) => option.details;
32+
33+
export const ModelOutputSuggestBox = ({
34+
modeledMethod,
35+
suggestions,
36+
onChange,
37+
}: Props) => {
38+
const [value, setValue] = useState<string | undefined>(
39+
modeledMethod && modeledMethodSupportsOutput(modeledMethod)
40+
? modeledMethod.output
41+
: undefined,
42+
);
43+
44+
useEffect(() => {
45+
if (modeledMethod && modeledMethodSupportsOutput(modeledMethod)) {
46+
setValue(modeledMethod.output);
47+
}
48+
}, [modeledMethod]);
49+
50+
// Debounce the callback to avoid updating the model too often.
51+
// Not doing this results in a lot of lag when typing.
52+
useDebounceCallback(
53+
value,
54+
(output: string | undefined) => {
55+
if (
56+
!modeledMethod ||
57+
!modeledMethodSupportsOutput(modeledMethod) ||
58+
output === undefined
59+
) {
60+
return;
61+
}
62+
63+
onChange({
64+
...modeledMethod,
65+
provenance: calculateNewProvenance(modeledMethod),
66+
output,
67+
});
68+
},
69+
500,
70+
);
71+
72+
const enabled = useMemo(
73+
() => modeledMethod && modeledMethodSupportsOutput(modeledMethod),
74+
[modeledMethod],
75+
);
76+
77+
if (modeledMethod?.type === "type") {
78+
return (
79+
<ReadonlyDropdown
80+
value={modeledMethod.relatedTypeName}
81+
aria-label="Related type name"
82+
/>
83+
);
84+
}
85+
86+
return (
87+
<SuggestBox<AccessPathOption, AccessPathDiagnostic>
88+
value={value}
89+
options={suggestions}
90+
disabled={!enabled}
91+
onChange={setValue}
92+
parseValueToTokens={parseValueToTokens}
93+
validateValue={validateAccessPath}
94+
getIcon={getIcon}
95+
getDetails={getDetails}
96+
aria-label="Output"
97+
/>
98+
);
99+
};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Codicon } from "../common";
2+
import { styled } from "styled-components";
3+
4+
export const ModelSuggestionIcon = styled(Codicon)`
5+
margin-right: 4px;
6+
color: var(--vscode-symbolIcon-fieldForeground);
7+
font-size: 16px;
8+
`;

0 commit comments

Comments
 (0)