From 95f96a2181d3a90bb6e83f883c87223a7fc38d3c Mon Sep 17 00:00:00 2001 From: jreyno77 Date: Thu, 29 Aug 2024 10:40:16 -0600 Subject: [PATCH 1/6] Added typedocs dependency and very minimal docs to begin documentation --- .gitignore | 1 + package-lock.json | 173 +++++++++++++++++++++++++++++++++++ package.json | 1 + src/buildParameters.ts | 43 +++++++-- src/connectionManager.ts | 51 ++++++++++- src/cqlLanguageClient.ts | 65 +++++++------ src/executeCql.ts | 26 +++++- src/extension.ts | 114 +++++++++++++++++++++-- src/findJavaRuntimes.ts | 63 ++++++++++++- src/javaServiceInstaller.ts | 76 ++++++++++++++- src/languageServerStarter.ts | 34 ++++++- src/normalizeCqlExecution.ts | 13 ++- tsconfig.json | 2 +- 13 files changed, 592 insertions(+), 70 deletions(-) diff --git a/.gitignore b/.gitignore index bb976e0..2d38387 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ jars/ bin .vscode-test coverage/ +docs diff --git a/package-lock.json b/package-lock.json index 272a974..1c26b20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "c8": "^10.1.2", "prettier": "^3.3.3", "sinon": "^18.0.0", + "typedoc": "^0.26.6", "typescript": "^5.3.2" }, "engines": { @@ -209,6 +210,15 @@ "node": ">=14" } }, + "node_modules/@shikijs/core": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.14.1.tgz", + "integrity": "sha512-KyHIIpKNaT20FtFPFjCQB5WVSTpLR/n+jQXhWHWVUMm9MaOaG9BGOG0MSyt7yA4+Lm+4c9rTc03tt3nYzeYSfw==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.4" + } + }, "node_modules/@sinonjs/commons": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", @@ -1024,6 +1034,19 @@ "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/escalade": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", @@ -1545,6 +1568,16 @@ "immediate": "~3.0.5" } }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2073,6 +2106,13 @@ "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", "dev": true }, + "node_modules/path-to-regexp": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", + "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", + "dev": true, + "license": "MIT" + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -2106,6 +2146,16 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -2254,6 +2304,17 @@ "node": ">=8" } }, + "node_modules/shiki": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.14.1.tgz", + "integrity": "sha512-FujAN40NEejeXdzPt+3sZ3F2dx1U24BY2XTY01+MG8mbxCiA2XukXdcbyMyLAHJ/1AUUnQd1tZlvIjefWWEJeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "1.14.1", + "@types/hast": "^3.0.4" + } + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -2521,6 +2582,55 @@ "node": ">=4" } }, + "node_modules/typedoc": { + "version": "0.26.6", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.26.6.tgz", + "integrity": "sha512-SfEU3SH3wHNaxhFPjaZE2kNl/NFtLNW5c1oHsg7mti7GjmUj1Roq6osBQeMd+F4kL0BoRBBr8gQAuqBlfFu8LA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lunr": "^2.3.9", + "markdown-it": "^14.1.0", + "minimatch": "^9.0.5", + "shiki": "^1.9.1", + "yaml": "^2.4.5" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x" + } + }, + "node_modules/typedoc/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/typedoc/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/typescript": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", @@ -2534,6 +2644,13 @@ "node": ">=14.17" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, "node_modules/undici-types": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz", @@ -2773,6 +2890,19 @@ "node": ">=10" } }, + "node_modules/yaml": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", + "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", @@ -3597,6 +3727,12 @@ "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true + }, "escalade": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", @@ -3977,6 +4113,15 @@ "immediate": "~3.0.5" } }, + "linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "requires": { + "uc.micro": "^2.0.0" + } + }, "locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4371,6 +4516,12 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, + "punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -4482,6 +4633,16 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true }, + "shiki": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.14.1.tgz", + "integrity": "sha512-FujAN40NEejeXdzPt+3sZ3F2dx1U24BY2XTY01+MG8mbxCiA2XukXdcbyMyLAHJ/1AUUnQd1tZlvIjefWWEJeA==", + "dev": true, + "requires": { + "@shikijs/core": "1.14.1", + "@types/hast": "^3.0.4" + } + }, "signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -4690,6 +4851,12 @@ "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true }, + "uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true + }, "undici-types": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz", @@ -4880,6 +5047,12 @@ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true }, + "yaml": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", + "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==", + "dev": true + }, "yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", diff --git a/package.json b/package.json index 028699c..ad58a7d 100644 --- a/package.json +++ b/package.json @@ -184,6 +184,7 @@ "c8": "^10.1.2", "prettier": "^3.3.3", "sinon": "^18.0.0", + "typedoc": "^0.26.6", "typescript": "^5.3.2" }, "dependencies": { diff --git a/src/buildParameters.ts b/src/buildParameters.ts index a9d63d0..caf5d15 100644 --- a/src/buildParameters.ts +++ b/src/buildParameters.ts @@ -1,10 +1,9 @@ -import { glob } from 'glob'; -import { window, workspace } from 'vscode'; -import { URI, Utils } from 'vscode-uri'; - import * as fs from 'fs'; import * as fse from 'fs-extra'; +import { glob } from 'glob'; import path from 'path'; +import { window, workspace } from 'vscode'; +import { URI, Utils } from 'vscode-uri'; import { Connection, ConnectionManager, Context } from './connectionManager'; export type EvaluationParameters = { @@ -13,7 +12,12 @@ export type EvaluationParameters = { testPath: URI | undefined; }; -// Should be working with normalized data +/** + * Builds the parameters required for CQL evaluation. + * @param {URI} uri - The URI of the CQL file. + * @param {string | undefined} expression - The specific CQL expression to evaluate, if any. + * @returns {EvaluationParameters} The parameters required for the CQL evaluation. + */ export function buildParameters(uri: URI, expression: string | undefined): EvaluationParameters { if (!fs.existsSync(uri.fsPath)) { window.showInformationMessage('No library content found. Please save before executing.'); @@ -56,8 +60,6 @@ export function buildParameters(uri: URI, expression: string | undefined): Evalu expression, terminologyPath, connection, - // I kind of want to make 'Local' a const that I can share.... - // not sure, but I already ran into an issue debugging when I changed the check below, but not this one contexts: contexts != undefined && connection?.name !== 'Local' && Object.values(contexts).length > 0 ? new Map(Object.entries(contexts).map(([key, context]) => [key, context])) @@ -73,6 +75,20 @@ export function buildParameters(uri: URI, expression: string | undefined): Evalu return evaluationParams; } +/** + * Generates the command-line arguments required for CQL evaluation. + * @param {object} params - The parameters needed to construct the command-line arguments. + * @param {string} params.fhirVersion - The FHIR version being used. + * @param {URI} params.optionsPath - The path to the CQL options file. + * @param {URI} params.libraryDirectory - The directory of the CQL library. + * @param {string} params.libraryName - The name of the CQL library. + * @param {string | undefined} params.expression - The specific CQL expression to evaluate, if any. + * @param {URI} params.terminologyPath - The path to the FHIR terminology. + * @param {Connection | undefined} params.connection - The current connection information. + * @param {Map} params.contexts - The execution contexts for the evaluation. + * @param {string} params.measurementPeriod - The measurement period to be used. + * @returns {string[]} The command-line arguments for the CQL evaluation. + */ function getCqlCommandArgs({ fhirVersion, optionsPath, @@ -117,7 +133,6 @@ function getCqlCommandArgs({ } if (connection?.name === 'Local') { - // connection.endpoint = projectPath.toString(); Can't use projectPath because Evaluator is not ok with cql-option.json and other files that are not fhir resources. modelPath = key; } if (modelPath) { @@ -137,6 +152,12 @@ function getCqlCommandArgs({ return args; } +/** + * Retrieves the local contexts for CQL evaluation based on the test path. + * @param {URI} testPath - The URI of the test directory. + * @param {string} libraryName - The name of the CQL library. + * @returns {Map} A map of local contexts for CQL evaluation. + */ function getLocalContexts(testPath: URI, libraryName: string): Map { let testCases: Map = new Map(); if (!fs.existsSync(testPath.fsPath)) { @@ -148,17 +169,19 @@ function getLocalContexts(testPath: URI, libraryName: string): Map fs.statSync(path.join(dir, d)).isDirectory()); for (let c of cases) { - // Should really be reading in Patient Resources and getting the ids and everything should be based on a repository like in the evaluator testCases.set(URI.file(path.join(dir, c)).toString(), { resourceType: 'Patient', resourceID: c, }); - // path: URI.file(path.join(dir, c)) For Patient specific directory } } return testCases; } +/** + * Determines the FHIR version used in the active text editor. + * @returns {string} The FHIR version (e.g., 'R4'). + */ function getFhirVersion(): string { const fhirVersionRegex = /using (FHIR|"FHIR") version '(\d(.|\d)*)'/; const matches = window.activeTextEditor?.document.getText().match(fhirVersionRegex); diff --git a/src/connectionManager.ts b/src/connectionManager.ts index ce9111d..a89646e 100644 --- a/src/connectionManager.ts +++ b/src/connectionManager.ts @@ -13,6 +13,9 @@ export interface Connection { contexts: Record; } +/** + * Manages connections and their associated contexts. + */ export class ConnectionManager { private static connectionManager: ConnectionManager; private connections: Record; @@ -23,11 +26,19 @@ export class ConnectionManager { this.connections = {}; } - public static getManager() { + /** + * Retrieves the singleton instance of the ConnectionManager. + * @returns {ConnectionManager} The instance of the ConnectionManager. + */ + public static getManager(): ConnectionManager { return ConnectionManager.connectionManager; } - public static _initialize(ec: ExtensionContext) { + /** + * Initializes the ConnectionManager with the given extension context. + * @param {ExtensionContext} ec - The extension context used to initialize the ConnectionManager. + */ + public static _initialize(ec: ExtensionContext): void { if (ConnectionManager.connectionManager) { return; } @@ -48,15 +59,27 @@ export class ConnectionManager { } } + /** + * Retrieves all connections managed by the ConnectionManager. + * @returns {Record} A record of all connections. + */ public getAllConnections(): Record { return this.connections; } + /** + * Retrieves the current active connection. + * @returns {Connection | undefined} The current connection, or undefined if none is set. + */ public getCurrentConnection(): Connection | undefined { return this.currentConnection; } - public setCurrentConnection(name: string | undefined) { + /** + * Sets the current active connection by its name. + * @param {string | undefined} name - The name of the connection to set as current. If undefined, unsets the current connection. + */ + public setCurrentConnection(name: string | undefined): void { if (name === undefined) { this.currentConnection = undefined; } else { @@ -64,23 +87,45 @@ export class ConnectionManager { } } + /** + * Adds or updates a connection in the ConnectionManager. + * @param {Connection} connection - The connection to add or update. + */ public upsertConnection(connection: Connection): void { this.connections[connection.name] = connection; } + /** + * Deletes a connection from the ConnectionManager by its name. + * @param {string} name - The name of the connection to delete. + */ public deleteConnection(name: string): void { delete this.connections[name]; } + /** + * Retrieves the contexts of the current active connection. + * @returns {Record | undefined} A record of contexts, or undefined if no connection is active. + */ public getCurrentContexts(): Record | undefined { return this.getCurrentConnection()?.contexts; } + /** + * Adds or updates a context within a specific connection. + * @param {string} connectionName - The name of the connection to which the context belongs. + * @param {Context} context - The context to add or update. + */ public upsertContext(connectionName: string, context: Context): void { this.connections[connectionName].contexts[context.resourceType + '/' + context.resourceID] = context; } + /** + * Deletes a context from a specific connection. + * @param {string} connectionName - The name of the connection from which to delete the context. + * @param {string} contextID - The ID of the context to delete. + */ public deleteContext(connectionName: string, contextID: string): void { delete this.connections[connectionName].contexts[contextID]; } diff --git a/src/cqlLanguageClient.ts b/src/cqlLanguageClient.ts index 0dc138b..2ed9ff5 100644 --- a/src/cqlLanguageClient.ts +++ b/src/cqlLanguageClient.ts @@ -21,10 +21,21 @@ import { statusBar } from './statusBar'; const extensionName = 'Language Support for CQL'; +/** + * Represents a CQL language client, responsible for managing the interaction between the extension and the CQL language server. + */ export class CqlLanguageClient { private languageClient: LanguageClient | undefined; private status: ClientStatus = ClientStatus.Uninitialized; + /** + * Initializes the CQL language client. + * @param {ExtensionContext} context - The VS Code extension context. + * @param {RequirementsData} requirements - The requirements for starting the language server. + * @param {LanguageClientOptions} clientOptions - The options for configuring the language client. + * @param {string} workspacePath - The path of the workspace in which the client operates. + * @returns {Promise} A promise that resolves when initialization is complete. + */ public async initialize( context: ExtensionContext, requirements: RequirementsData, @@ -36,30 +47,10 @@ export class CqlLanguageClient { } const serverOptions = prepareExecutable(requirements, context, workspacePath); - // Create the language client and start the client. this.languageClient = new LanguageClient('cql', extensionName, serverOptions, clientOptions); this.languageClient.onReady().then(() => { this.status = ClientStatus.Started; - // this.languageClient.onNotification(StatusNotification.type, (report) => { - // switch (report.type) { - // case 'ServiceReady': - // break; - // case 'Started': - // this.status = ClientStatus.Started; - // statusBar.setReady(); - // break; - // case 'Error': - // this.status = ClientStatus.Error; - // statusBar.setError(); - // break; - // case 'Starting': - // case 'Message': - // // message goes to progress report instead - // break; - // } - // statusBar.updateTooltip(report.message); - // }); this.languageClient!.onNotification(ProgressReportNotification.type, _progress => { // TODO: Support for long-running tasks @@ -110,7 +101,6 @@ export class CqlLanguageClient { return null; }); - // TODO: Set this once we have the initialization signal from the LS. statusBar.setReady(); }); @@ -118,6 +108,10 @@ export class CqlLanguageClient { this.status = ClientStatus.Initialized; } + /** + * Registers commands for the extension. + * @param {ExtensionContext} context - The VS Code extension context. + */ private registerCommands(context: ExtensionContext): void { context.subscriptions.push( commands.registerCommand(Commands.OPEN_OUTPUT, () => @@ -146,12 +140,6 @@ export class CqlLanguageClient { }), ); - // I'm not sure if the language server communicates at the start of the command, or only when we pass the arguments at the end. - // If we pass some information at the time of the command like the position of the cursor and return something, - // it would be better to just return something like a ExpressionDefinition in json. - // For now I'm assuming a position is sent from the client first and we need to use that to pass the cql evaluate command to the server - // Could try something like https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_selectionRange after grabbing the start and end positions of a selection - // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_signatureHelp *Definition Signature???* context.subscriptions.push( commands.registerCommand(Commands.EXECUTE_CQL_EXPRESSION_COMMAND, async (uri: Uri) => { await normalizeCqlExecution(uri, 'expression'); @@ -159,6 +147,9 @@ export class CqlLanguageClient { ); } + /** + * Starts the CQL language client. + */ public start(): void { if (this.languageClient && this.status === ClientStatus.Initialized) { this.languageClient.start(); @@ -166,23 +157,39 @@ export class CqlLanguageClient { } } - public stop() { + /** + * Stops the CQL language client. + */ + public stop(): void { if (this.languageClient) { this.languageClient.stop(); this.status = ClientStatus.Stopping; } } + /** + * Gets the language client instance. + * @returns {LanguageClient} The language client instance. + */ public getClient(): LanguageClient { return this.languageClient!; } + /** + * Gets the current status of the language client. + * @returns {ClientStatus} The current status of the language client. + */ public getClientStatus(): ClientStatus { return this.status; } } -function logNotification(message: string) { +/** + * Logs a notification message. + * @param {string} message - The message to log. + * @returns {Promise} A promise that resolves when the log operation is complete. + */ +function logNotification(message: string): Promise { return new Promise(() => { logger.verbose(message); }); diff --git a/src/executeCql.ts b/src/executeCql.ts index 88ddf5d..1ebbc42 100644 --- a/src/executeCql.ts +++ b/src/executeCql.ts @@ -1,9 +1,13 @@ -import { Position, TextEditor, Uri, commands, window, workspace } from 'vscode'; -import { Commands } from './commands'; - import * as fs from 'fs'; +import { Position, TextEditor, Uri, commands, window, workspace } from 'vscode'; import { EvaluationParameters } from './buildParameters'; +import { Commands } from './commands'; +/** + * Inserts a line of text at the end of the document in the given text editor. + * @param {TextEditor} textEditor - The text editor where the text should be inserted. + * @param {string} text - The text to insert. + */ async function insertLineAtEnd(textEditor: TextEditor, text: string) { const document = textEditor.document; await textEditor.edit(editBuilder => { @@ -11,12 +15,18 @@ async function insertLineAtEnd(textEditor: TextEditor, text: string) { }); } +/** + * Executes CQL (Clinical Quality Language) based on the provided evaluation parameters. + * Outputs results to a text document specified by the evaluation parameters. + * @param {EvaluationParameters} evaluationParams - The parameters required for executing CQL. + */ export async function executeCQL({ operationArgs, testPath, outputPath }: EvaluationParameters) { let cqlMessage = ''; let terminologyMessage = ''; let testMessage = `Test cases:\n`; let foundTest = false; const contextValues: string[] = []; + for (let i = 0; i < operationArgs?.length!; i++) { if (operationArgs![i].startsWith('-lu=')) { cqlMessage = `CQL: ${operationArgs![i].substring(4)}`; @@ -64,8 +74,14 @@ export async function executeCQL({ operationArgs, testPath, outputPath }: Evalua ); } -// Attempt to interleave test case names with result expressions, for better readability. -// Returns `result` unmodified if unable to interleave. +/** + * Attempts to interleave test case names with result expressions for better readability. + * Returns the result unmodified if unable to interleave. + * @param {string | undefined} expression - The CQL expression being evaluated. + * @param {string[]} contextValues - The context values to interleave with the results. + * @param {string} result - The result string to be interleaved. + * @returns {string} The interleaved result string, or the original result if interleaving fails. + */ const attemptInterleave = ( expression: string | undefined, contextValues: string[], diff --git a/src/extension.ts b/src/extension.ts index 9835a2d..eabf0b4 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -28,13 +28,27 @@ const cqlLanguageClient: CqlLanguageClient = new CqlLanguageClient(); const extensionName = 'Language Support for CQL'; let clientLogFile: string; +/** + * Handles errors encountered by the language client and manages client restarts. + */ export class ClientErrorHandler implements ErrorHandler { private restarts: number[]; + /** + * Creates an instance of ClientErrorHandler. + * @param {string} name - The name of the server. + */ constructor(private name: string) { this.restarts = []; } + /** + * Handles errors encountered by the client. + * @param {Error} _error - The error object. + * @param {Message} _message - The error message. + * @param {number} count - The error count. + * @returns {ErrorAction} - The action to be taken. + */ public error(_error: Error, _message: Message, count: number): ErrorAction { if (count && count <= 3) { logger.error( @@ -51,6 +65,10 @@ export class ClientErrorHandler implements ErrorHandler { return ErrorAction.Shutdown; } + /** + * Handles client closure events and determines whether to restart the client. + * @returns {CloseAction} - The action to be taken on closure. + */ public closed(): CloseAction { this.restarts.push(Date.now()); if (this.restarts.length < 5) { @@ -77,46 +95,84 @@ export class ClientErrorHandler implements ErrorHandler { } } +/** + * A wrapper around the VS Code OutputChannel that logs messages to a file. + */ export class OutputInfoCollector implements OutputChannel { private channel: OutputChannel; + /** + * Creates an instance of OutputInfoCollector. + * @param {string} name - The name of the output channel. + */ constructor(public name: string) { this.channel = window.createOutputChannel(this.name); } + + /** + * Replaces the current content of the output channel with the given value. + * @param {string} value - The string to replace the content with. + */ replace(value: string): void { this.clear(); this.append(value); } + /** + * Appends the given string to the output channel. + * @param {string} value - The string to append. + */ append(value: string): void { logger.info(value); this.channel.append(value); } + /** + * Appends the given string as a new line to the output channel. + * @param {string} value - The string to append as a new line. + */ appendLine(value: string): void { logger.info(value); this.channel.appendLine(value); } + /** + * Clears the content of the output channel. + */ clear(): void { this.channel.clear(); } + /** + * Shows the output channel. + * @param {boolean} [preserveFocus] - Whether to preserve focus on the current editor. + */ show(preserveFocus?: boolean): void; show(column?: ViewColumn, preserveFocus?: boolean): void; show(_column?: any, preserveFocus?: any) { this.channel.show(preserveFocus); } + /** + * Hides the output channel. + */ hide(): void { this.channel.hide(); } + /** + * Disposes of the output channel. + */ dispose(): void { this.channel.dispose(); } } +/** + * Activates the extension and initializes the language client and related services. + * @param {ExtensionContext} context - The VS Code extension context. + * @returns {Promise} A promise that resolves when the activation is complete. + */ export function activate(context: ExtensionContext): Promise { let storagePath = context.storagePath; if (!storagePath) { @@ -128,14 +184,13 @@ export function activate(context: ExtensionContext): Promise { return requirements .resolveRequirements(context) .catch(error => { - // show error + // Show error message if requirements resolution fails window.showErrorMessage(error.message, error.label).then(selection => { if (error.label && error.label === selection && error.command) { commands.executeCommand(error.command, error.commandParam); } }); - // rethrow to disrupt the chain. - throw error; + throw error; // rethrow to disrupt the chain. }) .then(async requirements => { return new Promise(async resolve => { @@ -181,15 +236,15 @@ export function activate(context: ExtensionContext): Promise { }), ); - // Connection Manager + // Initialize the Connection Manager ConnectionManager._initialize(context); - // Views + // Initialize the views and panels const connectionsProvider = new ConnectionsViewProvider(context.extensionUri); ConnectionsViewProvider.setContext(context); ConnectionPanel.setContext(context); - // Register commands here to make it available even when the language client fails + // Register commands here to make them available even when the language client fails context.subscriptions.push( commands.registerCommand(Commands.OPEN_SERVER_LOG, (column: ViewColumn) => openServerLogFile(workspacePath, column), @@ -224,6 +279,13 @@ export function activate(context: ExtensionContext): Promise { }); } +/** + * Starts the CQL language server. + * @param {ExtensionContext} context - The VS Code extension context. + * @param {requirements.RequirementsData} requirements - The requirements data for starting the server. + * @param {LanguageClientOptions} clientOptions - The language client options. + * @param {string} workspacePath - The path to the workspace directory. + */ async function startServer( context: ExtensionContext, requirements: requirements.RequirementsData, @@ -239,10 +301,19 @@ async function startServer( statusBar.showStatusBar(); } +/** + * Deactivates the extension and stops the language client. + */ export function deactivate(): void { cqlLanguageClient.stop(); } +/** + * Opens the CQL Language Server log file in the VS Code editor. + * @param {string} workspacePath - The path to the workspace directory. + * @param {ViewColumn} [column=ViewColumn.Active] - The view column in which to open the log file. + * @returns {Thenable} A promise that resolves to true if the log file was opened successfully. + */ function openServerLogFile( workspacePath: string, column: ViewColumn = ViewColumn.Active, @@ -251,6 +322,12 @@ function openServerLogFile( return openLogFile(serverLogFile, 'Could not open CQL Language Server log file', column); } +/** + * Opens the client log file in the VS Code editor. + * @param {string} logFile - The path to the client log file. + * @param {ViewColumn} [column=ViewColumn.Active] - The view column in which to open the log file. + * @returns {Thenable} A promise that resolves to true if the log file was opened successfully. + */ function openClientLogFile( logFile: string, column: ViewColumn = ViewColumn.Active, @@ -259,7 +336,7 @@ function openClientLogFile( const filename = path.basename(logFile); const dirname = path.dirname(logFile); - // find out the newest one + // Find out the newest log file glob(filename + '.*', { cwd: dirname }, (err, files) => { if (!err && files.length > 0) { files.sort(); @@ -273,11 +350,21 @@ function openClientLogFile( }); } +/** + * Opens both the client and server log files in the VS Code editor. + */ async function openLogs() { await commands.executeCommand(Commands.OPEN_CLIENT_LOG, ViewColumn.One); await commands.executeCommand(Commands.OPEN_SERVER_LOG, ViewColumn.Two); } +/** + * Opens a log file in the VS Code editor. + * @param {string} logFile - The path to the log file. + * @param {string} openingFailureWarning - The warning message to show if the log file cannot be opened. + * @param {ViewColumn} [column=ViewColumn.Active] - The view column in which to open the log file. + * @returns {Thenable} A promise that resolves to true if the log file was opened successfully. + */ function openLogFile( logFile: string, openingFailureWarning: string, @@ -306,11 +393,20 @@ function openLogFile( }); } -function getTempWorkspace() { +/** + * Generates a temporary workspace path. + * @returns {string} The path to the temporary workspace. + */ +function getTempWorkspace(): string { return path.resolve(os.tmpdir(), 'vscodesws_' + makeRandomHexString(5)); } -function makeRandomHexString(length: number) { +/** + * Generates a random hexadecimal string of the given length. + * @param {number} length - The length of the hexadecimal string to generate. + * @returns {string} A random hexadecimal string. + */ +function makeRandomHexString(length: number): string { const chars = [ '0', '1', diff --git a/src/findJavaRuntimes.ts b/src/findJavaRuntimes.ts index 9526bfb..92f8595 100644 --- a/src/findJavaRuntimes.ts +++ b/src/findJavaRuntimes.ts @@ -2,15 +2,18 @@ // Licensed under the MIT license. import * as cp from 'child_process'; +import expandTilde from 'expand-tilde'; import * as fse from 'fs-extra'; import _ from 'lodash'; import * as os from 'os'; import * as path from 'path'; import WinReg from 'winreg'; -import expandTilde from 'expand-tilde'; + +// Platform detection const isWindows: boolean = process.platform.indexOf('win') === 0; const isMac: boolean = process.platform.indexOf('darwin') === 0; const isLinux: boolean = process.platform.indexOf('linux') === 0; + const JAVAC_FILENAME = 'javac' + (isWindows ? '.exe' : ''); const JAVA_FILENAME = 'java' + (isWindows ? '.exe' : ''); @@ -21,7 +24,10 @@ export interface JavaRuntime { } /** - * return metadata for all installed JDKs. + * Return metadata for all installed JDKs. + * This function searches for Java installations in common locations, + * environment variables, and the Windows Registry (if applicable). + * @returns {Promise} A promise that resolves with an array of JavaRuntime objects. */ export async function findJavaHomes(): Promise { const ret: JavaRuntime[] = []; @@ -50,6 +56,12 @@ export async function findJavaHomes(): Promise { return ret; } +/** + * Updates the JDK map with new entries. + * @param {Map} map - The map of JDKs. + * @param {string[]} newJdks - The new JDK paths to add. + * @param {string} source - The source from which these JDKs were found. + */ function updateJDKs(map: Map, newJdks: string[], source: string) { for (const newJdk of newJdks) { const sources = map.get(newJdk); @@ -61,6 +73,11 @@ function updateJDKs(map: Map, newJdks: string[], source: strin } } +/** + * Retrieves Java home paths from the specified environment variable. + * @param {string} name - The environment variable name. + * @returns {Promise} A promise that resolves with an array of Java home paths. + */ async function fromEnv(name: string): Promise { const ret: string[] = []; if (process.env[name]) { @@ -72,6 +89,10 @@ async function fromEnv(name: string): Promise { return ret; } +/** + * Retrieves Java home paths from the system PATH. + * @returns {Promise} A promise that resolves with an array of Java home paths. + */ async function fromPath(): Promise { const ret: string[] = []; @@ -113,6 +134,10 @@ async function fromPath(): Promise { } } +/** + * Retrieves Java home paths from the Windows Registry. + * @returns {Promise} A promise that resolves with an array of Java home paths. + */ async function fromWindowsRegistry(): Promise { if (!isWindows) { return []; @@ -190,6 +215,10 @@ async function fromWindowsRegistry(): Promise { return ret; } +/** + * Searches for Java installations in common locations on macOS, Windows, and Linux. + * @returns {Promise} A promise that resolves with an array of Java home paths. + */ async function fromCommonPlaces(): Promise { const ret: string[] = []; @@ -263,6 +292,12 @@ async function fromCommonPlaces(): Promise { return ret; } +/** + * Verifies if the provided path is a valid Java home by checking for the existence of `javac`. + * @param {string} raw - The raw path to check. + * @param {string} javaFilename - The name of the Java executable (e.g., `javac`). + * @returns {Promise} A promise that resolves with the verified Java home path, or undefined if the path is invalid. + */ export async function verifyJavaHome( raw: string, javaFilename: string, @@ -279,7 +314,11 @@ export async function verifyJavaHome( return undefined; } -// iterate through symbolic links until file is found +/** + * Iterates through symbolic links until the final file is found. + * @param {string} file - The file path to check for symbolic links. + * @returns {Promise} A promise that resolves with the final file path. + */ async function findLinkedFile(file: string): Promise { if (!(await fse.pathExists(file)) || !(await fse.lstat(file)).isSymbolicLink()) { return file; @@ -287,6 +326,11 @@ async function findLinkedFile(file: string): Promise { return findLinkedFile(await fse.readlink(file)); } +/** + * Retrieves the Java version from the specified Java home. + * @param {string} javaHome - The path to the Java home directory. + * @returns {Promise} A promise that resolves with the Java version number, or undefined if the version could not be determined. + */ export async function getJavaVersion(javaHome: string): Promise { let javaVersion = await checkVersionInReleaseFile(javaHome); if (!javaVersion) { @@ -295,6 +339,11 @@ export async function getJavaVersion(javaHome: string): Promise} A promise that resolves with the Java version number. */ async function checkVersionInReleaseFile(javaHome: string): Promise { if (!javaHome) { @@ -341,7 +392,9 @@ async function checkVersionInReleaseFile(javaHome: string): Promise { } /** - * Get version by parsing `JAVA_HOME/bin/java -version` + * Retrieves the Java version by parsing the output of `JAVA_HOME/bin/java -version`. + * @param {string} javaHome - The path to the Java home directory. + * @returns {Promise} A promise that resolves with the Java version number. */ async function checkVersionByCLI(javaHome: string): Promise { if (!javaHome) { diff --git a/src/javaServiceInstaller.ts b/src/javaServiceInstaller.ts index acce2f7..f665207 100644 --- a/src/javaServiceInstaller.ts +++ b/src/javaServiceInstaller.ts @@ -12,10 +12,21 @@ interface MavenCoords { type?: string; } +/** + * Returns the directory path where JAR files are stored. + * @returns {string} The JAR home directory path. + */ function getJarHome(): string { return path.join(__dirname, '../jars'); } +/** + * Retrieves the path of a service JAR file based on the Maven coordinates. + * @param {ExtensionContext} context - The VS Code extension context. + * @param {string} serviceName - The name of the service. + * @returns {string} The path to the service JAR file. + * @throws Will throw an error if the Maven coordinates for the service are not found. + */ export function getServicePath(context: ExtensionContext, serviceName: string): string { const coords = getCoords(context); const serviceCoords = coords[serviceName]; @@ -26,6 +37,11 @@ export function getServicePath(context: ExtensionContext, serviceName: string): return getServicePathFromCoords(serviceCoords); } +/** + * Retrieves Maven coordinates from the `package.json` file in the extension context. + * @param {ExtensionContext} context - The VS Code extension context. + * @returns {{ [serviceName: string]: MavenCoords }} An object containing Maven coordinates keyed by service name. + */ function getCoords(context: ExtensionContext): { [serviceName: string]: MavenCoords; } { @@ -36,17 +52,32 @@ function getCoords(context: ExtensionContext): { return javaDependencies as { [serviceName: string]: MavenCoords }; } +/** + * Generates the path for a service JAR file based on Maven coordinates. + * @param {MavenCoords} coords - The Maven coordinates of the service. + * @returns {string} The path to the service JAR file. + */ function getServicePathFromCoords(coords: MavenCoords): string { const jarHome = getJarHome(); const jarName = getLocalName(coords); return path.join(jarHome, jarName); } +/** + * Installs Java dependencies specified in the `package.json` file. + * @param {ExtensionContext} context - The VS Code extension context. + * @returns {Promise} A promise that resolves when all dependencies are installed. + */ export async function installJavaDependencies(context: ExtensionContext): Promise { const coords = getCoords(context); await installJavaDependenciesFromCoords(coords); } +/** + * Installs Java dependencies based on the provided Maven coordinates. + * @param {{ [serviceName: string]: MavenCoords }} coordsMaps - An object containing Maven coordinates keyed by service name. + * @returns {Promise} A promise that resolves when all dependencies are installed. + */ async function installJavaDependenciesFromCoords(coordsMaps: { [serviceName: string]: MavenCoords; }): Promise { @@ -55,12 +86,22 @@ async function installJavaDependenciesFromCoords(coordsMaps: { } } +/** + * Generates the local filename for a JAR file based on Maven coordinates. + * @param {MavenCoords} coords - The Maven coordinates of the service. + * @returns {string} The local filename for the JAR file. + */ function getLocalName(coords: MavenCoords): string { return `${coords.artifactId}-${coords.version}${ coords.classifier ? '-' + coords.classifier : '' }${coords.type ? '.' + coords.type : '.jar'}`; } +/** + * Generates the URL for searching and downloading a JAR file based on Maven coordinates. + * @param {MavenCoords} coords - The Maven coordinates of the service. + * @returns {string} The URL for searching and downloading the JAR file. + */ function getSearchUrl(coords: MavenCoords): string { const repository = coords.version.toLowerCase().endsWith('-snapshot') ? 'snapshots' : 'releases'; return ( @@ -69,6 +110,12 @@ function getSearchUrl(coords: MavenCoords): string { ); } +/** + * Installs a service if its JAR file is missing. + * @param {string} serviceName - The name of the service. + * @param {MavenCoords} coords - The Maven coordinates of the service. + * @returns {Promise} A promise that resolves when the service is installed. + */ async function installServiceIfMissing(serviceName: string, coords: MavenCoords): Promise { const doesExist = isServiceInstalled(coords); if (!doesExist) { @@ -85,6 +132,11 @@ async function installServiceIfMissing(serviceName: string, coords: MavenCoords) } } +/** + * Checks if a service JAR file is already installed. + * @param {MavenCoords} coords - The Maven coordinates of the service. + * @returns {boolean} `true` if the service JAR file is installed, `false` otherwise. + */ function isServiceInstalled(coords: MavenCoords): boolean { const jarPath = getServicePathFromCoords(coords); try { @@ -95,7 +147,13 @@ function isServiceInstalled(coords: MavenCoords): boolean { } } -// Installs a jar using maven coordinates +/** + * Installs a JAR file based on Maven coordinates. + * @param {string} serviceName - The name of the service. + * @param {MavenCoords} coords - The Maven coordinates of the service. + * @param {Progress<{ message?: string; increment?: number }>} [progress] - The progress reporter. + * @returns {Promise} A promise that resolves when the JAR file is installed. + */ async function installJar( serviceName: string, coords: MavenCoords, @@ -129,6 +187,15 @@ async function installJar( } } +/** + * Downloads a file from a URL to a specified path. + * @param {string} url - The URL of the file to download. + * @param {string} path - The path to save the downloaded file. + * @param {string} _serviceName - The name of the service. + * @param {number} [totalBytes] - The total size of the file in bytes. + * @param {Progress<{ message?: string; increment?: number }>} [progress] - The progress reporter. + * @returns {Promise} A promise that resolves when the file download is complete. + */ async function downloadFile( url: string, path: string, @@ -155,6 +222,13 @@ async function downloadFile( }); } +/** + * Sets up the download by determining the redirect URL and file size. + * @param {string} serviceName - The name of the service. + * @param {string} url - The URL to initiate the download. + * @returns {Promise<{ serverDownloadUrl: string, serverDownloadSize: number | undefined }>} An object containing the redirect URL and the file size. + * @throws Will throw an error if the redirect URL cannot be determined. + */ async function setupDownload(serviceName: string, url: string) { let response = await fetch(url, { redirect: 'manual' }); const redirectUrl = response.headers.get('location'); diff --git a/src/languageServerStarter.ts b/src/languageServerStarter.ts index 1172a8e..70ff578 100644 --- a/src/languageServerStarter.ts +++ b/src/languageServerStarter.ts @@ -1,5 +1,6 @@ import * as net from 'net'; import * as path from 'path'; +import { ExtensionContext } from 'vscode'; import { Executable, ExecutableOptions, @@ -9,14 +10,20 @@ import { import { logger } from './log'; import { RequirementsData } from './requirements'; -import { ExtensionContext } from 'vscode'; - declare const v8debug: any; const DEBUG = typeof v8debug === 'object' || startedInDebugMode(); export interface TransportExecutable extends Executable { transport: TransportKind; } + +/** + * Prepares the executable command to start the CQL language server. + * @param {RequirementsData} requirements - The requirements for the CQL language server, including the Java home and server JAR paths. + * @param {ExtensionContext} context - The VS Code extension context. + * @param {string} workspacePath - The path to the workspace directory. + * @returns {TransportExecutable} The executable configuration, including the command, arguments, and transport method. + */ export function prepareExecutable( requirements: RequirementsData, context: ExtensionContext, @@ -34,6 +41,12 @@ export function prepareExecutable( return executable; } + +/** + * Waits for the server connection on the specified port. + * @param {string} port - The port number to listen on for the server connection. + * @returns {Thenable} A promise that resolves with the StreamInfo when the connection is established. + */ export function awaitServerConnection(port: string): Thenable { const addr = parseInt(port); return new Promise((res, rej) => { @@ -51,6 +64,13 @@ export function awaitServerConnection(port: string): Thenable { }); } +/** + * Prepares the command-line arguments to start the CQL language server. + * @param {RequirementsData} requirements - The requirements for the CQL language server, including the Java home and server JAR paths. + * @param {ExtensionContext} context - The VS Code extension context. + * @param {string} workspacePath - The path to the workspace directory. + * @returns {string[]} An array of command-line arguments for starting the CQL language server. + */ function prepareParams( requirements: RequirementsData, context: ExtensionContext, @@ -75,12 +95,20 @@ function prepareParams( return params; } +/** + * Determines if the process was started in debug mode. + * @returns {boolean} `true` if the process was started in debug mode, `false` otherwise. + */ function startedInDebugMode(): boolean { const args = (process as any).execArgv as string[]; return hasDebugFlag(args); } -// If vscode is started with a debug flag this enables the java ls debug as well. +/** + * Checks if the provided arguments contain debug flags. + * @param {string[]} args - The command-line arguments to check. + * @returns {boolean} `true` if the arguments contain debug flags, `false` otherwise. + */ export function hasDebugFlag(args: string[]): boolean { if (args) { // See https://nodejs.org/en/docs/guides/debugging-getting-started/ diff --git a/src/normalizeCqlExecution.ts b/src/normalizeCqlExecution.ts index 4c719ee..ecd2df8 100644 --- a/src/normalizeCqlExecution.ts +++ b/src/normalizeCqlExecution.ts @@ -2,7 +2,14 @@ import { Uri, window } from 'vscode'; import { buildParameters } from './buildParameters'; import { executeCQL } from './executeCql'; -export async function normalizeCqlExecution(uri: Uri, type: 'file' | 'expression') { +/** + * Normalizes the execution of CQL based on the type of operation ('file' or 'expression'). + * + * @param {Uri} uri - The URI of the file or resource. + * @param {'file' | 'expression'} type - The type of operation: 'file' for full file execution, 'expression' for single expression execution. + * @returns {Promise} A promise that resolves when the execution is complete. + */ +export async function normalizeCqlExecution(uri: Uri, type: 'file' | 'expression'): Promise { const editor = window.activeTextEditor; if (!editor) { window.showInformationMessage('No active text editor found.'); @@ -19,9 +26,7 @@ export async function normalizeCqlExecution(uri: Uri, type: 'file' | 'expression if (type === 'file') { operationArgs = buildParameters(uri, undefined); } else if (type === 'expression') { - // For now using parsing the definition here, but ideally should be communicating with the Language Server - // Could try something like https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_selectionRange after grabbing the start and end positions of a selection - // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_signatureHelp *Definition Signature???* + // Process the CQL expression based on the cursor position let cursorPosition = editor.selection.active; let line = editor.document.lineAt(cursorPosition).text; diff --git a/tsconfig.json b/tsconfig.json index 9f966ee..497f210 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,5 +12,5 @@ "noImplicitAny": true, "checkJs": true }, - "exclude": ["node_modules", ".vscode-test", "dist", "vscode-test.mjs"] + "exclude": ["node_modules", ".vscode-test", "dist", "vscode-test.mjs", "docs"] } From 4c1d1337902553a1318117a7075597ddae32d2de Mon Sep 17 00:00:00 2001 From: jreyno77 Date: Thu, 29 Aug 2024 12:07:09 -0600 Subject: [PATCH 2/6] add module documentation stubs --- src/buildParameters.ts | 17 +++-------------- src/connectionManager.ts | 3 +++ src/cqlLanguageClient.ts | 3 +++ src/executeCql.ts | 3 +++ src/extension.ts | 3 +++ src/findJavaRuntimes.ts | 4 +++- src/javaServiceInstaller.ts | 3 +++ src/languageServerStarter.ts | 3 +++ src/normalizeCqlExecution.ts | 3 +++ src/requirements.ts | 3 +++ src/statusBar.ts | 5 ++++- src/utils.ts | 3 +++ 12 files changed, 37 insertions(+), 16 deletions(-) diff --git a/src/buildParameters.ts b/src/buildParameters.ts index caf5d15..f6706ae 100644 --- a/src/buildParameters.ts +++ b/src/buildParameters.ts @@ -1,3 +1,6 @@ +/** + * This is a stub description for now. + */ import * as fs from 'fs'; import * as fse from 'fs-extra'; import { glob } from 'glob'; @@ -75,20 +78,6 @@ export function buildParameters(uri: URI, expression: string | undefined): Evalu return evaluationParams; } -/** - * Generates the command-line arguments required for CQL evaluation. - * @param {object} params - The parameters needed to construct the command-line arguments. - * @param {string} params.fhirVersion - The FHIR version being used. - * @param {URI} params.optionsPath - The path to the CQL options file. - * @param {URI} params.libraryDirectory - The directory of the CQL library. - * @param {string} params.libraryName - The name of the CQL library. - * @param {string | undefined} params.expression - The specific CQL expression to evaluate, if any. - * @param {URI} params.terminologyPath - The path to the FHIR terminology. - * @param {Connection | undefined} params.connection - The current connection information. - * @param {Map} params.contexts - The execution contexts for the evaluation. - * @param {string} params.measurementPeriod - The measurement period to be used. - * @returns {string[]} The command-line arguments for the CQL evaluation. - */ function getCqlCommandArgs({ fhirVersion, optionsPath, diff --git a/src/connectionManager.ts b/src/connectionManager.ts index a89646e..3d0dd7a 100644 --- a/src/connectionManager.ts +++ b/src/connectionManager.ts @@ -1,3 +1,6 @@ +/** + * This is a stub description for now. + */ import { ExtensionContext } from 'vscode'; import { Storage } from './storage'; diff --git a/src/cqlLanguageClient.ts b/src/cqlLanguageClient.ts index 2ed9ff5..66a2916 100644 --- a/src/cqlLanguageClient.ts +++ b/src/cqlLanguageClient.ts @@ -1,3 +1,6 @@ +/** + * This is a stub description for now. + */ import { commands, ExtensionContext, Uri, window, workspace } from 'vscode'; import { ConfigurationParams, diff --git a/src/executeCql.ts b/src/executeCql.ts index 1ebbc42..ca704cc 100644 --- a/src/executeCql.ts +++ b/src/executeCql.ts @@ -1,3 +1,6 @@ +/** + * This is a stub description for now. + */ import * as fs from 'fs'; import { Position, TextEditor, Uri, commands, window, workspace } from 'vscode'; import { EvaluationParameters } from './buildParameters'; diff --git a/src/extension.ts b/src/extension.ts index eabf0b4..55983ba 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,3 +1,6 @@ +/** + * This is a stub description for now. + */ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; diff --git a/src/findJavaRuntimes.ts b/src/findJavaRuntimes.ts index 92f8595..2eb3f95 100644 --- a/src/findJavaRuntimes.ts +++ b/src/findJavaRuntimes.ts @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. - +/** + * This is a stub description for now. + */ import * as cp from 'child_process'; import expandTilde from 'expand-tilde'; import * as fse from 'fs-extra'; diff --git a/src/javaServiceInstaller.ts b/src/javaServiceInstaller.ts index f665207..debc792 100644 --- a/src/javaServiceInstaller.ts +++ b/src/javaServiceInstaller.ts @@ -1,3 +1,6 @@ +/** + * This is a stub description for now. + */ import * as fs from 'fs'; import fetch from 'node-fetch'; import * as path from 'path'; diff --git a/src/languageServerStarter.ts b/src/languageServerStarter.ts index 70ff578..4910165 100644 --- a/src/languageServerStarter.ts +++ b/src/languageServerStarter.ts @@ -1,3 +1,6 @@ +/** + * This is a stub description for now. + */ import * as net from 'net'; import * as path from 'path'; import { ExtensionContext } from 'vscode'; diff --git a/src/normalizeCqlExecution.ts b/src/normalizeCqlExecution.ts index ecd2df8..41ed925 100644 --- a/src/normalizeCqlExecution.ts +++ b/src/normalizeCqlExecution.ts @@ -1,3 +1,6 @@ +/** + * This is a stub description for now. + */ import { Uri, window } from 'vscode'; import { buildParameters } from './buildParameters'; import { executeCQL } from './executeCql'; diff --git a/src/requirements.ts b/src/requirements.ts index 030496c..6f24d25 100644 --- a/src/requirements.ts +++ b/src/requirements.ts @@ -1,3 +1,6 @@ +/** + * This is a stub description for now. + */ import expandTilde from 'expand-tilde'; import * as fse from 'fs-extra'; import * as path from 'path'; diff --git a/src/statusBar.ts b/src/statusBar.ts index 200363c..30634ea 100644 --- a/src/statusBar.ts +++ b/src/statusBar.ts @@ -1,4 +1,7 @@ -import { StatusBarItem, window, StatusBarAlignment } from 'vscode'; +/** + * This is a stub description for now. + */ +import { StatusBarAlignment, StatusBarItem, window } from 'vscode'; import { Disposable } from 'vscode-languageclient'; class StatusBar implements Disposable { diff --git a/src/utils.ts b/src/utils.ts index 417bdbe..d4b0ea6 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,6 @@ +/** + * This is a stub description for now. + */ import * as fs from 'fs'; import * as path from 'path'; From d6fd17410d203e2a7e4d4151dace2f2d8d972d6e Mon Sep 17 00:00:00 2001 From: jreyno77 Date: Thu, 29 Aug 2024 16:17:13 -0600 Subject: [PATCH 3/6] finished up buildParameters docs and added a typedoc config to clean up the end docs generated. --- src/buildParameters.ts | 226 ++++++++++++++++++++++++++++++++++++++++- typedoc.json | 3 + 2 files changed, 225 insertions(+), 4 deletions(-) create mode 100644 typedoc.json diff --git a/src/buildParameters.ts b/src/buildParameters.ts index f6706ae..f9fe681 100644 --- a/src/buildParameters.ts +++ b/src/buildParameters.ts @@ -1,6 +1,7 @@ /** - * This is a stub description for now. + * Couldn't find a way to surface this documentation section. Would love to include most of the information from the buildParameters method here. */ + import * as fs from 'fs'; import * as fse from 'fs-extra'; import { glob } from 'glob'; @@ -17,6 +18,223 @@ export type EvaluationParameters = { /** * Builds the parameters required for CQL evaluation. + * + * This function gathers and constructs all the necessary operational arguments for invoking the CQL + * Language Service CLI. It retrieves and organizes the data points from the local environment, + * which include CQL files, terminology, and test data, to prepare for evaluation. Currently, only local + * paths are supported, but the design anticipates potential future support for remote URLs or database + * locations. + * + * **Quick Examples:** + * + * - **Local CQL File without Expression:** + * ```typescript + * const uri = URI.parse("file:///Users/developer/vscode-project/input/cql/my-library.cql"); + * const params = buildParameters(uri, undefined); + * console.log(params); + * // Output: + * // { + * // operationArgs: [ + * // "-fv=R4", + * // "-op=/Users/developer/vscode-project/input/cql/cql-options.json", + * // "-lu=/Users/developer/vscode-project/input/cql", + * // "-ln=my-library", + * // "-t=/Users/developer/vscode-project/input/vocabulary/valueset", + * // "-m=FHIR", + * // "-c=Patient" + * // ], + * // outputPath: URI.parse("file:///Users/developer/vscode-project/input/tests/results/my-library.txt"), + * // testPath: URI.parse("file:///Users/developer/vscode-project/input/tests") + * // } + * ``` + * + * - **Local CQL File with Expression:** + * ```typescript + * const params = buildParameters(uri, "myExpression"); + * console.log(params); + * // Output: + * // { + * // operationArgs: [ + * // "-fv=R4", + * // "-op=/Users/developer/vscode-project/input/cql/cql-options.json", + * // "-lu=/Users/developer/vscode-project/input/cql", + * // "-ln=my-library", + * // "-e=myExpression", + * // "-t=/Users/developer/vscode-project/input/vocabulary/valueset", + * // "-m=FHIR", + * // "-c=Patient" + * // ], + * // outputPath: URI.parse("file:///Users/developer/vscode-project/input/tests/results/my-library.txt"), + * // testPath: URI.parse("file:///Users/developer/vscode-project/input/tests") + * // } + * ``` + * + * - **Remote Connection with Expression:** + * ```typescript + * // Assuming ConnectionManager is configured to use a remote connection. + * const params = buildParameters(uri, "myExpression"); + * console.log(params); + * // Output: + * // { + * // operationArgs: [ + * // "-fv=R4", + * // "-op=/Users/developer/vscode-project/input/cql/cql-options.json", + * // "-lu=/Users/developer/vscode-project/input/cql", + * // "-ln=my-library", + * // "-e=myExpression", + * // "-t=/Users/developer/vscode-project/input/vocabulary/valueset", + * // "-m=FHIR", + * // "-mu=remote-url", + * // "-c=Patient" + * // ], + * // outputPath: URI.parse("file:///Users/developer/vscode-project/input/tests/results/my-library.txt"), + * // testPath: URI.parse("remote-url") + * // } + * ``` + * + * **Dependencies:** + * + * - **Active Editor Requirement:** + * The function is currently dependent on the active text editor being open with the CQL file. + * This is because the FHIR version is extracted from the open document. Ideally, this should + * be refactored to read from a file based on the URI to remove this dependency. + * See: [VS Code Active Text Editor Documentation](https://code.visualstudio.com/api/references/vscode-api#window.activeTextEditor). + * + * - **Connection Manager:** + * The function heavily relies on the {@link ConnectionManager} to determine the context and connection + * details, particularly when working with remote connections. If a remote connection is active, + * the function can use the remote connection URL as the test data path. + * + * **Operational Components:** + * + * 1. **CQL Data:** + * - The main CQL file specified by the `uri` parameter. + * - Extracts the `libraryName` and `libraryDirectory` from the given URI. + * + * 2. **Terminology Data:** + * - Points to local terminology files located in `input/vocabulary/valueset`. + * + * 3. **Test Data and Results:** + * - Test data is expected in `input/tests`, with results being saved in `input/tests/results`. + * - If a remote connection is active, the test data path may point to a remote URL instead. + * + * @remarks + * + * Build Parameters is meant to provide the extension with the capability of building a set of parameters + * to reflect the execution requirements of the Language Service CLI. + * + * There is an expected format for the retrieval of necessary data required to build these parameters. + * Those are identified as Module level constants. + * + * **Visual Representations:** + * + * ``` + * file:///Users/developer/vscode-project/input/cql/my-library.cql + * \___________________________________/ \____/\_______/\____________/ + * projectPath input lib_dir my-library.cql + * ``` + * + * ``` + * ..projectPath/input/vocabulary/valueset + * \____/ \_________________/ + * input vocabulary/valueset + * ``` + * + * ``` + * ..projectPath/input/tests/results/my-library.txt + * \____/ \____/ \______/\___________/ + * input tests results my-library.txt + * ``` + * + * **Example usage:** + * + * Given the following URI for a CQL file in a VS Code extension project: + * + * `uri: "file:///Users/developer/vscode-project/input/cql/my-library.cql"` + * + * **Breakdown of Example URI:** + * + * 1. **CQL:** + * - projectPath: `"file:///Users/developer/vscode-project"` + * - input: `"input"` + * - cql directory: `"cql"` + * - file name: `"my-library.cql"` + * + * 2. **Terminology:** + * - input: `"input"` + * - subdirectory: `"vocabulary/valueset"` + * + * 3. **Data/Results:** + * - input: `"input"` + * - test directory: `"tests"` + * - results directory: `"results"` + * - output file: `"my-library.txt"` + * + * **Derived Constants:** + * + * - **projectPath:** `"file:///Users/developer/vscode-project"` + * - The root path of the VS Code workspace. + * + * - **libraryDirectory:** `"file:///Users/developer/vscode-project/input/library"` + * - The directory containing the CQL file. + * + * - **libraryName:** `"my-library"` + * - The base name of the CQL file. + * + * - **terminologyPath:** `"file:///Users/developer/vscode-project/input/vocabulary/valueset"` + * - The path to the terminology files (value sets). + * + * - **testPath:** `"file:///Users/developer/vscode-project/input/tests"` + * - The path to test cases related to the CQL file. + * + * - **resultPath:** `"file:///Users/developer/vscode-project/input/tests/results"` + * - The directory where execution results will be stored. + * + * - **outputPath:** `"file:///Users/developer/vscode-project/input/tests/results/my-library.txt"` + * - The full path to the output file containing the evaluation results. + * --- + * **Considerations and Limitations:** + * + * - **Measurement Period:** + * *Currently not implemented; a default parameter is used during CQL execution.* + * + * - **FHIR as the Data Model:** + * *The function is limited to using FHIR as the data model.* + * + * - **CQL Options File Location:** + * *The `cql-options.json` file must be located in the same directory as the CQL libraries.* + * + * - **Library Name Dependency:** + * *The library name must be part of the CQL file name.* + * + * - **Context Limitations:** + * *The function is currently limited to handling the "Patient" context.* + * + * --- + * + * **Future Enhancements:** + * + * - **Measurement Period:** + * *Add support for defining and using measurement periods within CQL execution parameters.* + * + * - **Support for Additional Data Models:** + * *Expand support beyond FHIR to include other data models.* + * + * - **Flexible CQL Options File Location:** + * *Allow the `cql-options.json` file to be located in different directories or derive its path dynamically.* + * + * - **Automatic Library Name Extraction:** + * *Automatically derive the library name from the CQL file content, removing the dependency on the file name.* + * + * - **Expanded Context Support:** + * *Enable support for multiple contexts, such as Encounters or Organizations, for more complex CQL evaluations.* + * + * - **Remote URLs and Database Connections:** + * *Support retrieving CQL, terminology, and test data from remote URLs or databases.* + * + * - **Refactor FHIR Version Extraction:** + * *Eliminate the dependency on the active text editor by extracting the FHIR version directly from the CQL file.* + * * @param {URI} uri - The URI of the CQL file. * @param {string | undefined} expression - The specific CQL expression to evaluate, if any. * @returns {EvaluationParameters} The parameters required for the CQL evaluation. @@ -35,12 +253,12 @@ export function buildParameters(uri: URI, expression: string | undefined): Evalu const libraryName = Utils.basename(uri).replace('.cql', '').split('-')[0]; const projectPath = workspace.getWorkspaceFolder(uri)!.uri; const terminologyPath: URI = Utils.resolvePath(projectPath, 'input', 'vocabulary', 'valueset'); - const fhirVersion = getFhirVersion(); - const optionsPath = Utils.resolvePath(libraryDirectory, 'cql-options.json'); - const measurementPeriod = ''; const testPath = Utils.resolvePath(projectPath, 'input', 'tests'); const resultPath = Utils.resolvePath(testPath, 'results'); const outputPath = Utils.resolvePath(resultPath, `${libraryName}.txt`); + const fhirVersion = getFhirVersion(); + const optionsPath = Utils.resolvePath(libraryDirectory, 'cql-options.json'); + const measurementPeriod = ''; const connectionManager = ConnectionManager.getManager(); fse.ensureFileSync(outputPath.fsPath); diff --git a/typedoc.json b/typedoc.json new file mode 100644 index 0000000..40838c7 --- /dev/null +++ b/typedoc.json @@ -0,0 +1,3 @@ +{ + "excludeNotDocumented": true +} From 151668473b7620141f6840b99f5c3447b60d3aea Mon Sep 17 00:00:00 2001 From: jreyno77 Date: Thu, 29 Aug 2024 16:25:55 -0600 Subject: [PATCH 4/6] hotfix specify that remote data can be used in docs. --- src/buildParameters.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/buildParameters.ts b/src/buildParameters.ts index f9fe681..a834aeb 100644 --- a/src/buildParameters.ts +++ b/src/buildParameters.ts @@ -22,7 +22,7 @@ export type EvaluationParameters = { * This function gathers and constructs all the necessary operational arguments for invoking the CQL * Language Service CLI. It retrieves and organizes the data points from the local environment, * which include CQL files, terminology, and test data, to prepare for evaluation. Currently, only local - * paths are supported, but the design anticipates potential future support for remote URLs or database + * paths are supported(other than remote data), but the design anticipates potential future support for remote URLs or database * locations. * * **Quick Examples:** From cbc3ee353f437a7c46fbeb3127f6fbacb9ad37b6 Mon Sep 17 00:00:00 2001 From: jreyno77 Date: Fri, 6 Sep 2024 10:09:03 -0600 Subject: [PATCH 5/6] Added further documentation for connectionManager, executeCql, and normalizeCqlExecution --- src/connectionManager.ts | 122 ++++++++++++++++++++++++++++++++++- src/executeCql.ts | 111 +++++++++++++++++++++++++------ src/normalizeCqlExecution.ts | 63 ++++++++++++++++-- 3 files changed, 273 insertions(+), 23 deletions(-) diff --git a/src/connectionManager.ts b/src/connectionManager.ts index 3d0dd7a..2cb9520 100644 --- a/src/connectionManager.ts +++ b/src/connectionManager.ts @@ -17,7 +17,127 @@ export interface Connection { } /** - * Manages connections and their associated contexts. + * Manages multiple FHIR data connections and their associated contexts for CQL evaluations. + * + * The **Connection Manager** allows for managing multiple data connections, but only one connection can be active at a time. + * It currently supports FHIR server connections and local file system connections. It provides full CRUD capabilities and + * ensures that connections and contexts persist across sessions using VS Code's global state storage. + * + * + * **Quick Example:** + * + * - **Creating a New Connection**: + * ```typescript + * const connection: Connection = { + * name: "Local Connection", + * endpoint: "file:///Users/username/test-dir", + * contexts: { + * "Patient/123": { + * resourceID: "123", + * resourceType: "Patient", + * resourceDisplay: "John Doe" + * } + * } + * }; + * ConnectionManager.getManager().upsertConnection(connection); + * ``` + * + * + * - **Setting the Active Connection**: + * ```typescript + * ConnectionManager.getManager().setCurrentConnection("Local Connection"); + * ``` + * + * **Key Operations:** + * + * - **Read**: Retrieve all stored connections and their associated contexts. + * - **Upsert**: Add or update connections and contexts. + * - **Delete**: Remove connections and contexts. + * + * **Further Examples:** + * + * - **Retrieving the Current Active Connection**: + * ```typescript + * const currentConnection = ConnectionManager.getManager().getCurrentConnection(); + * console.log(currentConnection); + * ``` + * + * - **Deleting a Connection**: + * ```typescript + * ConnectionManager.getManager().deleteConnection("Remote Connection"); + * ``` + * + * **CRUD Operations:** + * + * - **Create or Update a Connection**: + * Use `upsertConnection` to add a new connection or update an existing one. + * ```typescript + * ConnectionManager.getManager().upsertConnection(connection); + * ``` + * + * - **Read Connections**: + * Retrieve all connections or get the current active connection. + * ```typescript + * const allConnections = ConnectionManager.getManager().getAllConnections(); + * const currentConnection = ConnectionManager.getManager().getCurrentConnection(); + * ``` + * + * - **Delete a Connection**: + * Remove a connection by name. + * ```typescript + * ConnectionManager.getManager().deleteConnection("Local Connection"); + * ``` + * + * - **Set the Current Active Connection**: + * Choose which connection will be the active one for CQL evaluations. + * ```typescript + * ConnectionManager.getManager().setCurrentConnection("Remote Connection"); + * ``` + * + * **Context Management**: + * + * - **Adding or Updating a Context**: + * Add or update a context within a specific connection. Contexts represent specific resources used in CQL evaluations (e.g., Patients). + * ```typescript + * const patientContext: Context = { resourceID: "123", resourceType: "Patient", resourceDisplay: "John Doe" }; + * ConnectionManager.getManager().upsertContext("Local Connection", patientContext); + * ``` + * + * - **Retrieving Contexts for the Current Connection**: + * ```typescript + * const contexts = ConnectionManager.getManager().getCurrentContexts(); + * console.log(contexts); + * ``` + * + * - **Deleting a Context**: + * ```typescript + * ConnectionManager.getManager().deleteContext("Local Connection", "Patient/123"); + * ``` + * + * **Important Notes:** + * + * - **Context Parameters**: + * Contexts represent specific resource types such as Patients, Encounters, or Organizations. Each connection may have multiple contexts, + * but currently, only "Patient" is supported. Future updates will expand this to include other resource types. + * + * - **Single Active Connection**: + * Only one connection can be active at a time. Switching between connections is managed by the `setCurrentConnection` method. + * + * **Considerations and Limitations:** + * + * - **FHIR-Only**: The connection manager is currently limited to managing FHIR-based data connections. + * - **Single Active Connection**: Only one connection may be active at a time for CQL evaluations. + * - **Context Limitations**: Only the "Patient" context parameter is currently supported. Other contexts like "Encounters" or "Organizations" + * are expected to be supported in future updates. + * + * **Future Enhancements:** + * + * - **Additional Context Support**: Expand support to include other context types, such as Encounters and Organizations. + * - **Terminology Repositories**: Add support for managing connections to external terminology repositories. + * - **Artifact Repositories**: Include support for managing repositories of CQL artifacts (e.g., Libraries, PlanDefinitions, Measures). + * - **Multiple Active Connections**: Enable the possibility of managing and utilizing multiple active connections for more complex workflows. + * + * @param {ExtensionContext} ec - The extension context used to initialize the ConnectionManager. */ export class ConnectionManager { private static connectionManager: ConnectionManager; diff --git a/src/executeCql.ts b/src/executeCql.ts index ca704cc..1c67532 100644 --- a/src/executeCql.ts +++ b/src/executeCql.ts @@ -1,27 +1,74 @@ -/** - * This is a stub description for now. - */ import * as fs from 'fs'; import { Position, TextEditor, Uri, commands, window, workspace } from 'vscode'; import { EvaluationParameters } from './buildParameters'; import { Commands } from './commands'; /** - * Inserts a line of text at the end of the document in the given text editor. - * @param {TextEditor} textEditor - The text editor where the text should be inserted. - * @param {string} text - The text to insert. - */ -async function insertLineAtEnd(textEditor: TextEditor, text: string) { - const document = textEditor.document; - await textEditor.edit(editBuilder => { - editBuilder.insert(new Position(textEditor.document.lineCount, 0), text + '\n'); - }); -} - -/** - * Executes CQL (Clinical Quality Language) based on the provided evaluation parameters. - * Outputs results to a text document specified by the evaluation parameters. - * @param {EvaluationParameters} evaluationParams - The parameters required for executing CQL. + * Executes Clinical Quality Language (CQL) operations based on the provided parameters. + * + * This function manages the execution of CQL commands by sending operation parameters to the CQL Language Server + * and capturing the results in a text file. The output includes messages that describe the execution environment + * (CQL file location, terminology, test cases, and context values), along with the results for each context. + * + * **Quick Example:** + * + * - **Executing CQL with Parameters**: + * ```typescript + * const evaluationParams: EvaluationParameters = { + * operationArgs: ['-lu=/path/to/cql', '-t=/path/to/terminology', '-mu=/path/to/test'], + * outputPath: Uri.parse("file:///path/to/output.txt"), + * testPath: Uri.parse("file:///path/to/test") + * }; + * + * await executeCQL(evaluationParams); + * // Outputs the execution result to the specified output file. + * ``` + * + * **Purpose:** + * + * - **CQL Execution**: Interacts with the CQL Language Server to execute a CQL library or expression. + * - **Results Logging**: Manages and formats the execution output into a text file, displaying key context-related data and results. + * - **Timing**: Logs the elapsed time of the execution for performance analysis. + * + * **Execution Breakdown:** + * + * 1. **CQL Message**: Captures the CQL file's location using the `-lu=` argument. + * 2. **Terminology Message**: Captures terminology data location using the `-t=` argument. + * 3. **Test Data and Contexts**: Logs the test cases and context values found in the provided data. + * + * **Example Output in the Resulting Text File:** + * + * ``` + * CQL: path/to/cql + * Terminology: /path/to/terminology + * Test cases: + * path/to/test - 123 + * path/to/test - 456 + * + * Patient 123: + * ... [execution result] ... + * Patient 456: + * ... [execution result] ... + * elapsed: 3.45 seconds + * ``` + * + * **Functionality:** + * + * - Executes the CQL using `Commands.EXECUTE_WORKSPACE_COMMAND` and `Commands.EXECUTE_CQL`. + * - Inserts important messages and results into the output text file. + * - Interleaves context-related information with the results, enhancing readability. + * + * **Future Enhancements:** + * + * - Improve result formatting for better readability (e.g., JSON or other structured formats). + * - Add support for more complex result handling, such as different output file formats (e.g., CSV, XML). + * + * **Considerations:** + * + * - **Context-Related Execution**: The function supports multiple contexts and interleaves their corresponding results. + * - **File Output**: Currently, results are only saved in a text file format. + * + * @param {EvaluationParameters} evaluationParams - The parameters required for executing CQL. See {@link EvaluationParameters}. */ export async function executeCQL({ operationArgs, testPath, outputPath }: EvaluationParameters) { let cqlMessage = ''; @@ -30,6 +77,7 @@ export async function executeCQL({ operationArgs, testPath, outputPath }: Evalua let foundTest = false; const contextValues: string[] = []; + // Loop through the operation arguments to build messages and find test data for (let i = 0; i < operationArgs?.length!; i++) { if (operationArgs![i].startsWith('-lu=')) { cqlMessage = `CQL: ${operationArgs![i].substring(4)}`; @@ -49,10 +97,12 @@ export async function executeCQL({ operationArgs, testPath, outputPath }: Evalua } } + // Handle the case when no test data is found if (!foundTest) { testMessage = `No data found at path ${testPath!}. Evaluation may fail if data is required.`; } + // Open the output file and start inserting information const textDocument = await workspace.openTextDocument(outputPath!); const textEditor = await window.showTextDocument(textDocument); @@ -60,6 +110,7 @@ export async function executeCQL({ operationArgs, testPath, outputPath }: Evalua await insertLineAtEnd(textEditor, `${terminologyMessage}`); await insertLineAtEnd(textEditor, `${testMessage}`); + // Start timing the execution const startExecution = Date.now(); const result: string | undefined = await commands.executeCommand( Commands.EXECUTE_WORKSPACE_COMMAND, @@ -68,9 +119,11 @@ export async function executeCQL({ operationArgs, testPath, outputPath }: Evalua ); const endExecution = Date.now(); + // Interleave results with context const expression = operationArgs?.find(arg => arg.startsWith('-e='))?.replace('-e=', ''); await insertLineAtEnd(textEditor, attemptInterleave(expression, contextValues, result || '')); + // Log the execution time await insertLineAtEnd( textEditor, `elapsed: ${((endExecution - startExecution) / 1000).toString()} seconds`, @@ -80,6 +133,16 @@ export async function executeCQL({ operationArgs, testPath, outputPath }: Evalua /** * Attempts to interleave test case names with result expressions for better readability. * Returns the result unmodified if unable to interleave. + * + * **Example of Interleaving Output**: + * + * ``` + * Patient: 123 + * ExpressionResult: ... + * Patient: 456 + * ExpressionResult: ... + * ``` + * * @param {string | undefined} expression - The CQL expression being evaluated. * @param {string[]} contextValues - The context values to interleave with the results. * @param {string} result - The result string to be interleaved. @@ -115,3 +178,15 @@ const attemptInterleave = ( return result; } }; + +/** + * Inserts a line of text at the end of the document in the given text editor. + * @param {TextEditor} textEditor - The text editor where the text should be inserted. + * @param {string} text - The text to insert. + */ +async function insertLineAtEnd(textEditor: TextEditor, text: string) { + const document = textEditor.document; + await textEditor.edit(editBuilder => { + editBuilder.insert(new Position(textEditor.document.lineCount, 0), text + '\n'); + }); +} diff --git a/src/normalizeCqlExecution.ts b/src/normalizeCqlExecution.ts index 41ed925..8863e00 100644 --- a/src/normalizeCqlExecution.ts +++ b/src/normalizeCqlExecution.ts @@ -1,17 +1,72 @@ -/** - * This is a stub description for now. - */ import { Uri, window } from 'vscode'; import { buildParameters } from './buildParameters'; import { executeCQL } from './executeCql'; /** - * Normalizes the execution of CQL based on the type of operation ('file' or 'expression'). + * Normalizes the execution of CQL operations based on the type of operation ('file' or 'expression'). + * + * This function serves as a **Gatekeeper** by validating the workspace environment and **Normalizing** the data + * required for execution. It takes VS Code-specific details, determines the type of operation to be executed + * (such as [$cql](https://build.fhir.org/ig/HL7/cql-ig/OperationDefinition-cql-cql.html), [$evaluate-measure](https://hl7.org/fhir/R4/measure-operation-evaluate-measure.html), [$apply](https://hl7.org/fhir/R4/plandefinition-operation-apply.html) for PlanDefinitions, or [$evaluate](https://build.fhir.org/ig/HL7/cql-ig/OperationDefinition-cql-library-evaluate.html) for Libraries), and standardizes + * this data into strings or URIs. These are then passed on to {@link buildParameters} for the construction of the + * necessary execution parameters, which are finally handed off to {@link executeCQL} to perform the execution. + * + * **Purpose:** + * + * - **Gatekeeper**: Validates the current state, ensuring the correct file type (CQL) and operation type (file or expression). + * - **Normalizer**: Takes workspace-specific details and standardizes them into a format suitable for execution. + * - **Executor**: Delegates the actual execution to the appropriate functions after normalizing the input data. + * + * **Quick Examples:** + * + * - **Executing a Full CQL File**: + * ```typescript + * const uri = URI.parse("file:///Users/developer/vscode-project/input/cql/my-library.cql"); + * await normalizeCqlExecution(uri, 'file'); + * // Executes the entire CQL file specified by the URI. + * ``` + * + * - **Executing a Single CQL Expression**: + * ```typescript + * const uri = URI.parse("file:///Users/developer/vscode-project/input/cql/my-library.cql"); + * await normalizeCqlExecution(uri, 'expression'); + * // Executes a single expression based on the current cursor position in the active editor. + * ``` + * + * **Dependencies:** + * + * - **Active Text Editor**: The function depends on the active text editor being open with the CQL file. + * - **CQL File Requirement**: The function currently only supports CQL files. In the future, support for Measure, PlanDefinition, and Questionnaire files may be added. + * - **Helper Functions**: + * - **`{@link buildParameters}(uri, expression)`**: Collects all parameters required for execution. + * - **`{@link executeCQL}(operationArgs)`**: Handles the actual execution of the CQL. + * + * **Considerations and Limitations:** + * + * - **Expression Execution**: + * - The function relies on the presence of a valid CQL `define` statement on the current cursor line when executing a single expression. + * - If no valid definition is found, an error message is displayed, and execution is halted. + * + * - **File Type Limitation**: + * - Currently limited to `.cql` files. Future enhancements may include support for [Measure](https://hl7.org/fhir/R4/measure.html), [PlanDefinition](https://hl7.org/fhir/R4/plandefinition.html), and [Questionnaire](https://hl7.org/fhir/R4/questionnaire.html) files. + * + * **Future Enhancements:** + * + * - **Measurement Period**: Incorporate support for Measure files. + * - **PlanDefinition and Questionnaire Support**: Expand the function to handle these additional FHIR resources. + * - **Flexible Contexts**: Enable the function to handle more complex execution contexts beyond CQL, such as PlanDefinitions or other FHIR artifacts. + * + * **Error Handling**: + * + * - **No Active Editor**: Displays a message if no active text editor is found. + * - **Invalid File Type**: Displays a message if the current file is not a CQL file. + * - **Missing Definition**: Displays an error if no valid CQL definition is found on the selected line during expression execution. * * @param {Uri} uri - The URI of the file or resource. * @param {'file' | 'expression'} type - The type of operation: 'file' for full file execution, 'expression' for single expression execution. * @returns {Promise} A promise that resolves when the execution is complete. */ + export async function normalizeCqlExecution(uri: Uri, type: 'file' | 'expression'): Promise { const editor = window.activeTextEditor; if (!editor) { From 90c91bcb73161af9ed672e1bc40d1bcbb48f3409 Mon Sep 17 00:00:00 2001 From: jreyno77 Date: Fri, 6 Sep 2024 13:19:36 -0600 Subject: [PATCH 6/6] Add doc generation to vscode:prepublish --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ad58a7d..c2c94d4 100644 --- a/package.json +++ b/package.json @@ -153,7 +153,7 @@ "color-name": "1.1.3" }, "scripts": { - "vscode:prepublish": "npm run compile", + "vscode:prepublish": "npm run compile && npx typedoc --out docs --entryPointStrategy expand src/", "compile": "tsc -p ./", "watch": "tsc -watch -p ./", "pretest": "npm run clean && npm run compile && cp -r src/test/suite/resources dist/test/suite/resources",