Merge branch 'main' into shati-patel/run-query-context-menu-local

This commit is contained in:
Shati Patel
2023-06-26 16:46:24 +01:00
committed by GitHub
15 changed files with 340 additions and 272 deletions

View File

@@ -503,7 +503,11 @@
},
{
"command": "codeQLQueries.runLocalQueryFromQueriesPanel",
"title": "Run local query",
"title": "Run local query"
},
{
"command": "codeQL.runLocalQueryFromFileTab",
"title": "CodeQL: Run local query",
"icon": "$(run)"
},
{
@@ -881,6 +885,13 @@
}
],
"menus": {
"editor/title": [
{
"command": "codeQL.runLocalQueryFromFileTab",
"group": "navigation",
"when": "config.codeQL.queriesPanel && resourceExtname == .ql && codeQL.currentDatabaseItem"
}
],
"view/title": [
{
"command": "codeQLDatabases.sortByName",
@@ -1177,6 +1188,10 @@
"command": "codeQLQueries.runLocalQueryFromQueriesPanel",
"when": "false"
},
{
"command": "codeQL.runLocalQueryFromFileTab",
"when": "false"
},
{
"command": "codeQL.runQueryContextEditor",
"when": "false"

View File

@@ -132,6 +132,7 @@ export type LocalQueryCommands = {
) => Promise<void>;
"codeQLQueries.runLocalQueryFromQueriesPanel": TreeViewContextSingleSelectionCommandFunction<QueryTreeViewItem>;
"codeQLQueries.runLocalQueryContextMenu": TreeViewContextSingleSelectionCommandFunction<QueryTreeViewItem>;
"codeQL.runLocalQueryFromFileTab": (uri: Uri) => Promise<void>;
"codeQL.runQueries": ExplorerSelectionCommandFunction<Uri>;
"codeQL.quickEval": (uri: Uri) => Promise<void>;
"codeQL.quickEvalContextEditor": (uri: Uri) => Promise<void>;

View File

@@ -7,6 +7,8 @@ import {
ModelRequest,
} from "./auto-model-api";
import type { UsageSnippetsBySignature } from "./auto-model-usages-query";
import { groupMethods, sortGroupNames, sortMethods } from "./shared/sorting";
import { Mode } from "./shared/mode";
// Soft limit on the number of candidates to send to the model.
// Note that the model may return fewer than this number of candidates.
@@ -19,6 +21,7 @@ export function createAutoModelRequest(
externalApiUsages: ExternalApiUsage[],
modeledMethods: Record<string, ModeledMethod>,
usages: UsageSnippetsBySignature,
mode: Mode,
): ModelRequest {
const request: ModelRequest = {
language,
@@ -26,11 +29,14 @@ export function createAutoModelRequest(
candidates: [],
};
// Sort by number of usages so we always send the most used methods first
externalApiUsages = [...externalApiUsages];
externalApiUsages.sort((a, b) => b.usages.length - a.usages.length);
// Sort the same way as the UI so we send the first ones listed in the UI first
const grouped = groupMethods(externalApiUsages, mode);
const sortedGroupNames = sortGroupNames(grouped);
const sortedExternalApiUsages = sortedGroupNames.flatMap((name) =>
sortMethods(grouped[name]),
);
for (const externalApiUsage of externalApiUsages) {
for (const externalApiUsage of sortedExternalApiUsages) {
const modeledMethod: ModeledMethod = modeledMethods[
externalApiUsage.signature
] ?? {

View File

@@ -457,6 +457,7 @@ export class DataExtensionsEditorView extends AbstractWebview<
externalApiUsages,
modeledMethods,
usages,
this.mode,
);
await this.showProgress({

View File

@@ -1,4 +1,4 @@
import { ExternalApiUsage } from "../../data-extensions-editor/external-api-usage";
import { ExternalApiUsage } from "../external-api-usage";
export function calculateModeledPercentage(
externalApiUsages: Array<Pick<ExternalApiUsage, "supported">>,

View File

@@ -0,0 +1,88 @@
import { ExternalApiUsage } from "../external-api-usage";
import { Mode } from "./mode";
import { calculateModeledPercentage } from "./modeled-percentage";
export function groupMethods(
externalApiUsages: ExternalApiUsage[],
mode: Mode,
): Record<string, ExternalApiUsage[]> {
const groupedByLibrary: Record<string, ExternalApiUsage[]> = {};
for (const externalApiUsage of externalApiUsages) {
// Group by package if using framework mode
const key =
mode === Mode.Framework
? externalApiUsage.packageName
: externalApiUsage.library;
groupedByLibrary[key] ??= [];
groupedByLibrary[key].push(externalApiUsage);
}
return groupedByLibrary;
}
export function sortGroupNames(
methods: Record<string, ExternalApiUsage[]>,
): string[] {
return Object.keys(methods).sort((a, b) =>
compareGroups(methods[a], a, methods[b], b),
);
}
export function sortMethods(
externalApiUsages: ExternalApiUsage[],
): ExternalApiUsage[] {
const sortedExternalApiUsages = [...externalApiUsages];
sortedExternalApiUsages.sort((a, b) => compareMethod(a, b));
return sortedExternalApiUsages;
}
function compareGroups(
a: ExternalApiUsage[],
aName: string,
b: ExternalApiUsage[],
bName: string,
): number {
const supportedPercentageA = calculateModeledPercentage(a);
const supportedPercentageB = calculateModeledPercentage(b);
// Sort first by supported percentage ascending
if (supportedPercentageA > supportedPercentageB) {
return 1;
}
if (supportedPercentageA < supportedPercentageB) {
return -1;
}
const numberOfUsagesA = a.reduce((acc, curr) => acc + curr.usages.length, 0);
const numberOfUsagesB = b.reduce((acc, curr) => acc + curr.usages.length, 0);
// If the number of usages is equal, sort by number of methods descending
if (numberOfUsagesA === numberOfUsagesB) {
const numberOfMethodsA = a.length;
const numberOfMethodsB = b.length;
// If the number of methods is equal, sort by library name ascending
if (numberOfMethodsA === numberOfMethodsB) {
return aName.localeCompare(bName);
}
return numberOfMethodsB - numberOfMethodsA;
}
// Then sort by number of usages descending
return numberOfUsagesB - numberOfUsagesA;
}
function compareMethod(a: ExternalApiUsage, b: ExternalApiUsage): number {
// Sort first by supported, putting unmodeled methods first.
if (a.supported && !b.supported) {
return 1;
}
if (!a.supported && b.supported) {
return -1;
}
// Then sort by number of usages descending
return b.usages.length - a.usages.length;
}

View File

@@ -105,6 +105,7 @@ export class LocalQueries extends DisposableObject {
this.runQueryFromQueriesPanel.bind(this),
"codeQLQueries.runLocalQueryContextMenu":
this.runQueryFromQueriesPanel.bind(this),
"codeQL.runLocalQueryFromFileTab": this.runQuery.bind(this),
"codeQL.runQueries": createMultiSelectionCommand(
this.runQueries.bind(this),
),

View File

@@ -10,7 +10,7 @@ import { ExternalApiUsage } from "../../data-extensions-editor/external-api-usag
import { ModeledMethod } from "../../data-extensions-editor/modeled-method";
import { assertNever } from "../../common/helpers-pure";
import { vscode } from "../vscode-api";
import { calculateModeledPercentage } from "./modeled";
import { calculateModeledPercentage } from "../../data-extensions-editor/shared/modeled-percentage";
import { LinkIconButton } from "../variant-analysis/LinkIconButton";
import { ViewTitle } from "../common";
import { DataExtensionEditorViewState } from "../../data-extensions-editor/shared/view-state";

View File

@@ -5,7 +5,7 @@ import { ExternalApiUsage } from "../../data-extensions-editor/external-api-usag
import { ModeledMethod } from "../../data-extensions-editor/modeled-method";
import { pluralize } from "../../common/word";
import { ModeledMethodDataGrid } from "./ModeledMethodDataGrid";
import { calculateModeledPercentage } from "./modeled";
import { calculateModeledPercentage } from "../../data-extensions-editor/shared/modeled-percentage";
import { decimalFormatter, percentFormatter } from "./formatters";
import { Codicon } from "../common";
import { Mode } from "../../data-extensions-editor/shared/mode";

View File

@@ -9,6 +9,7 @@ import { ExternalApiUsage } from "../../data-extensions-editor/external-api-usag
import { ModeledMethod } from "../../data-extensions-editor/modeled-method";
import { useMemo } from "react";
import { Mode } from "../../data-extensions-editor/shared/mode";
import { sortMethods } from "../../data-extensions-editor/shared/sorting";
type Props = {
externalApiUsages: ExternalApiUsage[];
@@ -26,21 +27,10 @@ export const ModeledMethodDataGrid = ({
mode,
onChange,
}: Props) => {
const sortedExternalApiUsages = useMemo(() => {
const sortedExternalApiUsages = [...externalApiUsages];
sortedExternalApiUsages.sort((a, b) => {
// Sort first by supported, putting unmodeled methods first.
if (a.supported && !b.supported) {
return 1;
}
if (!a.supported && b.supported) {
return -1;
}
// Then sort by number of usages descending
return b.usages.length - a.usages.length;
});
return sortedExternalApiUsages;
}, [externalApiUsages]);
const sortedExternalApiUsages = useMemo(
() => sortMethods(externalApiUsages),
[externalApiUsages],
);
return (
<VSCodeDataGrid>

View File

@@ -2,9 +2,12 @@ import * as React from "react";
import { useMemo } from "react";
import { ExternalApiUsage } from "../../data-extensions-editor/external-api-usage";
import { ModeledMethod } from "../../data-extensions-editor/modeled-method";
import { calculateModeledPercentage } from "./modeled";
import { LibraryRow } from "./LibraryRow";
import { Mode } from "../../data-extensions-editor/shared/mode";
import {
groupMethods,
sortGroupNames,
} from "../../data-extensions-editor/shared/sorting";
type Props = {
externalApiUsages: ExternalApiUsage[];
@@ -22,62 +25,12 @@ export const ModeledMethodsList = ({
mode,
onChange,
}: Props) => {
const grouped = useMemo(() => {
const groupedByLibrary: Record<string, ExternalApiUsage[]> = {};
const grouped = useMemo(
() => groupMethods(externalApiUsages, mode),
[externalApiUsages, mode],
);
for (const externalApiUsage of externalApiUsages) {
// Group by package if using framework mode
const key =
mode === Mode.Framework
? externalApiUsage.packageName
: externalApiUsage.library;
groupedByLibrary[key] ??= [];
groupedByLibrary[key].push(externalApiUsage);
}
return groupedByLibrary;
}, [externalApiUsages, mode]);
const sortedGroupNames = useMemo(() => {
return Object.keys(grouped).sort((a, b) => {
const supportedPercentageA = calculateModeledPercentage(grouped[a]);
const supportedPercentageB = calculateModeledPercentage(grouped[b]);
// Sort first by supported percentage ascending
if (supportedPercentageA > supportedPercentageB) {
return 1;
}
if (supportedPercentageA < supportedPercentageB) {
return -1;
}
const numberOfUsagesA = grouped[a].reduce(
(acc, curr) => acc + curr.usages.length,
0,
);
const numberOfUsagesB = grouped[b].reduce(
(acc, curr) => acc + curr.usages.length,
0,
);
// If the number of usages is equal, sort by number of methods descending
if (numberOfUsagesA === numberOfUsagesB) {
const numberOfMethodsA = grouped[a].length;
const numberOfMethodsB = grouped[b].length;
// If the number of methods is equal, sort by library name ascending
if (numberOfMethodsA === numberOfMethodsB) {
return a.localeCompare(b);
}
return numberOfMethodsB - numberOfMethodsA;
}
// Then sort by number of usages descending
return numberOfUsagesB - numberOfUsagesA;
});
}, [grouped]);
const sortedGroupNames = useMemo(() => sortGroupNames(grouped), [grouped]);
return (
<>

View File

@@ -1,6 +1,6 @@
import * as React from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
ResultTableProps,
className,
emptyQueryResultsMessage,
jumpToLocation,
@@ -19,158 +19,158 @@ import { onNavigation } from "./results";
import { tryGetResolvableLocation } from "../../common/bqrs-utils";
import { ScrollIntoViewHelper } from "./scroll-into-view-helper";
import { sendTelemetry } from "../common/telemetry";
import { assertNever } from "../../common/helpers-pure";
export type RawTableProps = ResultTableProps & {
export type RawTableProps = {
databaseUri: string;
resultSet: RawTableResultSet;
sortState?: RawResultsSortState;
offset: number;
};
interface RawTableState {
selectedItem?: { row: number; column: number };
interface TableItem {
readonly row: number;
readonly column: number;
}
export class RawTable extends React.Component<RawTableProps, RawTableState> {
private scroller = new ScrollIntoViewHelper();
export function RawTable({
databaseUri,
resultSet,
sortState,
offset,
}: RawTableProps) {
const [selectedItem, setSelectedItem] = useState<TableItem | undefined>();
constructor(props: RawTableProps) {
super(props);
this.setSelection = this.setSelection.bind(this);
this.handleNavigationEvent = this.handleNavigationEvent.bind(this);
this.state = {};
const scroller = useRef<ScrollIntoViewHelper | undefined>(undefined);
if (scroller.current === undefined) {
scroller.current = new ScrollIntoViewHelper();
}
useEffect(() => scroller.current?.update());
private setSelection(row: number, column: number) {
this.setState((prev) => ({
...prev,
selectedItem: { row, column },
}));
const setSelection = useCallback((row: number, column: number): void => {
setSelectedItem({ row, column });
sendTelemetry("local-results-raw-results-table-selected");
}, []);
const navigateWithDelta = useCallback(
(rowDelta: number, columnDelta: number): void => {
setSelectedItem((prevSelectedItem) => {
const numberOfAlerts = resultSet.rows.length;
if (numberOfAlerts === 0) {
return prevSelectedItem;
}
const currentRow = prevSelectedItem?.row;
const nextRow = currentRow === undefined ? 0 : currentRow + rowDelta;
if (nextRow < 0 || nextRow >= numberOfAlerts) {
return prevSelectedItem;
}
const currentColumn = prevSelectedItem?.column;
const nextColumn =
currentColumn === undefined ? 0 : currentColumn + columnDelta;
// Jump to the location of the new cell
const rowData = resultSet.rows[nextRow];
if (nextColumn < 0 || nextColumn >= rowData.length) {
return prevSelectedItem;
}
const cellData = rowData[nextColumn];
if (cellData != null && typeof cellData === "object") {
const location = tryGetResolvableLocation(cellData.url);
if (location !== undefined) {
jumpToLocation(location, databaseUri);
}
}
scroller.current?.scrollIntoViewOnNextUpdate();
return { row: nextRow, column: nextColumn };
});
},
[databaseUri, resultSet, scroller],
);
const handleNavigationEvent = useCallback(
(event: NavigateMsg) => {
switch (event.direction) {
case NavigationDirection.up: {
navigateWithDelta(-1, 0);
break;
}
case NavigationDirection.down: {
navigateWithDelta(1, 0);
break;
}
case NavigationDirection.left: {
navigateWithDelta(0, -1);
break;
}
case NavigationDirection.right: {
navigateWithDelta(0, 1);
break;
}
default:
assertNever(event.direction);
}
},
[navigateWithDelta],
);
useEffect(() => {
onNavigation.addListener(handleNavigationEvent);
return () => {
onNavigation.removeListener(handleNavigationEvent);
};
}, [handleNavigationEvent]);
const [dataRows, numTruncatedResults] = useMemo(() => {
if (resultSet.rows.length <= RAW_RESULTS_LIMIT) {
return [resultSet.rows, 0];
}
return [
resultSet.rows.slice(0, RAW_RESULTS_LIMIT),
resultSet.rows.length - RAW_RESULTS_LIMIT,
];
}, [resultSet]);
if (dataRows.length === 0) {
return emptyQueryResultsMessage();
}
render(): React.ReactNode {
const { resultSet, databaseUri } = this.props;
const tableRows = dataRows.map((row: ResultRow, rowIndex: number) => (
<RawTableRow
key={rowIndex}
rowIndex={rowIndex + offset}
row={row}
databaseUri={databaseUri}
selectedColumn={
selectedItem?.row === rowIndex ? selectedItem?.column : undefined
}
onSelected={setSelection}
scroller={scroller.current}
/>
));
let dataRows = resultSet.rows;
if (dataRows.length === 0) {
return emptyQueryResultsMessage();
}
let numTruncatedResults = 0;
if (dataRows.length > RAW_RESULTS_LIMIT) {
numTruncatedResults = dataRows.length - RAW_RESULTS_LIMIT;
dataRows = dataRows.slice(0, RAW_RESULTS_LIMIT);
}
const tableRows = dataRows.map((row: ResultRow, rowIndex: number) => (
<RawTableRow
key={rowIndex}
rowIndex={rowIndex + this.props.offset}
row={row}
databaseUri={databaseUri}
selectedColumn={
this.state.selectedItem?.row === rowIndex
? this.state.selectedItem?.column
: undefined
}
onSelected={this.setSelection}
scroller={this.scroller}
/>
));
if (numTruncatedResults > 0) {
const colSpan = dataRows[0].length + 1; // one row for each data column, plus index column
tableRows.push(
<tr>
<td
key={"message"}
colSpan={colSpan}
style={{ textAlign: "center", fontStyle: "italic" }}
>
Too many results to show at once. {numTruncatedResults} result(s)
omitted.
</td>
</tr>,
);
}
return (
<table className={className}>
<RawTableHeader
columns={resultSet.schema.columns}
schemaName={resultSet.schema.name}
sortState={this.props.sortState}
/>
<tbody>{tableRows}</tbody>
</table>
if (numTruncatedResults > 0) {
const colSpan = dataRows[0].length + 1; // one row for each data column, plus index column
tableRows.push(
<tr>
<td
key={"message"}
colSpan={colSpan}
style={{ textAlign: "center", fontStyle: "italic" }}
>
Too many results to show at once. {numTruncatedResults} result(s)
omitted.
</td>
</tr>,
);
}
private handleNavigationEvent(event: NavigateMsg) {
switch (event.direction) {
case NavigationDirection.up: {
this.navigateWithDelta(-1, 0);
break;
}
case NavigationDirection.down: {
this.navigateWithDelta(1, 0);
break;
}
case NavigationDirection.left: {
this.navigateWithDelta(0, -1);
break;
}
case NavigationDirection.right: {
this.navigateWithDelta(0, 1);
break;
}
}
}
private navigateWithDelta(rowDelta: number, columnDelta: number) {
this.setState((prevState) => {
const numberOfAlerts = this.props.resultSet.rows.length;
if (numberOfAlerts === 0) {
return prevState;
}
const currentRow = prevState.selectedItem?.row;
const nextRow = currentRow === undefined ? 0 : currentRow + rowDelta;
if (nextRow < 0 || nextRow >= numberOfAlerts) {
return prevState;
}
const currentColumn = prevState.selectedItem?.column;
const nextColumn =
currentColumn === undefined ? 0 : currentColumn + columnDelta;
// Jump to the location of the new cell
const rowData = this.props.resultSet.rows[nextRow];
if (nextColumn < 0 || nextColumn >= rowData.length) {
return prevState;
}
const cellData = rowData[nextColumn];
if (cellData != null && typeof cellData === "object") {
const location = tryGetResolvableLocation(cellData.url);
if (location !== undefined) {
jumpToLocation(location, this.props.databaseUri);
}
}
this.scroller.scrollIntoViewOnNextUpdate();
return {
...prevState,
selectedItem: { row: nextRow, column: nextColumn },
};
});
}
componentDidUpdate() {
this.scroller.update();
}
componentDidMount() {
this.scroller.update();
onNavigation.addListener(this.handleNavigationEvent);
}
componentWillUnmount() {
onNavigation.removeListener(this.handleNavigationEvent);
}
return (
<table className={className}>
<RawTableHeader
columns={resultSet.schema.columns}
schemaName={resultSet.schema.name}
sortState={sortState}
/>
<tbody>{tableRows}</tbody>
</table>
);
}

View File

@@ -66,6 +66,12 @@ describe("commands declared in package.json", () => {
contribContextMenuCmds.add(command);
});
menus["editor/title"].forEach((commandDecl: CmdDecl) => {
const { command } = commandDecl;
paletteCmds.delete(command);
contribContextMenuCmds.add(command);
});
debuggers.forEach((debuggerDecl: DebuggerDecl) => {
if (debuggerDecl.variables !== undefined) {
for (const command of Object.values(debuggerDecl.variables)) {

View File

@@ -9,6 +9,7 @@ import {
ClassificationType,
Method,
} from "../../../src/data-extensions-editor/auto-model-api";
import { Mode } from "../../../src/data-extensions-editor/shared/mode";
describe("createAutoModelRequest", () => {
const externalApiUsages: ExternalApiUsage[] = [
@@ -259,7 +260,13 @@ describe("createAutoModelRequest", () => {
it("creates a matching request", () => {
expect(
createAutoModelRequest("java", externalApiUsages, modeledMethods, usages),
createAutoModelRequest(
"java",
externalApiUsages,
modeledMethods,
usages,
Mode.Application,
),
).toEqual({
language: "java",
samples: [
@@ -340,60 +347,6 @@ describe("createAutoModelRequest", () => {
input: "Argument[0]",
classification: undefined,
},
{
package: "org.springframework.boot",
type: "SpringApplication",
name: "run",
signature: "(Class,String[])",
usages:
usages[
"org.springframework.boot.SpringApplication#run(Class,String[])"
],
input: "Argument[this]",
classification: undefined,
},
{
package: "org.springframework.boot",
type: "SpringApplication",
name: "run",
signature: "(Class,String[])",
usages:
usages[
"org.springframework.boot.SpringApplication#run(Class,String[])"
],
input: "Argument[0]",
classification: undefined,
},
{
package: "org.springframework.boot",
type: "SpringApplication",
name: "run",
signature: "(Class,String[])",
usages:
usages[
"org.springframework.boot.SpringApplication#run(Class,String[])"
],
input: "Argument[1]",
classification: undefined,
},
{
package: "java.io",
type: "PrintStream",
name: "println",
signature: "(String)",
usages: usages["java.io.PrintStream#println(String)"],
input: "Argument[this]",
classification: undefined,
},
{
package: "java.io",
type: "PrintStream",
name: "println",
signature: "(String)",
usages: usages["java.io.PrintStream#println(String)"],
input: "Argument[0]",
classification: undefined,
},
{
package: "org.sql2o",
type: "Sql2o",
@@ -430,6 +383,60 @@ describe("createAutoModelRequest", () => {
input: "Argument[2]",
classification: undefined,
},
{
package: "java.io",
type: "PrintStream",
name: "println",
signature: "(String)",
usages: usages["java.io.PrintStream#println(String)"],
input: "Argument[this]",
classification: undefined,
},
{
package: "java.io",
type: "PrintStream",
name: "println",
signature: "(String)",
usages: usages["java.io.PrintStream#println(String)"],
input: "Argument[0]",
classification: undefined,
},
{
package: "org.springframework.boot",
type: "SpringApplication",
name: "run",
signature: "(Class,String[])",
usages:
usages[
"org.springframework.boot.SpringApplication#run(Class,String[])"
],
input: "Argument[this]",
classification: undefined,
},
{
package: "org.springframework.boot",
type: "SpringApplication",
name: "run",
signature: "(Class,String[])",
usages:
usages[
"org.springframework.boot.SpringApplication#run(Class,String[])"
],
input: "Argument[0]",
classification: undefined,
},
{
package: "org.springframework.boot",
type: "SpringApplication",
name: "run",
signature: "(Class,String[])",
usages:
usages[
"org.springframework.boot.SpringApplication#run(Class,String[])"
],
input: "Argument[1]",
classification: undefined,
},
],
});
});

View File

@@ -1,4 +1,4 @@
import { calculateModeledPercentage } from "../modeled";
import { calculateModeledPercentage } from "../../../../src/data-extensions-editor/shared/modeled-percentage";
describe("calculateModeledPercentage", () => {
it("when there are no external API usages", () => {