CodeQL model editor: Show access path suggestions in the webview (#3305)

This commit is contained in:
Shati Patel
2024-02-05 14:16:38 +00:00
committed by GitHub
parent ad5ae27a0d
commit 7c233db4eb
10 changed files with 264 additions and 19 deletions

View File

@@ -89,7 +89,7 @@ export function parseAccessPathTokens(path: string): AccessPartToken[] {
// Regex for a single part of the access path
const tokenRegex = /^(\w+)(?:\[([^\]]*)])?$/;
type AccessPathDiagnostic = {
export type AccessPathDiagnostic = {
range: AccessPathRange;
message: string;
};

View File

@@ -23,8 +23,7 @@ import type { Diagnostic } from "./diagnostics";
import { useOpenKey } from "./useOpenKey";
const Input = styled(VSCodeTextField)<{ $error: boolean }>`
width: 430px;
width: 100%;
font-family: var(--vscode-editor-font-family);
${(props) =>
@@ -36,7 +35,6 @@ const Input = styled(VSCodeTextField)<{ $error: boolean }>`
`;
const Container = styled.div`
width: 430px;
display: flex;
flex-direction: column;
border-radius: 3px;

View File

@@ -13,6 +13,7 @@ import {
VSCodeTag,
} from "@vscode/webview-ui-toolkit/react";
import type { ModelEditorViewState } from "../../model-editor/shared/view-state";
import type { AccessPathSuggestionOptions } from "../../model-editor/suggestions";
const LibraryContainer = styled.div`
background-color: var(--vscode-peekViewResult-background);
@@ -76,6 +77,7 @@ export type LibraryRowProps = {
viewState: ModelEditorViewState;
hideModeledMethods: boolean;
revealedMethodSignature: string | null;
accessPathSuggestions?: AccessPathSuggestionOptions;
onChange: (methodSignature: string, modeledMethods: ModeledMethod[]) => void;
onMethodClick: (methodSignature: string) => void;
onSaveModelClick: (methodSignatures: string[]) => void;
@@ -99,6 +101,7 @@ export const LibraryRow = ({
viewState,
hideModeledMethods,
revealedMethodSignature,
accessPathSuggestions,
onChange,
onMethodClick,
onSaveModelClick,
@@ -237,6 +240,7 @@ export const LibraryRow = ({
viewState={viewState}
hideModeledMethods={hideModeledMethods}
revealedMethodSignature={revealedMethodSignature}
accessPathSuggestions={accessPathSuggestions}
onChange={onChange}
onMethodClick={onMethodClick}
/>

View File

@@ -33,6 +33,9 @@ import { DataGridCell, DataGridRow } from "../common/DataGrid";
import { validateModeledMethods } from "../../model-editor/shared/validation";
import { ModeledMethodAlert } from "../method-modeling/ModeledMethodAlert";
import { createEmptyModeledMethod } from "../../model-editor/modeled-method-empty";
import type { AccessPathOption } from "../../model-editor/suggestions";
import { ModelInputSuggestBox } from "./ModelInputSuggestBox";
import { ModelOutputSuggestBox } from "./ModelOutputSuggestBox";
const ApiOrMethodRow = styled.div`
min-height: calc(var(--input-height) * 1px);
@@ -74,6 +77,8 @@ export type MethodRowProps = {
modelingInProgress: boolean;
viewState: ModelEditorViewState;
revealedMethodSignature: string | null;
inputAccessPathSuggestions?: AccessPathOption[];
outputAccessPathSuggestions?: AccessPathOption[];
onChange: (methodSignature: string, modeledMethods: ModeledMethod[]) => void;
onMethodClick: (methodSignature: string) => void;
};
@@ -108,6 +113,8 @@ const ModelableMethodRow = forwardRef<HTMLElement | undefined, MethodRowProps>(
methodIsSelected,
viewState,
revealedMethodSignature,
inputAccessPathSuggestions,
outputAccessPathSuggestions,
onChange,
onMethodClick,
} = props;
@@ -259,22 +266,38 @@ const ModelableMethodRow = forwardRef<HTMLElement | undefined, MethodRowProps>(
/>
</DataGridCell>
<DataGridCell>
<ModelInputDropdown
language={viewState.language}
method={method}
modeledMethod={modeledMethod}
modelingStatus={modelingStatus}
onChange={modeledMethodChangedHandlers[index]}
/>
{inputAccessPathSuggestions === undefined ? (
<ModelInputDropdown
language={viewState.language}
method={method}
modeledMethod={modeledMethod}
modelingStatus={modelingStatus}
onChange={modeledMethodChangedHandlers[index]}
/>
) : (
<ModelInputSuggestBox
modeledMethod={modeledMethod}
suggestions={inputAccessPathSuggestions}
onChange={modeledMethodChangedHandlers[index]}
/>
)}
</DataGridCell>
<DataGridCell>
<ModelOutputDropdown
language={viewState.language}
method={method}
modeledMethod={modeledMethod}
modelingStatus={modelingStatus}
onChange={modeledMethodChangedHandlers[index]}
/>
{outputAccessPathSuggestions === undefined ? (
<ModelOutputDropdown
language={viewState.language}
method={method}
modeledMethod={modeledMethod}
modelingStatus={modelingStatus}
onChange={modeledMethodChangedHandlers[index]}
/>
) : (
<ModelOutputSuggestBox
modeledMethod={modeledMethod}
suggestions={outputAccessPathSuggestions}
onChange={modeledMethodChangedHandlers[index]}
/>
)}
</DataGridCell>
<DataGridCell>
<ModelKindDropdown

View File

@@ -18,6 +18,7 @@ import { percentFormatter } from "./formatters";
import { Mode } from "../../model-editor/shared/mode";
import { getLanguageDisplayName } from "../../common/query-language";
import { INITIAL_HIDE_MODELED_METHODS_VALUE } from "../../model-editor/shared/hide-modeled-methods";
import type { AccessPathSuggestionOptions } from "../../model-editor/suggestions";
const LoadingContainer = styled.div`
text-align: center;
@@ -122,6 +123,10 @@ export function ModelEditor({
Record<string, ModeledMethod[]>
>(initialModeledMethods);
const [accessPathSuggestions, setAccessPathSuggestions] = useState<
AccessPathSuggestionOptions | undefined
>(undefined);
useEffect(() => {
const listener = (evt: MessageEvent) => {
if (evt.origin === window.origin) {
@@ -147,7 +152,7 @@ export function ModelEditor({
setRevealedMethodSignature(msg.methodSignature);
break;
case "setAccessPathSuggestions":
// TODO
setAccessPathSuggestions(msg.accessPathSuggestions);
break;
default:
assertNever(msg);
@@ -386,6 +391,7 @@ export function ModelEditor({
viewState={viewState}
hideModeledMethods={hideModeledMethods}
revealedMethodSignature={revealedMethodSignature}
accessPathSuggestions={accessPathSuggestions}
onChange={onChange}
onMethodClick={onMethodClick}
onSaveModelClick={onSaveModelClick}

View File

@@ -0,0 +1,94 @@
import { useEffect, useMemo, useState } from "react";
import type { ModeledMethod } from "../../model-editor/modeled-method";
import {
calculateNewProvenance,
modeledMethodSupportsInput,
} from "../../model-editor/modeled-method";
import { ReadonlyDropdown } from "../common/ReadonlyDropdown";
import type { AccessPathOption } from "../../model-editor/suggestions";
import { SuggestBox } from "../common/SuggestBox";
import { useDebounceCallback } from "../common/useDebounceCallback";
import type { AccessPathDiagnostic } from "../../model-editor/shared/access-paths";
import {
parseAccessPathTokens,
validateAccessPath,
} from "../../model-editor/shared/access-paths";
import { ModelSuggestionIcon } from "./ModelSuggestionIcon";
type Props = {
modeledMethod: ModeledMethod | undefined;
suggestions: AccessPathOption[];
onChange: (modeledMethod: ModeledMethod) => void;
};
const parseValueToTokens = (value: string) =>
parseAccessPathTokens(value).map((t) => t.text);
const getIcon = (option: AccessPathOption) => (
<ModelSuggestionIcon name={option.icon} />
);
const getDetails = (option: AccessPathOption) => option.details;
export const ModelInputSuggestBox = ({
modeledMethod,
suggestions,
onChange,
}: Props) => {
const [value, setValue] = useState<string | undefined>(
modeledMethod && modeledMethodSupportsInput(modeledMethod)
? modeledMethod.input
: undefined,
);
useEffect(() => {
if (modeledMethod && modeledMethodSupportsInput(modeledMethod)) {
setValue(modeledMethod.input);
}
}, [modeledMethod]);
// Debounce the callback to avoid updating the model too often.
// Not doing this results in a lot of lag when typing.
useDebounceCallback(
value,
(input: string | undefined) => {
if (
!modeledMethod ||
!modeledMethodSupportsInput(modeledMethod) ||
input === undefined
) {
return;
}
onChange({
...modeledMethod,
provenance: calculateNewProvenance(modeledMethod),
input,
});
},
500,
);
const enabled = useMemo(
() => modeledMethod && modeledMethodSupportsInput(modeledMethod),
[modeledMethod],
);
if (modeledMethod?.type === "type") {
return <ReadonlyDropdown value={modeledMethod.path} aria-label="Path" />;
}
return (
<SuggestBox<AccessPathOption, AccessPathDiagnostic>
value={value}
onChange={setValue}
options={suggestions}
parseValueToTokens={parseValueToTokens}
validateValue={validateAccessPath}
getIcon={getIcon}
getDetails={getDetails}
disabled={!enabled}
aria-label="Input"
/>
);
};

View File

@@ -0,0 +1,99 @@
import { useEffect, useMemo, useState } from "react";
import type { ModeledMethod } from "../../model-editor/modeled-method";
import {
calculateNewProvenance,
modeledMethodSupportsOutput,
} from "../../model-editor/modeled-method";
import { ReadonlyDropdown } from "../common/ReadonlyDropdown";
import type { AccessPathOption } from "../../model-editor/suggestions";
import { SuggestBox } from "../common/SuggestBox";
import { useDebounceCallback } from "../common/useDebounceCallback";
import type { AccessPathDiagnostic } from "../../model-editor/shared/access-paths";
import {
parseAccessPathTokens,
validateAccessPath,
} from "../../model-editor/shared/access-paths";
import { ModelSuggestionIcon } from "./ModelSuggestionIcon";
type Props = {
modeledMethod: ModeledMethod | undefined;
suggestions: AccessPathOption[];
onChange: (modeledMethod: ModeledMethod) => void;
};
const parseValueToTokens = (value: string) =>
parseAccessPathTokens(value).map((t) => t.text);
const getIcon = (option: AccessPathOption) => (
<ModelSuggestionIcon name={option.icon} />
);
const getDetails = (option: AccessPathOption) => option.details;
export const ModelOutputSuggestBox = ({
modeledMethod,
suggestions,
onChange,
}: Props) => {
const [value, setValue] = useState<string | undefined>(
modeledMethod && modeledMethodSupportsOutput(modeledMethod)
? modeledMethod.output
: undefined,
);
useEffect(() => {
if (modeledMethod && modeledMethodSupportsOutput(modeledMethod)) {
setValue(modeledMethod.output);
}
}, [modeledMethod]);
// Debounce the callback to avoid updating the model too often.
// Not doing this results in a lot of lag when typing.
useDebounceCallback(
value,
(output: string | undefined) => {
if (
!modeledMethod ||
!modeledMethodSupportsOutput(modeledMethod) ||
output === undefined
) {
return;
}
onChange({
...modeledMethod,
provenance: calculateNewProvenance(modeledMethod),
output,
});
},
500,
);
const enabled = useMemo(
() => modeledMethod && modeledMethodSupportsOutput(modeledMethod),
[modeledMethod],
);
if (modeledMethod?.type === "type") {
return (
<ReadonlyDropdown
value={modeledMethod.relatedTypeName}
aria-label="Related type name"
/>
);
}
return (
<SuggestBox<AccessPathOption, AccessPathDiagnostic>
value={value}
options={suggestions}
disabled={!enabled}
onChange={setValue}
parseValueToTokens={parseValueToTokens}
validateValue={validateAccessPath}
getIcon={getIcon}
getDetails={getDetails}
aria-label="Output"
/>
);
};

View File

@@ -0,0 +1,8 @@
import { Codicon } from "../common";
import { styled } from "styled-components";
export const ModelSuggestionIcon = styled(Codicon)`
margin-right: 4px;
color: var(--vscode-symbolIcon-fieldForeground);
font-size: 16px;
`;

View File

@@ -8,6 +8,7 @@ import { HiddenMethodsRow } from "./HiddenMethodsRow";
import type { ModelEditorViewState } from "../../model-editor/shared/view-state";
import { ScreenReaderOnly } from "../common/ScreenReaderOnly";
import { DataGrid, DataGridCell } from "../common/DataGrid";
import type { AccessPathSuggestionOptions } from "../../model-editor/suggestions";
export const MULTIPLE_MODELS_GRID_TEMPLATE_COLUMNS =
"0.5fr 0.125fr 0.125fr 0.125fr 0.125fr max-content";
@@ -21,6 +22,7 @@ export type ModeledMethodDataGridProps = {
viewState: ModelEditorViewState;
hideModeledMethods: boolean;
revealedMethodSignature: string | null;
accessPathSuggestions?: AccessPathSuggestionOptions;
onChange: (methodSignature: string, modeledMethods: ModeledMethod[]) => void;
onMethodClick: (methodSignature: string) => void;
};
@@ -34,6 +36,7 @@ export const ModeledMethodDataGrid = ({
viewState,
hideModeledMethods,
revealedMethodSignature,
accessPathSuggestions,
onChange,
onMethodClick,
}: ModeledMethodDataGridProps) => {
@@ -77,6 +80,10 @@ export const ModeledMethodDataGrid = ({
</DataGridCell>
{methodsWithModelability.map(({ method, methodCanBeModeled }) => {
const modeledMethods = modeledMethodsMap[method.signature] ?? [];
const inputAccessPathSuggestions =
accessPathSuggestions?.input?.[method.signature];
const outputAccessPathSuggestions =
accessPathSuggestions?.output?.[method.signature];
return (
<MethodRow
key={method.signature}
@@ -88,6 +95,8 @@ export const ModeledMethodDataGrid = ({
modelingInProgress={inProgressMethods.has(method.signature)}
viewState={viewState}
revealedMethodSignature={revealedMethodSignature}
inputAccessPathSuggestions={inputAccessPathSuggestions}
outputAccessPathSuggestions={outputAccessPathSuggestions}
onChange={onChange}
onMethodClick={onMethodClick}
/>

View File

@@ -8,6 +8,7 @@ import {
sortGroupNames,
} from "../../model-editor/shared/sorting";
import type { ModelEditorViewState } from "../../model-editor/shared/view-state";
import type { AccessPathSuggestionOptions } from "../../model-editor/suggestions";
export type ModeledMethodsListProps = {
methods: Method[];
@@ -16,6 +17,7 @@ export type ModeledMethodsListProps = {
selectedSignatures: Set<string>;
inProgressMethods: Set<string>;
revealedMethodSignature: string | null;
accessPathSuggestions?: AccessPathSuggestionOptions;
viewState: ModelEditorViewState;
hideModeledMethods: boolean;
onChange: (methodSignature: string, modeledMethods: ModeledMethod[]) => void;
@@ -43,6 +45,7 @@ export const ModeledMethodsList = ({
viewState,
hideModeledMethods,
revealedMethodSignature,
accessPathSuggestions,
onChange,
onMethodClick,
onSaveModelClick,
@@ -91,6 +94,7 @@ export const ModeledMethodsList = ({
viewState={viewState}
hideModeledMethods={hideModeledMethods}
revealedMethodSignature={revealedMethodSignature}
accessPathSuggestions={accessPathSuggestions}
onChange={onChange}
onMethodClick={onMethodClick}
onSaveModelClick={onSaveModelClick}