Skip to content

Commit 20469b0

Browse files
Merge branch 'main' into robertbrignull/remove_selected_model
2 parents 26fcef8 + 96fb004 commit 20469b0

File tree

12 files changed

+205
-35
lines changed

12 files changed

+205
-35
lines changed

extensions/ql-vscode/scripts/source-map.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -115,21 +115,35 @@ async function extractSourceMap() {
115115
}
116116

117117
if (stacktrace.includes("at")) {
118-
const rawSourceMaps = new Map<string, RawSourceMap>();
118+
const rawSourceMaps = new Map<string, RawSourceMap | null>();
119119

120120
const mappedStacktrace = await replaceAsync(
121121
stacktrace,
122122
stackLineRegex,
123123
async (match, name, file, line, column) => {
124124
if (!rawSourceMaps.has(file)) {
125-
const rawSourceMap: RawSourceMap = await readJSON(
126-
resolve(sourceMapsDirectory, `${basename(file)}.map`),
127-
);
128-
rawSourceMaps.set(file, rawSourceMap);
125+
try {
126+
const rawSourceMap: RawSourceMap = await readJSON(
127+
resolve(sourceMapsDirectory, `${basename(file)}.map`),
128+
);
129+
rawSourceMaps.set(file, rawSourceMap);
130+
} catch (e: unknown) {
131+
// If the file is not found, we will not decode it and not try reading this source map again
132+
if (e instanceof Error && "code" in e && e.code === "ENOENT") {
133+
rawSourceMaps.set(file, null);
134+
} else {
135+
throw e;
136+
}
137+
}
138+
}
139+
140+
const sourceMap = rawSourceMaps.get(file);
141+
if (!sourceMap) {
142+
return match;
129143
}
130144

131145
const originalPosition = await SourceMapConsumer.with(
132-
rawSourceMaps.get(file) as RawSourceMap,
146+
sourceMap,
133147
null,
134148
async function (consumer) {
135149
return consumer.originalPositionFor({

extensions/ql-vscode/src/config.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -702,6 +702,10 @@ export function showQueriesPanel(): boolean {
702702
const MODEL_SETTING = new Setting("model", ROOT_SETTING);
703703
const FLOW_GENERATION = new Setting("flowGeneration", MODEL_SETTING);
704704
const LLM_GENERATION = new Setting("llmGeneration", MODEL_SETTING);
705+
const LLM_GENERATION_BATCH_SIZE = new Setting(
706+
"llmGenerationBatchSize",
707+
MODEL_SETTING,
708+
);
705709
const EXTENSIONS_DIRECTORY = new Setting("extensionsDirectory", MODEL_SETTING);
706710
const SHOW_MULTIPLE_MODELS = new Setting("showMultipleModels", MODEL_SETTING);
707711

@@ -725,6 +729,14 @@ export class ModelConfigListener extends ConfigListener implements ModelConfig {
725729
return !!LLM_GENERATION.getValue<boolean>();
726730
}
727731

732+
/**
733+
* Limits the number of candidates we send to the model in each request to avoid long requests.
734+
* Note that the model may return fewer than this number of candidates.
735+
*/
736+
public get llmGenerationBatchSize(): number {
737+
return LLM_GENERATION_BATCH_SIZE.getValue<number | null>() || 10;
738+
}
739+
728740
public getExtensionsDirectory(languageId: string): string | undefined {
729741
return EXTENSIONS_DIRECTORY.getValue<string>({
730742
languageId,

extensions/ql-vscode/src/model-editor/auto-modeler.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,7 @@ import { DatabaseItem } from "../databases/local-databases";
1717
import { Mode } from "./shared/mode";
1818
import { CancellationTokenSource } from "vscode";
1919
import { ModelingStore } from "./modeling-store";
20-
21-
// Limit the number of candidates we send to the model in each request
22-
// to avoid long requests.
23-
// Note that the model may return fewer than this number of candidates.
24-
const candidateBatchSize = 20;
20+
import { ModelConfigListener } from "../config";
2521

2622
/**
2723
* The auto-modeler holds state around auto-modeling jobs and allows
@@ -36,6 +32,7 @@ export class AutoModeler {
3632
private readonly app: App,
3733
private readonly cliServer: CodeQLCliServer,
3834
private readonly queryRunner: QueryRunner,
35+
private readonly modelConfig: ModelConfigListener,
3936
private readonly modelingStore: ModelingStore,
4037
private readonly queryStorageDir: string,
4138
private readonly databaseItem: DatabaseItem,
@@ -109,6 +106,9 @@ export class AutoModeler {
109106
cancellationTokenSource: CancellationTokenSource,
110107
): Promise<void> {
111108
void extLogger.log(`Modeling package ${packageName}`);
109+
110+
const candidateBatchSize = this.modelConfig.llmGenerationBatchSize;
111+
112112
await withProgress(async (progress) => {
113113
// Fetch the candidates to send to the model
114114
const allCandidateMethods = getCandidates(mode, methods, modeledMethods);

extensions/ql-vscode/src/model-editor/methods-usage/methods-usage-data-provider.ts

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -89,26 +89,26 @@ export class MethodsUsageDataProvider
8989

9090
getTreeItem(item: MethodsUsageTreeViewItem): TreeItem {
9191
if (isMethodTreeViewItem(item)) {
92+
const { method } = item;
93+
9294
return {
93-
label: `${item.packageName}.${item.typeName}.${item.methodName}${item.methodParameters}`,
95+
label: `${method.packageName}.${method.typeName}.${method.methodName}${method.methodParameters}`,
9496
collapsibleState: TreeItemCollapsibleState.Collapsed,
95-
iconPath: this.getModelingStatusIcon(item),
97+
iconPath: this.getModelingStatusIcon(method),
9698
};
9799
} else {
98-
const method = this.getParent(item);
99-
if (!method || !isMethodTreeViewItem(method)) {
100-
throw new Error("Parent not found for tree item");
101-
}
100+
const { method, usage } = item;
101+
102102
return {
103-
label: item.label,
104-
description: `${this.relativePathWithinDatabase(item.url.uri)} [${
105-
item.url.startLine
106-
}, ${item.url.endLine}]`,
103+
label: usage.label,
104+
description: `${this.relativePathWithinDatabase(usage.url.uri)} [${
105+
usage.url.startLine
106+
}, ${usage.url.endLine}]`,
107107
collapsibleState: TreeItemCollapsibleState.None,
108108
command: {
109109
title: "Show usage",
110110
command: "codeQLModelEditor.jumpToMethod",
111-
arguments: [method, item, this.databaseItem],
111+
arguments: [method, usage, this.databaseItem],
112112
},
113113
};
114114
}
@@ -146,7 +146,7 @@ export class MethodsUsageDataProvider
146146
getChildren(item?: MethodsUsageTreeViewItem): MethodsUsageTreeViewItem[] {
147147
if (item === undefined) {
148148
if (this.hideModeledMethods) {
149-
return this.sortedTreeItems.filter((api) => !api.supported);
149+
return this.sortedTreeItems.filter((api) => !api.method.supported);
150150
} else {
151151
return [...this.sortedTreeItems];
152152
}
@@ -172,21 +172,24 @@ export class MethodsUsageDataProvider
172172
usage: Usage,
173173
): UsageTreeViewItem | undefined {
174174
const method = this.sortedTreeItems.find(
175-
(m) => m.signature === methodSignature,
175+
(m) => m.method.signature === methodSignature,
176176
);
177177
if (!method) {
178178
return undefined;
179179
}
180180

181-
return method.children.find((u) => usagesAreEqual(u, usage));
181+
return method.children.find((u) => usagesAreEqual(u.usage, usage));
182182
}
183183
}
184184

185-
type MethodTreeViewItem = Method & {
185+
type MethodTreeViewItem = {
186+
method: Method;
186187
children: UsageTreeViewItem[];
187188
};
188189

189-
type UsageTreeViewItem = Usage & {
190+
type UsageTreeViewItem = {
191+
method: Method;
192+
usage: Usage;
190193
parent: MethodTreeViewItem;
191194
};
192195

@@ -195,7 +198,7 @@ export type MethodsUsageTreeViewItem = MethodTreeViewItem | UsageTreeViewItem;
195198
function isMethodTreeViewItem(
196199
item: MethodsUsageTreeViewItem,
197200
): item is MethodTreeViewItem {
198-
return "children" in item && "usages" in item;
201+
return "children" in item && "method" in item;
199202
}
200203

201204
function usagesAreEqual(u1: Usage, u2: Usage): boolean {
@@ -225,12 +228,13 @@ function sortMethodsInGroups(methods: readonly Method[], mode: Mode): Method[] {
225228
function createTreeItems(methods: readonly Method[]): MethodTreeViewItem[] {
226229
return methods.map((method) => {
227230
const newMethod: MethodTreeViewItem = {
228-
...method,
231+
method,
229232
children: [],
230233
};
231234

232235
newMethod.children = method.usages.map((usage) => ({
233-
...usage,
236+
method,
237+
usage,
234238
// This needs to be a reference to the parent method, not a copy of it.
235239
parent: newMethod,
236240
}));

extensions/ql-vscode/src/model-editor/model-editor-view.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export class ModelEditorView extends AbstractWebview<
7676
app,
7777
cliServer,
7878
queryRunner,
79+
this.modelConfig,
7980
modelingStore,
8081
queryStorageDir,
8182
databaseItem,
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import * as React from "react";
2+
3+
import { Meta, StoryFn } from "@storybook/react";
4+
5+
import { InProgressDropdown as InProgressDropdownComponent } from "../../view/model-editor/InProgressDropdown";
6+
7+
export default {
8+
title: "CodeQL Model Editor/In Progress Dropdown",
9+
component: InProgressDropdownComponent,
10+
} as Meta<typeof InProgressDropdownComponent>;
11+
12+
const Template: StoryFn<typeof InProgressDropdownComponent> = (args) => (
13+
<InProgressDropdownComponent />
14+
);
15+
16+
export const InProgressDropdown = Template.bind({});

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type Props = {
1919
value: string | undefined;
2020
options: Array<{ value: string; label: string }>;
2121
disabled?: boolean;
22+
className?: string;
2223
disabledPlaceholder?: string;
2324
onChange?: (event: ChangeEvent<HTMLSelectElement>) => void;
2425

@@ -40,6 +41,7 @@ export function Dropdown({
4041
options,
4142
disabled,
4243
disabledPlaceholder,
44+
className,
4345
onChange,
4446
...props
4547
}: Props) {
@@ -49,6 +51,7 @@ export function Dropdown({
4951
value={disabled ? disabledValue : value}
5052
disabled={disabled}
5153
onChange={onChange}
54+
className={className}
5255
{...props}
5356
>
5457
{disabled ? (

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import * as React from "react";
2+
import { styled } from "styled-components";
23
import { Dropdown } from "../common/Dropdown";
34

5+
const StyledDropdown = styled(Dropdown)`
6+
font-style: italic;
7+
`;
8+
49
export const InProgressDropdown = () => {
510
return (
6-
<Dropdown
11+
<StyledDropdown
712
value="Thinking..."
813
options={[]}
914
disabled={true}

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,33 @@ const ModelableMethodRow = forwardRef<HTMLElement | undefined, MethodRowProps>(
126126
[method, modeledMethods, onChange],
127127
);
128128

129+
const removeModelClickedHandlers = useMemo(
130+
() =>
131+
modeledMethods.map((_, index) => () => {
132+
const newModeledMethods = [...modeledMethods];
133+
newModeledMethods.splice(index, 1);
134+
onChange(method.signature, newModeledMethods);
135+
}),
136+
[method, modeledMethods, onChange],
137+
);
138+
139+
const handleAddModelClick = useCallback(() => {
140+
const newModeledMethod: ModeledMethod = {
141+
type: "none",
142+
input: "",
143+
output: "",
144+
kind: "",
145+
provenance: "manual",
146+
signature: method.signature,
147+
packageName: method.packageName,
148+
typeName: method.typeName,
149+
methodName: method.methodName,
150+
methodParameters: method.methodParameters,
151+
};
152+
const newModeledMethods = [...modeledMethods, newModeledMethod];
153+
onChange(method.signature, newModeledMethods);
154+
}, [method, modeledMethods, onChange]);
155+
129156
const jumpToMethod = useCallback(
130157
() => sendJumpToMethodMessage(method),
131158
[method],
@@ -228,6 +255,7 @@ const ModelableMethodRow = forwardRef<HTMLElement | undefined, MethodRowProps>(
228255
key={index}
229256
appearance="icon"
230257
aria-label="Add new model"
258+
onClick={handleAddModelClick}
231259
disabled={addModelButtonDisabled}
232260
>
233261
<Codicon name="add" />
@@ -237,6 +265,7 @@ const ModelableMethodRow = forwardRef<HTMLElement | undefined, MethodRowProps>(
237265
key={index}
238266
appearance="icon"
239267
aria-label="Remove model"
268+
onClick={removeModelClickedHandlers[index]}
240269
>
241270
<Codicon name="trash" />
242271
</CodiconRow>

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

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,4 +358,84 @@ describe(MethodRow.name, () => {
358358
expect(removeButton?.getElementsByTagName("input")[0]).toBeEnabled();
359359
}
360360
});
361+
362+
it("can add a new model", async () => {
363+
render({
364+
modeledMethods: [modeledMethod],
365+
viewState: {
366+
...viewState,
367+
showMultipleModels: true,
368+
},
369+
});
370+
371+
onChange.mockReset();
372+
await userEvent.click(screen.getByLabelText("Add new model"));
373+
374+
expect(onChange).toHaveBeenCalledTimes(1);
375+
expect(onChange).toHaveBeenCalledWith(method.signature, [
376+
modeledMethod,
377+
{
378+
type: "none",
379+
input: "",
380+
output: "",
381+
kind: "",
382+
provenance: "manual",
383+
signature: method.signature,
384+
packageName: method.packageName,
385+
typeName: method.typeName,
386+
methodName: method.methodName,
387+
methodParameters: method.methodParameters,
388+
},
389+
]);
390+
});
391+
392+
it("can delete the first modeled method", async () => {
393+
render({
394+
modeledMethods: [
395+
{ ...modeledMethod, type: "source" },
396+
{ ...modeledMethod, type: "sink" },
397+
{ ...modeledMethod, type: "none" },
398+
{ ...modeledMethod, type: "summary" },
399+
],
400+
viewState: {
401+
...viewState,
402+
showMultipleModels: true,
403+
},
404+
});
405+
406+
onChange.mockReset();
407+
await userEvent.click(screen.getAllByLabelText("Remove model")[0]);
408+
409+
expect(onChange).toHaveBeenCalledTimes(1);
410+
expect(onChange).toHaveBeenCalledWith(method.signature, [
411+
{ ...modeledMethod, type: "sink" },
412+
{ ...modeledMethod, type: "none" },
413+
{ ...modeledMethod, type: "summary" },
414+
]);
415+
});
416+
417+
it("can delete a modeled method in the middle", async () => {
418+
render({
419+
modeledMethods: [
420+
{ ...modeledMethod, type: "source" },
421+
{ ...modeledMethod, type: "sink" },
422+
{ ...modeledMethod, type: "none" },
423+
{ ...modeledMethod, type: "summary" },
424+
],
425+
viewState: {
426+
...viewState,
427+
showMultipleModels: true,
428+
},
429+
});
430+
431+
onChange.mockReset();
432+
await userEvent.click(screen.getAllByLabelText("Remove model")[2]);
433+
434+
expect(onChange).toHaveBeenCalledTimes(1);
435+
expect(onChange).toHaveBeenCalledWith(method.signature, [
436+
{ ...modeledMethod, type: "source" },
437+
{ ...modeledMethod, type: "sink" },
438+
{ ...modeledMethod, type: "summary" },
439+
]);
440+
});
361441
});

0 commit comments

Comments
 (0)