|
| 1 | +import * as fs from 'fs-extra'; |
| 2 | +import * as glob from 'glob-promise'; |
| 3 | +import * as yaml from 'js-yaml'; |
| 4 | +import * as path from 'path'; |
| 5 | +import { ExtensionContext, window as Window, workspace, Uri } from 'vscode'; |
| 6 | +import { ErrorCodes, ResponseError } from 'vscode-languageclient'; |
| 7 | +import { CodeQLCliServer } from './cli'; |
| 8 | +import { DatabaseUI } from './databases-ui'; |
| 9 | +import * as helpers from './helpers'; |
| 10 | +import { logger } from './logging'; |
| 11 | +import { UserCancellationException } from './queries'; |
| 12 | + |
| 13 | +const QUICK_QUERIES_DIR_NAME = 'quick-queries'; |
| 14 | +const QUICK_QUERY_QUERY_NAME = 'quick-query.ql'; |
| 15 | + |
| 16 | +async function getQlPackFor(cliServer: CodeQLCliServer, dbschemePath: string): Promise<string> { |
| 17 | + const qlpacks = await cliServer.resolveQlpacks(helpers.getOnDiskWorkspaceFolders()); |
| 18 | + const packs: { packDir: string | undefined, packName: string }[] = |
| 19 | + Object.entries(qlpacks).map(([packName, dirs]) => { |
| 20 | + if (dirs.length < 1) { |
| 21 | + logger.log(`In getQlPackFor ${dbschemePath}, qlpack ${packName} has no directories`); |
| 22 | + return { packName, packDir: undefined }; |
| 23 | + } |
| 24 | + if (dirs.length > 1) { |
| 25 | + logger.log(`In getQlPackFor ${dbschemePath}, qlpack ${packName} has more than one directory; arbitrarily choosing the first`); |
| 26 | + } |
| 27 | + return { |
| 28 | + packName, |
| 29 | + packDir: dirs[0] |
| 30 | + } |
| 31 | + }); |
| 32 | + for (const { packDir, packName } of packs) { |
| 33 | + if (packDir !== undefined) { |
| 34 | + const qlpack = yaml.safeLoad(await fs.readFile(path.join(packDir, 'qlpack.yml'), 'utf8')); |
| 35 | + if (qlpack.dbscheme !== undefined && path.basename(qlpack.dbscheme) === path.basename(dbschemePath)) { |
| 36 | + return packName; |
| 37 | + } |
| 38 | + } |
| 39 | + } |
| 40 | + throw new Error(`Could not find qlpack file for dbscheme ${dbschemePath}`); |
| 41 | +} |
| 42 | + |
| 43 | +/** |
| 44 | + * `getBaseText` heuristically returns an appropriate import |
| 45 | + * statement prelude based on the filename of the dbscheme file |
| 46 | + * given. This information might be more appropriately provided by |
| 47 | + * the qlpack itself. |
| 48 | + */ |
| 49 | +function getBaseText(dbschemeBase: string) { |
| 50 | + if (dbschemeBase == 'semmlecode.javascript.dbscheme') return 'import javascript\n\nselect ""'; |
| 51 | + if (dbschemeBase == 'semmlecode.cpp.dbscheme') return 'import cpp\n\nselect ""'; |
| 52 | + if (dbschemeBase == 'semmlecode.java.dbscheme') return 'import java\n\nselect ""'; |
| 53 | + if (dbschemeBase == 'semmlecode.python.dbscheme') return 'import python\n\nselect ""'; |
| 54 | + return ''; |
| 55 | +} |
| 56 | + |
| 57 | +async function getQuickQueriesDir(ctx: ExtensionContext): Promise<string> { |
| 58 | + const storagePath = ctx.storagePath; |
| 59 | + if (storagePath === undefined) { |
| 60 | + throw new Error('Workspace storage path is undefined'); |
| 61 | + } |
| 62 | + const queriesPath = path.join(storagePath, QUICK_QUERIES_DIR_NAME); |
| 63 | + fs.ensureDir(queriesPath, { mode: 0o700 }); |
| 64 | + return queriesPath; |
| 65 | +} |
| 66 | + |
| 67 | +/** |
| 68 | + * Show a buffer the user can enter a simple query into. |
| 69 | + */ |
| 70 | +export async function displayQuickQuery(ctx: ExtensionContext, cliServer: CodeQLCliServer, databaseUI: DatabaseUI) { |
| 71 | + try { |
| 72 | + |
| 73 | + // If there is already a quick query open, don't clobber it, just |
| 74 | + // show it. |
| 75 | + const existing = workspace.textDocuments.find(doc => path.basename(doc.uri.fsPath) === QUICK_QUERY_QUERY_NAME); |
| 76 | + if (existing !== undefined) { |
| 77 | + Window.showTextDocument(existing); |
| 78 | + return; |
| 79 | + } |
| 80 | + |
| 81 | + const queriesDir = await getQuickQueriesDir(ctx); |
| 82 | + |
| 83 | + // We need this folder in workspace folders so the language server |
| 84 | + // knows how to find its qlpack.yml |
| 85 | + if (workspace.workspaceFolders === undefined |
| 86 | + || !workspace.workspaceFolders.some(folder => folder.uri.fsPath === queriesDir)) { |
| 87 | + workspace.updateWorkspaceFolders( |
| 88 | + (workspace.workspaceFolders || []).length, |
| 89 | + 0, |
| 90 | + { uri: Uri.file(queriesDir), name: "Quick Queries" } |
| 91 | + ); |
| 92 | + } |
| 93 | + |
| 94 | + // We're going to infer which qlpack to use from the current database |
| 95 | + const dbItem = await databaseUI.getDatabaseItem(); |
| 96 | + if (dbItem === undefined) { |
| 97 | + throw new Error('Can\'t start quick query without a selected database'); |
| 98 | + } |
| 99 | + |
| 100 | + const datasetFolder = await dbItem.getDatasetFolder(cliServer); |
| 101 | + const dbschemes = await glob(path.join(datasetFolder, '*.dbscheme')) |
| 102 | + |
| 103 | + if (dbschemes.length < 1) { |
| 104 | + throw new Error(`Can't find dbscheme for current database in ${datasetFolder}`); |
| 105 | + } |
| 106 | + |
| 107 | + const dbscheme = dbschemes[0]; |
| 108 | + if (dbschemes.length > 1) { |
| 109 | + logger.log(`Found multiple dbschemes in ${datasetFolder}; arbitrarily choosing the first, ${dbscheme}`); |
| 110 | + } |
| 111 | + |
| 112 | + const qlpack = await getQlPackFor(cliServer, dbscheme); |
| 113 | + const quickQueryQlpackYaml: any = { |
| 114 | + name: "quick-query", |
| 115 | + version: "1.0.0", |
| 116 | + libraryPathDependencies: [qlpack] |
| 117 | + }; |
| 118 | + |
| 119 | + const qlFile = path.join(queriesDir, QUICK_QUERY_QUERY_NAME); |
| 120 | + const qlPackFile = path.join(queriesDir, 'qlpack.yml'); |
| 121 | + await fs.writeFile(qlFile, getBaseText(path.basename(dbscheme)), 'utf8'); |
| 122 | + await fs.writeFile(qlPackFile, yaml.safeDump(quickQueryQlpackYaml), 'utf8'); |
| 123 | + Window.showTextDocument(await workspace.openTextDocument(qlFile)); |
| 124 | + } |
| 125 | + |
| 126 | + // TODO: clean up error handling for top-level commands like this |
| 127 | + catch (e) { |
| 128 | + if (e instanceof UserCancellationException) { |
| 129 | + logger.log(e.message); |
| 130 | + } |
| 131 | + else if (e instanceof ResponseError && e.code == ErrorCodes.RequestCancelled) { |
| 132 | + logger.log(e.message); |
| 133 | + } |
| 134 | + else if (e instanceof Error) |
| 135 | + helpers.showAndLogErrorMessage(e.message); |
| 136 | + else |
| 137 | + throw e; |
| 138 | + } |
| 139 | +} |
0 commit comments