Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3eef0e9
Give modal methods their own describe blocks
elenatanasoiu Apr 19, 2023
ae320d0
Set up new config setting for auto generating QL packs
elenatanasoiu Apr 19, 2023
8392284
Introduce new dialog box with "No, never ask me again" option
elenatanasoiu Apr 19, 2023
f260143
Remember user's choice to not be prompted again
elenatanasoiu Apr 19, 2023
c877f6c
Fix error in skeleton wizard tests
elenatanasoiu Apr 19, 2023
4e2b26f
Rename `codeQL.createQuery.folder` -> `codeQL.createQuery.qlPackLocat…
elenatanasoiu Apr 27, 2023
9aeb520
Rename `SKELETON_WIZARD_FOLDER` -> `QL_PACK_LOCATION`
elenatanasoiu Apr 27, 2023
6034105
Rename `codeQL.autogenerateQlPacks` -> `codeQL.createQuery.autogenera…
elenatanasoiu Apr 27, 2023
025ec25
Convert `autogenerateQlPacks` to use enum
elenatanasoiu Apr 27, 2023
c71a83b
Correct test descriptions to say `ternary` instead of `binary`
elenatanasoiu Apr 27, 2023
d242117
Fix comment to list correct options
elenatanasoiu Apr 27, 2023
f10185a
Write correct value to setting
elenatanasoiu Apr 27, 2023
9c47a27
Remove "default" option
elenatanasoiu May 3, 2023
62597f1
Remove extra space
elenatanasoiu May 3, 2023
c8b0b1c
Use parent setting for "Create Query" config
elenatanasoiu May 3, 2023
530ae68
Use strictly type choices for config setting
elenatanasoiu May 3, 2023
42320fc
Don't try to restore setting
elenatanasoiu May 3, 2023
9ecb503
Reduce options to "ask" and "never"
elenatanasoiu May 3, 2023
a85d9d1
Update comment
elenatanasoiu May 3, 2023
cc8f1fd
Merge branch 'main' into yer-a-ternary-choice-query
elenatanasoiu May 3, 2023
9515e3b
Merge branch 'main' into yer-a-ternary-choice-query
elenatanasoiu May 3, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions extensions/ql-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -371,11 +371,23 @@
"default": false,
"description": "Allow database to be downloaded via HTTP. Warning: enabling this option will allow downloading from insecure servers."
},
"codeQL.createQuery.folder": {
"codeQL.createQuery.qlPackLocation": {
"type": "string",
"default": "",
"patternErrorMessage": "Please enter a valid folder",
"markdownDescription": "The name of the folder where we want to create queries and query packs via the \"CodeQL: Create Query\" command. The folder should exist."
"markdownDescription": "The name of the folder where we want to create queries and QL packs via the \"CodeQL: Create Query\" command. The folder should exist."
},
"codeQL.createQuery.autogenerateQlPacks": {
"type": "string",
"default": "ask",
"enum": [
"ask",
"never"
],
"enumDescriptions": [
"Ask to create a QL pack when a new CodeQL database is added.",
"Never create a QL pack when a new CodeQL database is added."
],
"description": "Ask the user to generate a QL pack when a new CodeQL database is downloaded."
}
}
},
Expand Down
38 changes: 30 additions & 8 deletions extensions/ql-vscode/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -666,17 +666,39 @@ export function allowHttp(): boolean {
}

/**
* The name of the folder where we want to create skeleton wizard QL packs.
* Parent setting for all settings related to the "Create Query" command.
*/
const CREATE_QUERY_COMMAND = new Setting("createQuery", ROOT_SETTING);

