Create new queries in selected folder of queries panel
This will change the behavior of the "Create new query" command to create the new query in the same folder as the first selected item in the queries panel. If no items are selected, the behavior is the same as before. I've used events to communicate the selection from the queries panel to the local queries module. This is some more code and some extra complexity, but it ensures that we don't have a dependency from the local queries module to the queries panel module. This makes testing easier.
This commit is contained in:
@@ -795,7 +795,11 @@ async function activateWithInstalledDistribution(
|
||||
);
|
||||
ctx.subscriptions.push(databaseUI);
|
||||
|
||||
QueriesModule.initialize(app, languageContext, cliServer);
|
||||
const queriesModule = QueriesModule.initialize(
|
||||
app,
|
||||
languageContext,
|
||||
cliServer,
|
||||
);
|
||||
|
||||
void extLogger.log("Initializing evaluator log viewer.");
|
||||
const evalLogViewer = new EvalLogViewer();
|
||||
@@ -934,6 +938,10 @@ async function activateWithInstalledDistribution(
|
||||
);
|
||||
ctx.subscriptions.push(localQueries);
|
||||
|
||||
queriesModule.onDidChangeSelection((event) =>
|
||||
localQueries.setSelectedQueryTreeViewItems(event.selection),
|
||||
);
|
||||
|
||||
void extLogger.log("Initializing debugger factory.");
|
||||
ctx.subscriptions.push(
|
||||
new QLDebugAdapterDescriptorFactory(queryStorageDir, qs, localQueries),
|
||||
|
||||
@@ -63,6 +63,8 @@ export enum QuickEvalType {
|
||||
}
|
||||
|
||||
export class LocalQueries extends DisposableObject {
|
||||
private selectedQueryTreeViewItems: readonly QueryTreeViewItem[] = [];
|
||||
|
||||
public constructor(
|
||||
private readonly app: App,
|
||||
private readonly queryRunner: QueryRunner,
|
||||
@@ -77,6 +79,12 @@ export class LocalQueries extends DisposableObject {
|
||||
super();
|
||||
}
|
||||
|
||||
public setSelectedQueryTreeViewItems(
|
||||
selection: readonly QueryTreeViewItem[],
|
||||
) {
|
||||
this.selectedQueryTreeViewItems = selection;
|
||||
}
|
||||
|
||||
public getCommands(): LocalQueryCommands {
|
||||
return {
|
||||
"codeQL.runQuery": this.runQuery.bind(this),
|
||||
@@ -333,6 +341,7 @@ export class LocalQueries extends DisposableObject {
|
||||
this.app.logger,
|
||||
this.databaseManager,
|
||||
contextStoragePath,
|
||||
this.selectedQueryTreeViewItems,
|
||||
language,
|
||||
);
|
||||
await skeletonQueryWizard.execute();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { join } from "path";
|
||||
import { Uri, workspace, window as Window } from "vscode";
|
||||
import { dirname, join } from "path";
|
||||
import { Uri, window as Window, workspace } from "vscode";
|
||||
import { CodeQLCliServer } from "../codeql-cli/cli";
|
||||
import { BaseLogger } from "../common/logging";
|
||||
import { Credentials } from "../common/authentication";
|
||||
@@ -24,8 +24,9 @@ import {
|
||||
isCodespacesTemplate,
|
||||
setQlPackLocation,
|
||||
} from "../config";
|
||||
import { existsSync } from "fs-extra";
|
||||
import { lstat, pathExists } from "fs-extra";
|
||||
import { askForLanguage } from "../codeql-cli/query-language";
|
||||
import { QueryTreeViewItem } from "../queries-panel/query-tree-view-item";
|
||||
|
||||
type QueryLanguagesToDatabaseMap = Record<string, string>;
|
||||
|
||||
@@ -51,6 +52,7 @@ export class SkeletonQueryWizard {
|
||||
private readonly logger: BaseLogger,
|
||||
private readonly databaseManager: DatabaseManager,
|
||||
private readonly databaseStoragePath: string | undefined,
|
||||
private readonly selectedItems: readonly QueryTreeViewItem[],
|
||||
private language: QueryLanguage | undefined = undefined,
|
||||
) {}
|
||||
|
||||
@@ -71,7 +73,7 @@ export class SkeletonQueryWizard {
|
||||
this.qlPackStoragePath = await this.determineStoragePath();
|
||||
|
||||
const skeletonPackAlreadyExists =
|
||||
existsSync(join(this.qlPackStoragePath, this.folderName)) ||
|
||||
(await pathExists(join(this.qlPackStoragePath, this.folderName))) ||
|
||||
isFolderAlreadyInWorkspace(this.folderName);
|
||||
|
||||
if (skeletonPackAlreadyExists) {
|
||||
@@ -109,7 +111,29 @@ export class SkeletonQueryWizard {
|
||||
});
|
||||
}
|
||||
|
||||
public async determineStoragePath() {
|
||||
public async determineStoragePath(): Promise<string> {
|
||||
if (this.selectedItems.length === 0) {
|
||||
return this.determineRootStoragePath();
|
||||
}
|
||||
|
||||
// Just like VS Code's "New File" command, if the user has selected multiple files/folders in the queries panel,
|
||||
// we will create the new file in the same folder as the first selected item.
|
||||
// See https://github.com/microsoft/vscode/blob/a8b7239d0311d4915b57c837972baf4b01394491/src/vs/workbench/contrib/files/browser/fileActions.ts#L893-L900
|
||||
const selectedItem = this.selectedItems[0];
|
||||
|
||||
const path = selectedItem.path;
|
||||
|
||||
// We use stat to protect against outdated query tree items
|
||||
const fileStat = await lstat(path);
|
||||
|
||||
if (fileStat.isDirectory()) {
|
||||
return path;
|
||||
}
|
||||
|
||||
return dirname(path);
|
||||
}
|
||||
|
||||
public async determineRootStoragePath() {
|
||||
const firstStorageFolder = getFirstWorkspaceFolder();
|
||||
|
||||
if (isCodespacesTemplate()) {
|
||||
@@ -118,7 +142,7 @@ export class SkeletonQueryWizard {
|
||||
|
||||
let storageFolder = getQlPackLocation();
|
||||
|
||||
if (storageFolder === undefined || !existsSync(storageFolder)) {
|
||||
if (storageFolder === undefined || !(await pathExists(storageFolder))) {
|
||||
storageFolder = await Window.showInputBox({
|
||||
title:
|
||||
"Please choose a folder in which to create your new query pack. You can change this in the extension settings.",
|
||||
@@ -131,7 +155,7 @@ export class SkeletonQueryWizard {
|
||||
throw new UserCancellationException("No storage folder entered.");
|
||||
}
|
||||
|
||||
if (!existsSync(storageFolder)) {
|
||||
if (!(await pathExists(storageFolder))) {
|
||||
throw new UserCancellationException(
|
||||
"Invalid folder. Must be a folder that already exists.",
|
||||
);
|
||||
|
||||
@@ -7,9 +7,18 @@ import { QueriesPanel } from "./queries-panel";
|
||||
import { QueryDiscovery } from "./query-discovery";
|
||||
import { QueryPackDiscovery } from "./query-pack-discovery";
|
||||
import { LanguageContextStore } from "../language-context-store";
|
||||
import { TreeViewSelectionChangeEvent } from "vscode";
|
||||
import { QueryTreeViewItem } from "./query-tree-view-item";
|
||||
|
||||
export class QueriesModule extends DisposableObject {
|
||||
private queriesPanel: QueriesPanel | undefined;
|
||||
private readonly onDidChangeSelectionEmitter = this.push(
|
||||
this.app.createEventEmitter<
|
||||
TreeViewSelectionChangeEvent<QueryTreeViewItem>
|
||||
>(),
|
||||
);
|
||||
|
||||
public readonly onDidChangeSelection = this.onDidChangeSelectionEmitter.event;
|
||||
|
||||
private constructor(readonly app: App) {
|
||||
super();
|
||||
@@ -52,6 +61,9 @@ export class QueriesModule extends DisposableObject {
|
||||
void queryDiscovery.initialRefresh();
|
||||
|
||||
this.queriesPanel = new QueriesPanel(queryDiscovery, app);
|
||||
this.queriesPanel.onDidChangeSelection((event) =>
|
||||
this.onDidChangeSelectionEmitter.fire(event),
|
||||
);
|
||||
this.push(this.queriesPanel);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { DisposableObject } from "../common/disposable-object";
|
||||
import { QueryTreeDataProvider } from "./query-tree-data-provider";
|
||||
import { QueryDiscovery } from "./query-discovery";
|
||||
import { TextEditor, TreeView, window } from "vscode";
|
||||
import {
|
||||
Event,
|
||||
TextEditor,
|
||||
TreeView,
|
||||
TreeViewSelectionChangeEvent,
|
||||
window,
|
||||
} from "vscode";
|
||||
import { App } from "../common/app";
|
||||
import { QueryTreeViewItem } from "./query-tree-view-item";
|
||||
|
||||
@@ -16,6 +22,7 @@ export class QueriesPanel extends DisposableObject {
|
||||
super();
|
||||
|
||||
this.dataProvider = new QueryTreeDataProvider(queryDiscovery, app);
|
||||
this.push(this.dataProvider);
|
||||
|
||||
this.treeView = window.createTreeView("codeQLQueries", {
|
||||
treeDataProvider: this.dataProvider,
|
||||
@@ -25,6 +32,12 @@ export class QueriesPanel extends DisposableObject {
|
||||
this.subscribeToTreeSelectionEvents();
|
||||
}
|
||||
|
||||
public get onDidChangeSelection(): Event<
|
||||
TreeViewSelectionChangeEvent<QueryTreeViewItem>
|
||||
> {
|
||||
return this.treeView.onDidChangeSelection;
|
||||
}
|
||||
|
||||
private subscribeToTreeSelectionEvents(): void {
|
||||
// Keep track of whether the user has changed their text editor while
|
||||
// the tree view was not visible. If so, we will focus the text editor
|
||||
|
||||
@@ -9,8 +9,14 @@ import { TextDocument, window, workspace, WorkspaceFolder } from "vscode";
|
||||
import { extLogger } from "../../../../src/common/logging/vscode";
|
||||
import { QlPackGenerator } from "../../../../src/local-queries/qlpack-generator";
|
||||
import * as workspaceFolders from "../../../../src/common/vscode/workspace-folders";
|
||||
import { createFileSync, ensureDirSync, removeSync } from "fs-extra";
|
||||
import { join } from "path";
|
||||
import {
|
||||
createFileSync,
|
||||
ensureDir,
|
||||
ensureDirSync,
|
||||
ensureFile,
|
||||
removeSync,
|
||||
} from "fs-extra";
|
||||
import { dirname, join } from "path";
|
||||
import { testCredentialsWithStub } from "../../../factories/authentication";
|
||||
import {
|
||||
DatabaseItem,
|
||||
@@ -22,6 +28,11 @@ import { createMockDB } from "../../../factories/databases/databases";
|
||||
import { asError } from "../../../../src/common/helpers-pure";
|
||||
import { Setting } from "../../../../src/config";
|
||||
import { QueryLanguage } from "../../../../src/common/query-language";
|
||||
import {
|
||||
createQueryTreeFileItem,
|
||||
createQueryTreeFolderItem,
|
||||
QueryTreeViewItem,
|
||||
} from "../../../../src/queries-panel/query-tree-view-item";
|
||||
|
||||
describe("SkeletonQueryWizard", () => {
|
||||
let mockCli: CodeQLCliServer;
|
||||
@@ -49,6 +60,7 @@ describe("SkeletonQueryWizard", () => {
|
||||
|
||||
const credentials = testCredentialsWithStub();
|
||||
const chosenLanguage = "ruby";
|
||||
const selectedItems: QueryTreeViewItem[] = [];
|
||||
|
||||
jest.spyOn(extLogger, "log").mockResolvedValue(undefined);
|
||||
|
||||
@@ -117,6 +129,7 @@ describe("SkeletonQueryWizard", () => {
|
||||
extLogger,
|
||||
mockDatabaseManager,
|
||||
storagePath,
|
||||
selectedItems,
|
||||
);
|
||||
|
||||
askForGitHubRepoSpy = jest
|
||||
@@ -144,6 +157,7 @@ describe("SkeletonQueryWizard", () => {
|
||||
extLogger,
|
||||
mockDatabaseManager,
|
||||
storagePath,
|
||||
selectedItems,
|
||||
QueryLanguage.Swift,
|
||||
);
|
||||
});
|
||||
@@ -272,6 +286,7 @@ describe("SkeletonQueryWizard", () => {
|
||||
extLogger,
|
||||
mockDatabaseManagerWithItems,
|
||||
storagePath,
|
||||
selectedItems,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -407,7 +422,7 @@ describe("SkeletonQueryWizard", () => {
|
||||
});
|
||||
|
||||
describe("determineStoragePath", () => {
|
||||
it("should prompt the user to provide a storage path", async () => {
|
||||
it("should prompt the user to provide a storage path when no items are selected", async () => {
|
||||
const chosenPath = await wizard.determineStoragePath();
|
||||
|
||||
expect(showInputBoxSpy).toHaveBeenCalledWith(
|
||||
@@ -416,10 +431,142 @@ describe("SkeletonQueryWizard", () => {
|
||||
expect(chosenPath).toEqual(storagePath);
|
||||
});
|
||||
|
||||
describe("with folders and files", () => {
|
||||
let queriesDir: tmp.DirResult;
|
||||
|
||||
beforeEach(async () => {
|
||||
queriesDir = tmp.dirSync({
|
||||
prefix: "queries_",
|
||||
unsafeCleanup: true,
|
||||
});
|
||||
|
||||
await ensureDir(join(queriesDir.name, "folder"));
|
||||
await ensureFile(join(queriesDir.name, "queries-java", "example.ql"));
|
||||
});
|
||||
|
||||
describe("with selected folder", () => {
|
||||
let selectedItems: QueryTreeViewItem[];
|
||||
|
||||
beforeEach(async () => {
|
||||
selectedItems = [
|
||||
createQueryTreeFolderItem(
|
||||
"folder",
|
||||
join(queriesDir.name, "folder"),
|
||||
[
|
||||
createQueryTreeFileItem(
|
||||
"example.ql",
|
||||
join(queriesDir.name, "folder", "example.ql"),
|
||||
"java",
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
wizard = new SkeletonQueryWizard(
|
||||
mockCli,
|
||||
jest.fn(),
|
||||
credentials,
|
||||
extLogger,
|
||||
mockDatabaseManager,
|
||||
storagePath,
|
||||
selectedItems,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns the selected folder path", async () => {
|
||||
const chosenPath = await wizard.determineStoragePath();
|
||||
|
||||
expect(chosenPath).toEqual(selectedItems[0].path);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with selected file", () => {
|
||||
let selectedItems: QueryTreeViewItem[];
|
||||
|
||||
beforeEach(async () => {
|
||||
selectedItems = [
|
||||
createQueryTreeFileItem(
|
||||
"example.ql",
|
||||
join(queriesDir.name, "queries-java", "example.ql"),
|
||||
"java",
|
||||
),
|
||||
];
|
||||
|
||||
wizard = new SkeletonQueryWizard(
|
||||
mockCli,
|
||||
jest.fn(),
|
||||
credentials,
|
||||
extLogger,
|
||||
mockDatabaseManager,
|
||||
storagePath,
|
||||
selectedItems,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns the selected file path", async () => {
|
||||
const chosenPath = await wizard.determineStoragePath();
|
||||
|
||||
expect(chosenPath).toEqual(dirname(selectedItems[0].path));
|
||||
});
|
||||
});
|
||||
|
||||
describe("with multiple selected items", () => {
|
||||
let selectedItems: QueryTreeViewItem[];
|
||||
|
||||
beforeEach(async () => {
|
||||
selectedItems = [
|
||||
createQueryTreeFileItem(
|
||||
"example.ql",
|
||||
join(queriesDir.name, "queries-java", "example.ql"),
|
||||
"java",
|
||||
),
|
||||
createQueryTreeFolderItem(
|
||||
"folder",
|
||||
join(queriesDir.name, "folder"),
|
||||
[
|
||||
createQueryTreeFileItem(
|
||||
"example.ql",
|
||||
join(queriesDir.name, "folder", "example.ql"),
|
||||
"java",
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
wizard = new SkeletonQueryWizard(
|
||||
mockCli,
|
||||
jest.fn(),
|
||||
credentials,
|
||||
extLogger,
|
||||
mockDatabaseManager,
|
||||
storagePath,
|
||||
selectedItems,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns the first selected item path", async () => {
|
||||
const chosenPath = await wizard.determineStoragePath();
|
||||
|
||||
expect(chosenPath).toEqual(dirname(selectedItems[0].path));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("determineRootStoragePath", () => {
|
||||
it("should prompt the user to provide a storage path", async () => {
|
||||
const chosenPath = await wizard.determineRootStoragePath();
|
||||
|
||||
expect(showInputBoxSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ value: storagePath }),
|
||||
);
|
||||
expect(chosenPath).toEqual(storagePath);
|
||||
});
|
||||
|
||||
it("should write the chosen folder to settings", async () => {
|
||||
const updateValueSpy = jest.spyOn(Setting.prototype, "updateValue");
|
||||
|
||||
await wizard.determineStoragePath();
|
||||
await wizard.determineRootStoragePath();
|
||||
|
||||
expect(updateValueSpy).toHaveBeenCalledWith(storagePath, 2);
|
||||
});
|
||||
@@ -453,7 +600,7 @@ describe("SkeletonQueryWizard", () => {
|
||||
});
|
||||
|
||||
it("should not prompt the user", async () => {
|
||||
const chosenPath = await wizard.determineStoragePath();
|
||||
const chosenPath = await wizard.determineRootStoragePath();
|
||||
|
||||
expect(showInputBoxSpy).not.toHaveBeenCalled();
|
||||
expect(chosenPath).toEqual(storagePath);
|
||||
@@ -484,7 +631,7 @@ describe("SkeletonQueryWizard", () => {
|
||||
});
|
||||
|
||||
it("should return it and not prompt the user", async () => {
|
||||
const chosenPath = await wizard.determineStoragePath();
|
||||
const chosenPath = await wizard.determineRootStoragePath();
|
||||
|
||||
expect(showInputBoxSpy).not.toHaveBeenCalled();
|
||||
expect(chosenPath).toEqual(storedPath);
|
||||
@@ -513,7 +660,7 @@ describe("SkeletonQueryWizard", () => {
|
||||
});
|
||||
|
||||
it("should prompt the user for to provide a new folder name", async () => {
|
||||
const chosenPath = await wizard.determineStoragePath();
|
||||
const chosenPath = await wizard.determineRootStoragePath();
|
||||
|
||||
expect(showInputBoxSpy).toHaveBeenCalled();
|
||||
expect(chosenPath).toEqual(storagePath);
|
||||
|
||||
Reference in New Issue
Block a user