From 29756e3f97d43cb94ce8d8f3efebd4f97aeab96a Mon Sep 17 00:00:00 2001 From: Josh Johnson Date: Sat, 28 Apr 2018 20:36:58 -0400 Subject: [PATCH] Add XQuery Execution --- package.json | 12 ++ src/constants.ts | 6 + src/extension.ts | 6 + src/xquery-execution/child-process.ts | 30 ++++ .../commands/executeXQuery.ts | 132 ++++++++++++++++++ 5 files changed, 186 insertions(+) create mode 100644 src/xquery-execution/child-process.ts create mode 100644 src/xquery-execution/commands/executeXQuery.ts diff --git a/package.json b/package.json index 31b0fbd..efade7d 100644 --- a/package.json +++ b/package.json @@ -123,6 +123,18 @@ "default": "", "description": "The full path to the executable to run when executing XQuery scripts.", "scope": "resource" + }, + "xmlTools.xqueryExecutionInputLimit": { + "type": "integer", + "default": 100, + "description": "The maximum number of input files to enumerate when executing XQuery scripts.", + "scope": "resource" + }, + "xmlTools.xqueryExecutionInputSearchPattern": { + "type": "string", + "default": "**/*.xml", + "description": "The pattern used to search for input XML files when executing XQuery scripts.", + "scope": "resource" } } }, diff --git a/src/constants.ts b/src/constants.ts index a97ec0b..fd53f46 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -2,6 +2,7 @@ export const extensionPrefix = "xmlTools"; export namespace commands { export const evaluateXPath = "xmlTools.evaluateXPath"; + export const executeXQuery = "xmlTools.executeXQuery"; export const formatAsXml = "xmlTools.formatAsXml"; export const minifyXml = "xmlTools.minifyXml"; } @@ -17,6 +18,10 @@ export namespace configKeys { export const removeCommentsOnMinify = "removeCommentsOnMinify"; export const splitAttributesOnFormat = "splitAttributesOnFormat"; export const splitXmlnsOnFormat = "splitXmlnsOnFormat"; + export const xqueryExecutionArguments = "xqueryExecutionArguments"; + export const xqueryExecutionEngine = "xqueryExecutionEngine"; + export const xqueryExecutionInputLimit = "xqueryExecutionInputLimit"; + export const xqueryExecutionInputSearchPattern = "xqueryExecutionInputSearchPattern"; } export namespace diagnosticCollections { @@ -30,6 +35,7 @@ export namespace languageIds { export namespace nativeCommands { export const cursorMove = "cursorMove"; + export const openGlobalSettings = "workbench.action.openGlobalSettings"; export const revealLine = "revealLine"; export const setContext = "setContext"; } diff --git a/src/extension.ts b/src/extension.ts index 54c1dc7..8fcd255 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -9,6 +9,7 @@ import { XmlFormattingEditProvider } from "./formatting/xml-formatting-edit-prov import { XQueryLinter } from "./linting/xquery-linter"; import { XmlTreeDataProvider } from "./tree-view/xml-tree-data-provider"; import { evaluateXPath } from "./xpath/commands/evaluateXPath"; +import { executeXQuery } from "./xquery-execution/commands/executeXQuery"; import * as constants from "./constants"; @@ -50,6 +51,11 @@ export function activate(context: ExtensionContext) { context.subscriptions.push( commands.registerTextEditorCommand(constants.commands.evaluateXPath, evaluateXPath) ); + + /* XQuery Features */ + context.subscriptions.push( + commands.registerTextEditorCommand(constants.commands.executeXQuery, executeXQuery) + ); } export function deactivate() { diff --git a/src/xquery-execution/child-process.ts b/src/xquery-execution/child-process.ts new file mode 100644 index 0000000..161f963 --- /dev/null +++ b/src/xquery-execution/child-process.ts @@ -0,0 +1,30 @@ +const child_process = require("child_process"); + +export class ChildProcess { + static async spawn(executable: string, args: string[]): Promise { + return new Promise((resolve, reject) => { + + let output = ""; + const handle = child_process.spawn(executable, args); + + handle.stdout.on("data", (data: string) => { + output += data; + }); + + handle.stderr.on("data", (data: string) => { + output += data; + }); + + handle.on("close", (code: string) => { + if (code === "0") { + resolve(); + } + + else { + reject({ code: code, message: output }); + } + }); + + }); + } +} diff --git a/src/xquery-execution/commands/executeXQuery.ts b/src/xquery-execution/commands/executeXQuery.ts new file mode 100644 index 0000000..a209e3d --- /dev/null +++ b/src/xquery-execution/commands/executeXQuery.ts @@ -0,0 +1,132 @@ +import { commands, window, workspace } from "vscode"; +import { Disposable, Range, TextEditor, TextEditorEdit, Uri } from "vscode"; + +import * as constants from "../../constants"; + +import { ChildProcess } from "../child-process"; + +export async function executeXQuery(editor: TextEditor, edit: TextEditorEdit): Promise { + const config = workspace.getConfiguration(constants.extensionPrefix); + + // this disposable will be used for creating status bar messages + let disposable: Disposable; + + if (editor.document.languageId !== constants.languageIds.xquery) { + window.showErrorMessage("This action can only be performed on an XQuery file."); + return; + } + + const executable = config.get(constants.configKeys.xqueryExecutionEngine, null); + let args = config.get(constants.configKeys.xqueryExecutionArguments, []); + + if (!executable || executable === "") { + const action = await window.showWarningMessage("An XQuery execution engine has not been defined.", "Define Now"); + + if (action === "Define Now") { + commands.executeCommand(constants.nativeCommands.openGlobalSettings); + } + + return; + } + + let inputFile: Uri; + disposable = window.setStatusBarMessage("Searching for XML files in folder..."); + + const searchPattern = config.get(constants.configKeys.xqueryExecutionInputSearchPattern); + const inputLimit = config.get(constants.configKeys.xqueryExecutionInputLimit); + + const files = await workspace.findFiles(searchPattern, "", inputLimit); + + disposable.dispose(); + + // user does not have a folder open - prompt for file name + if (typeof files === "undefined") { + window.showErrorMessage("You must have a folder opened in VS Code to use this feature."); + return; + } + + // if there is only one XML file, default it + // otherwise, prompt the user to select one from the open folder + if (files.length > 1) { + const qpItems = new Array(); + + files.forEach((file) => { + const filename = file.fsPath.replace("\\", "/"); + + qpItems.push({ // must implement vscode.QuickPickItem + label: filename.substring(filename.lastIndexOf("/") + 1), + description: file.fsPath, + file: file + }); + }); + + const selection = await window.showQuickPick(qpItems, { placeHolder: "Please select an input file." }); + + if (!selection) { + return; + } + + inputFile = selection.file; + } + + else { + inputFile = files[0]; + } + + // prompt for output file name + let outputPath: string = null; + let outputPathPos = -1; + + for (let i = 0; i < args.length; i++) { + if (i > 0) { + if (args[i - 1].search(/out|result/)) { + outputPath = args[i]; + outputPathPos = i; + } + } + } + + if (outputPath) { + outputPath = await window.showInputBox({ + placeHolder: "ex. C:\\TEMP\XQueryOutput\\MyOutputFile.xml", + prompt: "Please specify the output file path. Existing file behavior is determined by the execution engine you have specified.", + value: outputPath + }); + + args[outputPathPos] = outputPath; + } + + // call out to the execution engine + disposable = window.setStatusBarMessage("Executing XQuery Script..."); + args = args.map((value: string) => { + return value + .replace("$(script)", editor.document.uri.fsPath) + .replace("$(input)", inputFile.fsPath) + .replace("$(project)", workspace.rootPath); + }); + + try { + await ChildProcess.spawn(executable, args); + } + + catch (error) { + if (error.message.search(/[Ll]ine:?\s*\d+/gm) > -1) { + const match: RegExpExecArray = /[Ll]ine:?\s*\d+/gm.exec(error.message); + const line: number = (Number.parseInt(match[0].replace(/([Ll]ine:?\s*)|\s/, "")) - 1); + + const selection: string = await window.showErrorMessage(error.message, `Go to Line ${line}`); + + if (selection === `Go to Line ${line}`) { + editor.revealRange(new Range(line, 0, line, 0)); + } + } + + else { + window.showErrorMessage(error.message); + } + } + + finally { + disposable.dispose(); + } +}