/**
* The name of the folder where we want to create QL packs.
**/
const SKELETON_WIZARD_FOLDER = new Setting(
"folder",
new Setting("createQuery", ROOT_SETTING),
const QL_PACK_LOCATION = new Setting("qlPackLocation", CREATE_QUERY_COMMAND);

export function getQlPackLocation(): string | undefined {
return QL_PACK_LOCATION.getValue<string>() || undefined;
}

export async function setQlPackLocation(folder: string | undefined) {
await QL_PACK_LOCATION.updateValue(folder, ConfigurationTarget.Global);
}

/**
* Whether to ask the user to autogenerate a QL pack. The options are "ask" and "never".
**/
const AUTOGENERATE_QL_PACKS = new Setting(
"autogenerateQlPacks",
CREATE_QUERY_COMMAND,
);

export function getSkeletonWizardFolder(): string | undefined {
return SKELETON_WIZARD_FOLDER.getValue<string>() || undefined;
const AutogenerateQLPacksValues = ["ask", "never"] as const;
type AutogenerateQLPacks = typeof AutogenerateQLPacksValues[number];

export function getAutogenerateQlPacks(): AutogenerateQLPacks {
const value = AUTOGENERATE_QL_PACKS.getValue<AutogenerateQLPacks>();
return AutogenerateQLPacksValues.includes(value) ? value : "ask";
}

export async function setSkeletonWizardFolder(folder: string | undefined) {
await SKELETON_WIZARD_FOLDER.updateValue(folder, ConfigurationTarget.Global);
export async function setAutogenerateQlPacks(choice: AutogenerateQLPacks) {
await AUTOGENERATE_QL_PACKS.updateValue(choice, ConfigurationTarget.Global);
}
21 changes: 17 additions & 4 deletions extensions/ql-vscode/src/databases/local-databases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import {
isLikelyDatabaseRoot,
showAndLogExceptionWithTelemetry,
isFolderAlreadyInWorkspace,
showBinaryChoiceDialog,
getFirstWorkspaceFolder,
showNeverAskAgainDialog,
} from "../helpers";
import { ProgressCallback, withProgress } from "../common/vscode/progress";
import {
Expand All @@ -26,7 +26,11 @@ import { asError, getErrorMessage } from "../pure/helpers-pure";
import { QueryRunner } from "../query-server";
import { pathsEqual } from "../pure/files";
import { redactableError } from "../pure/errors";
import { isCodespacesTemplate } from "../config";
import {
getAutogenerateQlPacks,
isCodespacesTemplate,
setAutogenerateQlPacks,
} from "../config";
import { QlPackGenerator } from "../qlpack-generator";
import { QueryLanguage } from "../common/query-language";
import { App } from "../common/app";
Expand Down Expand Up @@ -745,11 +749,20 @@ export class DatabaseManager extends DisposableObject {
return;
}

const answer = await showBinaryChoiceDialog(
if (getAutogenerateQlPacks() === "never") {
return;
}

const answer = await showNeverAskAgainDialog(
Comment thread
elenatanasoiu marked this conversation as resolved.
`We've noticed you don't have a CodeQL pack available to analyze this database. Can we set up a query pack for you?`,
);

if (!answer) {
if (answer === "No") {
return;
}

if (answer === "No, and never ask me again") {
await setAutogenerateQlPacks("never");
return;
}

Expand Down
40 changes: 40 additions & 0 deletions extensions/ql-vscode/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,46 @@ export function isWorkspaceFolderOnDisk(
return workspaceFolder.uri.scheme === "file";
}

/**
* Opens a modal dialog for the user to make a choice between yes/no/never be asked again.
*
* @param message The message to show.
* @param modal If true (the default), show a modal dialog box, otherwise dialog is non-modal and can
* be closed even if the user does not make a choice.
* @param yesTitle The text in the box indicating the affirmative choice.
* @param noTitle The text in the box indicating the negative choice.
* @param neverTitle The text in the box indicating the opt out choice.
*
* @return
* `Yes` if the user clicks 'Yes',
* `No` if the user clicks 'No' or cancels the dialog,
* `No, and never ask me again` if the user clicks 'No, and never ask me again',
* `undefined` if the dialog is closed without the user making a choice.
*/
export async function showNeverAskAgainDialog(
message: string,
modal = true,
yesTitle = "Yes",
noTitle = "No",
neverAskAgainTitle = "No, and never ask me again",
): Promise<string | undefined> {
const yesItem = { title: yesTitle, isCloseAffordance: true };
const noItem = { title: noTitle, isCloseAffordance: false };
const neverAskAgainItem = {
title: neverAskAgainTitle,
isCloseAffordance: false,
};
const chosenItem = await Window.showInformationMessage(
message,
{ modal },
yesItem,
noItem,
neverAskAgainItem,
);

return chosenItem?.title;
}

/** Gets all active workspace folders that are on the filesystem. */
export function getOnDiskWorkspaceFoldersObjects() {
const workspaceFolders = workspace.workspaceFolders ?? [];
Expand Down
8 changes: 4 additions & 4 deletions extensions/ql-vscode/src/skeleton-query-wizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ import {
downloadGitHubDatabase,
} from "./databases/database-fetcher";
import {
getSkeletonWizardFolder,
getQlPackLocation,
isCodespacesTemplate,
setSkeletonWizardFolder,
setQlPackLocation,
} from "./config";
import { existsSync } from "fs-extra";

Expand Down Expand Up @@ -115,7 +115,7 @@ export class SkeletonQueryWizard {
return firstStorageFolder;
}

let storageFolder = getSkeletonWizardFolder();
let storageFolder = getQlPackLocation();

if (storageFolder === undefined || !existsSync(storageFolder)) {
storageFolder = await Window.showInputBox({
Expand All @@ -136,7 +136,7 @@ export class SkeletonQueryWizard {
);
}

await setSkeletonWizardFolder(storageFolder);
await setQlPackLocation(storageFolder);
return storageFolder;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ describe("SkeletonQueryWizard", () => {

originalValue = workspace
.getConfiguration("codeQL.createQuery")
.get("folder");
.get("qlPackLocation");

// Set isCodespacesTemplate to true to indicate we are in the codespace template
await workspace
Expand All @@ -421,9 +421,13 @@ describe("SkeletonQueryWizard", () => {
});

afterEach(async () => {
await workspace
.getConfiguration("codeQL.createQuery")
.update("qlPackLocation", originalValue);

await workspace
.getConfiguration("codeQL")
.update("codespacesTemplate", originalValue);
.update("codespacesTemplate", false);
});

it("should not prompt the user", async () => {
Expand All @@ -445,16 +449,16 @@ describe("SkeletonQueryWizard", () => {

originalValue = workspace
.getConfiguration("codeQL.createQuery")
.get("folder");
.get("qlPackLocation");
await workspace
.getConfiguration("codeQL.createQuery")
.update("folder", storedPath);
.update("qlPackLocation", storedPath);
});

afterEach(async () => {
await workspace
.getConfiguration("codeQL.createQuery")
.update("folder", originalValue);
.update("qlPackLocation", originalValue);
});

it("should return it and not prompt the user", async () => {
Expand All @@ -474,16 +478,16 @@ describe("SkeletonQueryWizard", () => {

originalValue = workspace
.getConfiguration("codeQL.createQuery")
.get("folder");
.get("qlPackLocation");
await workspace
.getConfiguration("codeQL.createQuery")
.update("folder", storedPath);
.update("qlPackLocation", storedPath);
});

afterEach(async () => {
await workspace
.getConfiguration("codeQL.createQuery")
.update("folder", originalValue);
.update("qlPackLocation", originalValue);
});

it("should prompt the user for to provide a new folder name", async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ describe("local databases", () => {
let packAddSpy: jest.Mock<any, []>;
let logSpy: jest.Mock<any, []>;

let showBinaryChoiceDialogSpy: jest.SpiedFunction<
typeof helpers.showBinaryChoiceDialog
let showNeverAskAgainDialogSpy: jest.SpiedFunction<
typeof helpers.showNeverAskAgainDialog
>;

let dir: tmp.DirResult;
Expand All @@ -63,9 +63,9 @@ describe("local databases", () => {
/* */
});

showBinaryChoiceDialogSpy = jest
.spyOn(helpers, "showBinaryChoiceDialog")
.mockResolvedValue(true);
showNeverAskAgainDialogSpy = jest
.spyOn(helpers, "showNeverAskAgainDialog")
.mockResolvedValue("Yes");

extensionContextStoragePath = dir.name;

Expand Down Expand Up @@ -649,19 +649,31 @@ describe("local databases", () => {
it("should offer the user to set up a skeleton QL pack", async () => {
await (databaseManager as any).createSkeletonPacks(mockDbItem);

expect(showBinaryChoiceDialogSpy).toBeCalledTimes(1);
expect(showNeverAskAgainDialogSpy).toBeCalledTimes(1);
});

it("should return early if the user refuses help", async () => {
showBinaryChoiceDialogSpy = jest
.spyOn(helpers, "showBinaryChoiceDialog")
.mockResolvedValue(false);
showNeverAskAgainDialogSpy = jest
.spyOn(helpers, "showNeverAskAgainDialog")
.mockResolvedValue("No");

await (databaseManager as any).createSkeletonPacks(mockDbItem);

expect(generateSpy).not.toBeCalled();
});

it("should return early and write choice to settings if user wants to never be asked again", async () => {
showNeverAskAgainDialogSpy = jest
.spyOn(helpers, "showNeverAskAgainDialog")
.mockResolvedValue("No, and never ask me again");
const updateValueSpy = jest.spyOn(Setting.prototype, "updateValue");

await (databaseManager as any).createSkeletonPacks(mockDbItem);

expect(generateSpy).not.toBeCalled();
expect(updateValueSpy).toHaveBeenCalledWith("never", 1);
});

it("should create the skeleton QL pack for the user", async () => {
await (databaseManager as any).createSkeletonPacks(mockDbItem);

Expand Down Expand Up @@ -694,9 +706,9 @@ describe("local databases", () => {
});

it("should exit early", async () => {
showBinaryChoiceDialogSpy = jest
.spyOn(helpers, "showBinaryChoiceDialog")
.mockResolvedValue(false);
showNeverAskAgainDialogSpy = jest
.spyOn(helpers, "showNeverAskAgainDialog")
.mockResolvedValue("No");

await (databaseManager as any).createSkeletonPacks(mockDbItem);

Expand Down
Loading