Skip to content
Merged
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
10 changes: 9 additions & 1 deletion extensions/ql-vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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),
Expand Down
9 changes: 9 additions & 0 deletions extensions/ql-vscode/src/local-queries/local-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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),
Expand Down Expand Up @@ -333,6 +341,7 @@ export class LocalQueries extends DisposableObject {
this.app.logger,
this.databaseManager,
contextStoragePath,
this.selectedQueryTreeViewItems,
language,
);
await skeletonQueryWizard.execute();
Expand Down
61 changes: 47 additions & 14 deletions extensions/ql-vscode/src/local-queries/skeleton-query-wizard.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import { join } from "path";
import { Uri, workspace, window as Window } from "vscode";
import { basename, 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";
import { QueryLanguage } from "../common/query-language";
import {
getFirstWorkspaceFolder,
isFolderAlreadyInWorkspace,
} from "../common/vscode/workspace-folders";
import { getFirstWorkspaceFolder } from "../common/vscode/workspace-folders";
import { getErrorMessage } from "../common/helpers-pure";
import { QlPackGenerator } from "./qlpack-generator";
import { DatabaseItem, DatabaseManager } from "../databases/local-databases";
Expand All @@ -24,8 +21,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>;

Expand All @@ -51,6 +49,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,
) {}

Expand All @@ -70,9 +69,9 @@ export class SkeletonQueryWizard {

this.qlPackStoragePath = await this.determineStoragePath();

const skeletonPackAlreadyExists =
existsSync(join(this.qlPackStoragePath, this.folderName)) ||
isFolderAlreadyInWorkspace(this.folderName);
const skeletonPackAlreadyExists = await pathExists(
join(this.qlPackStoragePath, this.folderName),
);

if (skeletonPackAlreadyExists) {
// just create a new example query file in skeleton QL pack
Expand Down Expand Up @@ -109,7 +108,41 @@ export class SkeletonQueryWizard {
});
}

public async determineStoragePath() {
public async determineStoragePath(): Promise<string> {
if (this.selectedItems.length === 0) {
return this.determineRootStoragePath();
}

const storagePath = await this.determineStoragePathFromSelection();

// If the user has selected a folder or file within a folder that matches the current
// folder name, we should create a query rather than a query pack
if (basename(storagePath) === this.folderName) {
return dirname(storagePath);
}

return storagePath;
}

private async determineStoragePathFromSelection(): Promise<string> {
// 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()) {
Expand All @@ -118,7 +151,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.",
Expand All @@ -131,7 +164,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.",
);
Expand Down Expand Up @@ -208,7 +241,7 @@ export class SkeletonQueryWizard {
await qlPackGenerator.createExampleQlFile(this.fileName);
} catch (e: unknown) {
void this.logger.log(
`Could not create skeleton QL pack: ${getErrorMessage(e)}`,
`Could not create query example file: ${getErrorMessage(e)}`,
);
}
}
Expand Down
12 changes: 12 additions & 0 deletions extensions/ql-vscode/src/queries-panel/queries-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
}
}
15 changes: 14 additions & 1 deletion extensions/ql-vscode/src/queries-panel/queries-panel.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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,
Expand All @@ -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
Expand Down
Loading