Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions extensions/ql-vscode/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## [UNRELEASED]

- Fix Quick Query failing to start when no workspace is open.
- After an extension-managed CodeQL CLI update, the notification now offers to open the CLI release notes instead of the extension log. [#1095](https://github.com/github/vscode-codeql/issues/1095)

## 1.17.8 - 17 July 2026
Expand Down
12 changes: 12 additions & 0 deletions extensions/ql-vscode/src/local-queries/quick-query-dir.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ensureDir } from "fs-extra";
import { join } from "path";
import type { App } from "../common/app";

const QUICK_QUERIES_DIR_NAME = "quick-queries";

export async function getQuickQueriesDir(app: App): Promise<string> {
const storagePath = app.workspaceStoragePath ?? app.globalStoragePath;
const queriesPath = join(storagePath, QUICK_QUERIES_DIR_NAME);
await ensureDir(queriesPath, { mode: 0o700 });
return queriesPath;
}
15 changes: 2 additions & 13 deletions extensions/ql-vscode/src/local-queries/quick-query.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ensureDir, writeFile, pathExists, readFile } from "fs-extra";
import { writeFile, pathExists, readFile } from "fs-extra";
import { dump, load } from "js-yaml";
import { basename, join } from "path";
import { window as Window, workspace, Uri } from "vscode";
Expand All @@ -11,11 +11,10 @@ import type { ProgressCallback } from "../common/vscode/progress";
import { UserCancellationException } from "../common/vscode/progress";
import { getErrorMessage } from "../common/helpers-pure";
import { FALLBACK_QLPACK_FILENAME, getQlPackFilePath } from "../common/ql";
import type { App } from "../common/app";
import type { ExtensionApp } from "../common/vscode/extension-app";
import type { QlPackFile } from "../packaging/qlpack-file";
import { getQuickQueriesDir } from "./quick-query-dir";

const QUICK_QUERIES_DIR_NAME = "quick-queries";
const QUICK_QUERY_QUERY_NAME = "quick-query.ql";
const QUICK_QUERY_WORKSPACE_FOLDER_NAME = "Quick Queries";
const QLPACK_FILE_HEADER = "# This is an automatically generated file.\n\n";
Expand All @@ -24,16 +23,6 @@ export function isQuickQueryPath(queryPath: string): boolean {
return basename(queryPath) === QUICK_QUERY_QUERY_NAME;
}

async function getQuickQueriesDir(app: App): Promise<string> {
const storagePath = app.workspaceStoragePath;
if (storagePath === undefined) {
throw new Error("Workspace storage path is undefined");
}
const queriesPath = join(storagePath, QUICK_QUERIES_DIR_NAME);
await ensureDir(queriesPath, { mode: 0o700 });
return queriesPath;
}

function updateQuickQueryDir(queriesDir: string, index: number, len: number) {
workspace.updateWorkspaceFolders(index, len, {
uri: Uri.file(queriesDir),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { DirResult } from "tmp";
import { dirSync } from "tmp";
import { pathExists, readFile, writeFile } from "fs-extra";
import { join } from "path";
import { createMockApp } from "../../__mocks__/appMock";
import { getQuickQueriesDir } from "../../../src/local-queries/quick-query-dir";

describe("getQuickQueriesDir", () => {
let dir: DirResult;

beforeEach(() => {
dir = dirSync({
unsafeCleanup: true,
});
});

afterEach(() => {
dir.removeCallback();
});

it("uses global storage when no workspace is open", async () => {
const app = {
...createMockApp({ globalStoragePath: dir.name }),
workspaceStoragePath: undefined,
};

const quickQueriesDir = await getQuickQueriesDir(app);

expect(quickQueriesDir).toBe(join(dir.name, "quick-queries"));
expect(await pathExists(quickQueriesDir)).toBe(true);
});

it("preserves existing query files across repeated calls", async () => {
const app = createMockApp({ workspaceStoragePath: dir.name });
const quickQueriesDir = await getQuickQueriesDir(app);
const queryPath = join(quickQueriesDir, "quick-query.ql");
await writeFile(queryPath, "select 1");

expect(await getQuickQueriesDir(app)).toBe(quickQueriesDir);
expect(await readFile(queryPath, "utf8")).toBe("select 1");
});

it("propagates an error when the storage path is a file", async () => {
const storagePath = join(dir.name, "not-a-directory");
await writeFile(storagePath, "existing file");
const app = createMockApp({ workspaceStoragePath: storagePath });

await expect(getQuickQueriesDir(app)).rejects.toThrow();
expect(await readFile(storagePath, "utf8")).toBe("existing file");
});

it("prefers workspace storage when a workspace is open", async () => {
const workspaceStoragePath = join(dir.name, "workspace-storage");
const app = createMockApp({
workspaceStoragePath,
globalStoragePath: dir.name,
});

const quickQueriesDir = await getQuickQueriesDir(app);

expect(quickQueriesDir).toBe(join(workspaceStoragePath, "quick-queries"));
expect(await pathExists(quickQueriesDir)).toBe(true);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { window, workspace } from "vscode";
import { dirSync } from "tmp";
import type { CodeQLCliServer } from "../../../../src/codeql-cli/cli";
import type { ExtensionApp } from "../../../../src/common/vscode/extension-app";
import type { DatabaseUI } from "../../../../src/databases/local-databases-ui";
import { displayQuickQuery } from "../../../../src/local-queries/quick-query";
import { createMockApp } from "../../../__mocks__/appMock";
import { mockedObject } from "../../utils/mocking.helpers";

describe("Quick Query without a workspace", () => {
it("shows the existing warning instead of a storage-path error", async () => {
const dir = dirSync({ unsafeCleanup: true });
const warning = jest
.spyOn(window, "showWarningMessage")
.mockResolvedValue(undefined);

try {
expect(workspace.workspaceFile).toBeUndefined();
expect(workspace.workspaceFolders ?? []).toHaveLength(0);

const app = mockedObject<ExtensionApp>({
...createMockApp({ globalStoragePath: dir.name }),
workspaceStoragePath: undefined,
});

await displayQuickQuery(
app,
mockedObject<CodeQLCliServer>({}),
mockedObject<DatabaseUI>({}),
jest.fn(),
);

expect(warning).toHaveBeenCalledWith(
'"Quick query" requires reloading your workspace as a multi-root workspace, which may cause query history and databases to be lost.',
{
modal: true,
detail:
'The "Create query" command does not require reloading the workspace.',
},
'Run "Create query"',
'Run "Quick query" anyway',
);
} finally {
warning.mockRestore();
dir.removeCallback();
}
});
});