CodeQL model editor: Show access path suggestions in the webview (#3305)
This commit is contained in:
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
`;
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user