From bb77c29d219f1d59a4fe0bb4b0c10471c3676c03 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Thu, 26 Oct 2023 20:50:40 +0200 Subject: [PATCH 01/98] Refactor WorkflowLanguageService + renamings Rename WorkflowLanguageService to LanguageServiceBase and make it generic (for any kind of documents) as it doesn't have anything particularly workflow related about it. Includes more rename refactorings around language services definitions. --- .../src/languageService.ts | 10 ++--- .../src/languageService.ts | 6 +-- .../server-common/src/languageTypes.ts | 40 +++++++++++-------- .../src/models/workflowDocument.ts | 4 +- .../src/providers/completionProvider.ts | 2 +- .../src/providers/formattingProvider.ts | 2 +- .../src/providers/hover/hoverProvider.ts | 2 +- server/packages/server-common/src/server.ts | 18 ++++----- .../src/services/cleanWorkflow.ts | 2 +- .../yaml-language-service/src/index.ts | 4 +- .../src/yamlLanguageService.ts | 4 +- 11 files changed, 51 insertions(+), 43 deletions(-) diff --git a/server/gx-workflow-ls-format2/src/languageService.ts b/server/gx-workflow-ls-format2/src/languageService.ts index bfffb2a..1ec5294 100644 --- a/server/gx-workflow-ls-format2/src/languageService.ts +++ b/server/gx-workflow-ls-format2/src/languageService.ts @@ -4,14 +4,14 @@ import { FormattingOptions, TextEdit, WorkflowDocument, - WorkflowLanguageService, + LanguageServiceBase, Position, Hover, CompletionList, Diagnostic, WorkflowValidator, } from "@gxwf/server-common/src/languageTypes"; -import { LanguageService, getLanguageService } from "@gxwf/yaml-language-service/src/yamlLanguageService"; +import { YAMLLanguageService, getLanguageService } from "@gxwf/yaml-language-service/src/yamlLanguageService"; import { GxFormat2WorkflowDocument } from "./gxFormat2WorkflowDocument"; import { GalaxyWorkflowFormat2SchemaLoader } from "./schema"; import { GxFormat2CompletionService } from "./services/completionService"; @@ -22,8 +22,8 @@ import { GxFormat2SchemaValidationService, WorkflowValidationService } from "./s * A wrapper around the YAML Language Service to support language features * for gxformat2 Galaxy workflow files. */ -export class GxFormat2WorkflowLanguageService extends WorkflowLanguageService { - private _yamlLanguageService: LanguageService; +export class GxFormat2WorkflowLanguageService extends LanguageServiceBase { + private _yamlLanguageService: YAMLLanguageService; private _schemaLoader: GalaxyWorkflowFormat2SchemaLoader; private _hoverService: GxFormat2HoverService; private _completionService: GxFormat2CompletionService; @@ -40,7 +40,7 @@ export class GxFormat2WorkflowLanguageService extends WorkflowLanguageService { ]; } - public override parseWorkflowDocument(document: TextDocument): WorkflowDocument { + public override parseDocument(document: TextDocument): WorkflowDocument { const yamlDocument = this._yamlLanguageService.parseYAMLDocument(document); return new GxFormat2WorkflowDocument(document, yamlDocument); } diff --git a/server/gx-workflow-ls-native/src/languageService.ts b/server/gx-workflow-ls-native/src/languageService.ts index 33c616d..cdb0b27 100644 --- a/server/gx-workflow-ls-native/src/languageService.ts +++ b/server/gx-workflow-ls-native/src/languageService.ts @@ -17,7 +17,7 @@ import { TextDocument, TextEdit, WorkflowDocument, - WorkflowLanguageService, + LanguageServiceBase, } from "@gxwf/server-common/src/languageTypes"; import NativeWorkflowSchema from "../../../workflow-languages/schemas/native.schema.json"; import { NativeWorkflowDocument } from "./nativeWorkflowDocument"; @@ -26,7 +26,7 @@ import { NativeWorkflowDocument } from "./nativeWorkflowDocument"; * A wrapper around the JSON Language Service to support language features * for native Galaxy workflow files AKA '.ga' workflows. */ -export class NativeWorkflowLanguageService extends WorkflowLanguageService { +export class NativeWorkflowLanguageService extends LanguageServiceBase { private _jsonLanguageService: LanguageService; private _documentSettings: DocumentLanguageSettings = { schemaValidation: "error" }; @@ -42,7 +42,7 @@ export class NativeWorkflowLanguageService extends WorkflowLanguageService { return NativeWorkflowSchema; } - public override parseWorkflowDocument(document: TextDocument): WorkflowDocument { + public override parseDocument(document: TextDocument): WorkflowDocument { const jsonDocument = this._jsonLanguageService.parseJSONDocument(document); return new NativeWorkflowDocument(document, jsonDocument); } diff --git a/server/packages/server-common/src/languageTypes.ts b/server/packages/server-common/src/languageTypes.ts index 09c8fe9..a7bb292 100644 --- a/server/packages/server-common/src/languageTypes.ts +++ b/server/packages/server-common/src/languageTypes.ts @@ -50,6 +50,8 @@ import { import { WorkflowDocument } from "./models/workflowDocument"; import { WorkflowDocuments } from "./models/workflowDocuments"; import { GalaxyWorkflowLanguageServer } from "./server"; +import { ASTNodeManager } from "./ast/nodeManager"; +import { URI } from "vscode-uri"; export { TextDocument, @@ -116,9 +118,9 @@ export interface ValidationRule { /** * Validates the given workflow document and provides diagnostics according * to this rule. - * @param workflowDocument The workflow document + * @param documentContext The workflow document */ - validate(workflowDocument: WorkflowDocument): Promise; + validate(documentContext: DocumentContext): Promise; } /** @@ -140,31 +142,37 @@ export interface WorkflowValidator { doValidation(workflowDocument: WorkflowDocument): Promise; } +/** + * Provides information about a processed text document. + */ +export interface DocumentContext { + uri: URI; + textDocument: TextDocument; + nodeManager: ASTNodeManager; +} + /** * Abstract service defining the base functionality that a workflow language must * implement to provide assistance for workflow documents editing. */ -export abstract class WorkflowLanguageService { +export abstract class LanguageServiceBase { public abstract format(document: TextDocument, range: Range, options: FormattingOptions): TextEdit[]; - public abstract parseWorkflowDocument(document: TextDocument): WorkflowDocument; - public abstract doHover(workflowDocument: WorkflowDocument, position: Position): Promise; - public abstract doComplete(workflowDocument: WorkflowDocument, position: Position): Promise; + public abstract parseDocument(document: TextDocument): T; + public abstract doHover(documentContext: T, position: Position): Promise; + public abstract doComplete(documentContext: T, position: Position): Promise; - /** Performs basic syntax and semantic validation based on the workflow schema. */ - protected abstract doValidation(workflowDocument: WorkflowDocument): Promise; + /** Performs basic syntax and semantic validation based on the document schema. */ + protected abstract doValidation(documentContext: T): Promise; /** * Validates the document and reports all the diagnostics found. * An optional validation profile can be used to provide additional custom diagnostics. */ - public async validate( - workflowDocument: WorkflowDocument, - useProfile: ValidationProfile | null = null - ): Promise { - const diagnostics = await this.doValidation(workflowDocument); + public async validate(documentContext: T, useProfile?: ValidationProfile): Promise { + const diagnostics = await this.doValidation(documentContext); if (useProfile) { useProfile.rules.forEach(async (validationRule) => { - const contributedDiagnostics = await validationRule.validate(workflowDocument); + const contributedDiagnostics = await validationRule.validate(documentContext); diagnostics.push(...contributedDiagnostics); }); } @@ -175,13 +183,13 @@ export abstract class WorkflowLanguageService { export abstract class ServerContext { protected connection: Connection; protected workflowDocuments: WorkflowDocuments; - protected languageService: WorkflowLanguageService; + protected workflowLanguageService: LanguageServiceBase; protected server: GalaxyWorkflowLanguageServer; constructor(server: GalaxyWorkflowLanguageServer) { this.server = server; this.workflowDocuments = server.workflowDocuments; - this.languageService = server.languageService; + this.workflowLanguageService = server.workflowLanguageService; this.connection = server.connection; } } diff --git a/server/packages/server-common/src/models/workflowDocument.ts b/server/packages/server-common/src/models/workflowDocument.ts index ab595b3..693b8f0 100644 --- a/server/packages/server-common/src/models/workflowDocument.ts +++ b/server/packages/server-common/src/models/workflowDocument.ts @@ -1,4 +1,4 @@ -import { TextDocument } from "../languageTypes"; +import { DocumentContext, TextDocument } from "../languageTypes"; import { URI } from "vscode-uri"; import { ParsedDocument } from "../ast/types"; import { ASTNodeManager } from "../ast/nodeManager"; @@ -6,7 +6,7 @@ import { ASTNodeManager } from "../ast/nodeManager"; /** * This class contains information about workflow semantics. */ -export abstract class WorkflowDocument { +export abstract class WorkflowDocument implements DocumentContext { protected _textDocument: TextDocument; protected _documentUri: URI; protected _parsedDocument: ParsedDocument; diff --git a/server/packages/server-common/src/providers/completionProvider.ts b/server/packages/server-common/src/providers/completionProvider.ts index a53c054..e32bb6c 100644 --- a/server/packages/server-common/src/providers/completionProvider.ts +++ b/server/packages/server-common/src/providers/completionProvider.ts @@ -14,7 +14,7 @@ export class CompletionProvider extends Provider { private async onCompletion(params: CompletionParams): Promise { const workflowDocument = this.workflowDocuments.get(params.textDocument.uri); if (workflowDocument) { - const result = await this.languageService.doComplete(workflowDocument, params.position); + const result = await this.workflowLanguageService.doComplete(workflowDocument, params.position); return result; } return null; diff --git a/server/packages/server-common/src/providers/formattingProvider.ts b/server/packages/server-common/src/providers/formattingProvider.ts index 1966c2f..6775458 100644 --- a/server/packages/server-common/src/providers/formattingProvider.ts +++ b/server/packages/server-common/src/providers/formattingProvider.ts @@ -32,7 +32,7 @@ export class FormattingProvider extends Provider { private onFormat(documentUri: string, range: Range | undefined, options: FormattingOptions): TextEdit[] { const workflowDocument = this.workflowDocuments.get(documentUri); if (workflowDocument) { - const edits = this.languageService.format( + const edits = this.workflowLanguageService.format( workflowDocument.textDocument, range ?? this.getFullRange(workflowDocument.textDocument), options diff --git a/server/packages/server-common/src/providers/hover/hoverProvider.ts b/server/packages/server-common/src/providers/hover/hoverProvider.ts index 094b924..8e96123 100644 --- a/server/packages/server-common/src/providers/hover/hoverProvider.ts +++ b/server/packages/server-common/src/providers/hover/hoverProvider.ts @@ -23,7 +23,7 @@ export class HoverProvider extends Provider { if (!workflowDocument) { return null; } - const hover = await this.languageService.doHover(workflowDocument, params.position); + const hover = await this.workflowLanguageService.doHover(workflowDocument, params.position); if (!hover) { return null; } diff --git a/server/packages/server-common/src/server.ts b/server/packages/server-common/src/server.ts index e0722fd..00e1df2 100644 --- a/server/packages/server-common/src/server.ts +++ b/server/packages/server-common/src/server.ts @@ -6,7 +6,7 @@ import { TextDocuments, WorkspaceFolder, } from "vscode-languageserver"; -import { TextDocument, WorkflowDocument, WorkflowLanguageService } from "./languageTypes"; +import { TextDocument, WorkflowDocument, LanguageServiceBase } from "./languageTypes"; import { WorkflowDocuments } from "./models/workflowDocuments"; import { FormattingProvider } from "./providers/formattingProvider"; import { HoverProvider } from "./providers/hover/hoverProvider"; @@ -18,7 +18,7 @@ import { CompletionProvider } from "./providers/completionProvider"; import { ValidationProfiles } from "./providers/validation/profiles"; export class GalaxyWorkflowLanguageServer { - public readonly languageService: WorkflowLanguageService; + public readonly workflowLanguageService: LanguageServiceBase; public readonly configService: ConfigService; public readonly documents = new TextDocuments(TextDocument); public readonly workflowDocuments = new WorkflowDocuments(); @@ -26,9 +26,9 @@ export class GalaxyWorkflowLanguageServer { constructor( public readonly connection: Connection, - languageService: WorkflowLanguageService + workflowLanguageService: LanguageServiceBase ) { - this.languageService = languageService; + this.workflowLanguageService = workflowLanguageService; this.configService = new ConfigService(connection, () => this.onConfigurationChanged()); // Track open, change and close text document events this.trackDocumentChanges(connection); @@ -88,9 +88,9 @@ export class GalaxyWorkflowLanguageServer { * An event that fires when a workflow document has been opened or the content changes. */ private onDidChangeContent(textDocument: TextDocument): void { - const workflowDocument = this.languageService.parseWorkflowDocument(textDocument); + const workflowDocument = this.workflowLanguageService.parseDocument(textDocument); this.workflowDocuments.addOrReplaceWorkflowDocument(workflowDocument); - this.validate(workflowDocument); + this.validateWorkflow(workflowDocument); } private onDidClose(textDocument: TextDocument): void { @@ -101,7 +101,7 @@ export class GalaxyWorkflowLanguageServer { private onConfigurationChanged(): void { this.workflowDocuments.all().forEach((workflowDocument) => { - this.validate(workflowDocument); + this.validateWorkflow(workflowDocument); }); } @@ -109,13 +109,13 @@ export class GalaxyWorkflowLanguageServer { this.workflowDocuments.dispose(); } - private async validate(workflowDocument: WorkflowDocument): Promise { + private async validateWorkflow(workflowDocument: WorkflowDocument): Promise { if (WorkflowDocuments.schemesToSkip.includes(workflowDocument.uri.scheme)) { return; } const settings = await this.configService.getDocumentSettings(workflowDocument.textDocument.uri); const validationProfile = ValidationProfiles.get(settings.validation.profile); - this.languageService.validate(workflowDocument, validationProfile).then((diagnostics) => { + this.workflowLanguageService.validate(workflowDocument, validationProfile).then((diagnostics) => { this.connection.sendDiagnostics({ uri: workflowDocument.textDocument.uri, diagnostics }); }); } diff --git a/server/packages/server-common/src/services/cleanWorkflow.ts b/server/packages/server-common/src/services/cleanWorkflow.ts index 1050960..23b5204 100644 --- a/server/packages/server-common/src/services/cleanWorkflow.ts +++ b/server/packages/server-common/src/services/cleanWorkflow.ts @@ -48,7 +48,7 @@ export class CleanWorkflowService extends ServiceBase { params: CleanWorkflowContentsParams ): Promise { const tempDocument = this.createTempWorkflowDocumentWithContents(params.contents); - const workflowDocument = this.languageService.parseWorkflowDocument(tempDocument); + const workflowDocument = this.workflowLanguageService.parseDocument(tempDocument); if (workflowDocument) { return await this.cleanWorkflowContentsResult(workflowDocument); } diff --git a/server/packages/yaml-language-service/src/index.ts b/server/packages/yaml-language-service/src/index.ts index 82908d9..9d8fd89 100644 --- a/server/packages/yaml-language-service/src/index.ts +++ b/server/packages/yaml-language-service/src/index.ts @@ -1,4 +1,4 @@ import { YAMLDocument } from "./parser"; -import { LanguageService, getLanguageService } from "./yamlLanguageService"; +import { YAMLLanguageService, getLanguageService } from "./yamlLanguageService"; -export { YAMLDocument, LanguageService, getLanguageService }; +export { YAMLDocument, YAMLLanguageService, getLanguageService }; diff --git a/server/packages/yaml-language-service/src/yamlLanguageService.ts b/server/packages/yaml-language-service/src/yamlLanguageService.ts index 1a23010..f87d7c9 100644 --- a/server/packages/yaml-language-service/src/yamlLanguageService.ts +++ b/server/packages/yaml-language-service/src/yamlLanguageService.ts @@ -15,14 +15,14 @@ export interface CustomFormatterOptions { lineWidth?: number; } -export interface LanguageService { +export interface YAMLLanguageService { configure(settings: LanguageSettings): void; parseYAMLDocument(document: TextDocument): YAMLDocument; doValidation(yamlDocument: YAMLDocument): Promise; doFormat(document: TextDocument, options: FormattingOptions & CustomFormatterOptions): TextEdit[]; } -export function getLanguageService(): LanguageService { +export function getLanguageService(): YAMLLanguageService { const formatter = new YAMLFormatter(); const validator = new YAMLValidation(); return { From c747badf10c4f3608836212d3205c8ae853c1435 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Thu, 26 Oct 2023 21:33:11 +0200 Subject: [PATCH 02/98] Extract base and add WorkflowTestsDocument --- .../server-common/src/models/document.ts | 34 +++++++++++++++++++ .../src/models/workflowDocument.ts | 34 +++---------------- .../src/models/workflowTestsDocument.ts | 8 +++++ 3 files changed, 46 insertions(+), 30 deletions(-) create mode 100644 server/packages/server-common/src/models/document.ts create mode 100644 server/packages/server-common/src/models/workflowTestsDocument.ts diff --git a/server/packages/server-common/src/models/document.ts b/server/packages/server-common/src/models/document.ts new file mode 100644 index 0000000..3f942a5 --- /dev/null +++ b/server/packages/server-common/src/models/document.ts @@ -0,0 +1,34 @@ +import { DocumentContext, TextDocument } from "../languageTypes"; +import { URI } from "vscode-uri"; +import { ParsedDocument } from "../ast/types"; +import { ASTNodeManager } from "../ast/nodeManager"; + +/** + * This class contains basic common handling logic for any kind of known document. + */ +export abstract class DocumentBase implements DocumentContext { + protected _textDocument: TextDocument; + protected _documentUri: URI; + protected _parsedDocument: ParsedDocument; + protected _nodeManager: ASTNodeManager; + + constructor(textDocument: TextDocument, parsedDocument: ParsedDocument) { + this._textDocument = textDocument; + this._parsedDocument = parsedDocument; + this._nodeManager = new ASTNodeManager(textDocument, parsedDocument); + this._documentUri = URI.parse(this._textDocument.uri); + } + + public get uri(): URI { + return this._documentUri; + } + + public get textDocument(): TextDocument { + return this._textDocument; + } + + /** Abstract Syntax Tree Node Manager associated with this document. */ + public get nodeManager(): ASTNodeManager { + return this._nodeManager; + } +} diff --git a/server/packages/server-common/src/models/workflowDocument.ts b/server/packages/server-common/src/models/workflowDocument.ts index 693b8f0..6607556 100644 --- a/server/packages/server-common/src/models/workflowDocument.ts +++ b/server/packages/server-common/src/models/workflowDocument.ts @@ -1,34 +1,8 @@ -import { DocumentContext, TextDocument } from "../languageTypes"; -import { URI } from "vscode-uri"; -import { ParsedDocument } from "../ast/types"; -import { ASTNodeManager } from "../ast/nodeManager"; +import { DocumentBase } from "./document"; /** - * This class contains information about workflow semantics. + * This class abstracts the common logic of workflow documents. */ -export abstract class WorkflowDocument implements DocumentContext { - protected _textDocument: TextDocument; - protected _documentUri: URI; - protected _parsedDocument: ParsedDocument; - protected _nodeManager: ASTNodeManager; - - constructor(textDocument: TextDocument, parsedDocument: ParsedDocument) { - this._textDocument = textDocument; - this._parsedDocument = parsedDocument; - this._nodeManager = new ASTNodeManager(textDocument, parsedDocument); - this._documentUri = URI.parse(this._textDocument.uri); - } - - public get uri(): URI { - return this._documentUri; - } - - public get textDocument(): TextDocument { - return this._textDocument; - } - - /** Abstract Syntax Tree Node Manager associated with this document. */ - public get nodeManager(): ASTNodeManager { - return this._nodeManager; - } +export abstract class WorkflowDocument extends DocumentBase { + // TODO: add workflow document specific logic } diff --git a/server/packages/server-common/src/models/workflowTestsDocument.ts b/server/packages/server-common/src/models/workflowTestsDocument.ts new file mode 100644 index 0000000..0260b0b --- /dev/null +++ b/server/packages/server-common/src/models/workflowTestsDocument.ts @@ -0,0 +1,8 @@ +import { DocumentBase } from "./document"; + +/** + * This class contains information about a document containing workflow tests. + */ +export abstract class WorkflowTestsDocument extends DocumentBase { + // TODO: implement workflow test document specific logic +} From 1d76066c5156c5d471ea20c9757488c33699382f Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Thu, 26 Oct 2023 21:49:58 +0200 Subject: [PATCH 03/98] Refactor WorkflowDocuments into DocumentsCache Nothing particularly workflow related about it. --- .../server-common/src/languageTypes.ts | 6 +-- .../src/models/documentsCache.ts | 43 +++++++++++++++++++ .../src/models/workflowDocuments.ts | 42 ------------------ .../src/providers/completionProvider.ts | 2 +- .../src/providers/formattingProvider.ts | 2 +- .../src/providers/hover/hoverProvider.ts | 2 +- .../src/providers/symbolsProvider.ts | 2 +- server/packages/server-common/src/server.ts | 14 +++--- .../src/services/cleanWorkflow.ts | 2 +- 9 files changed, 58 insertions(+), 57 deletions(-) create mode 100644 server/packages/server-common/src/models/documentsCache.ts delete mode 100644 server/packages/server-common/src/models/workflowDocuments.ts diff --git a/server/packages/server-common/src/languageTypes.ts b/server/packages/server-common/src/languageTypes.ts index a7bb292..1558399 100644 --- a/server/packages/server-common/src/languageTypes.ts +++ b/server/packages/server-common/src/languageTypes.ts @@ -48,7 +48,7 @@ import { DocumentSymbolParams, } from "vscode-languageserver/browser"; import { WorkflowDocument } from "./models/workflowDocument"; -import { WorkflowDocuments } from "./models/workflowDocuments"; +import { DocumentsCache } from "./models/documentsCache"; import { GalaxyWorkflowLanguageServer } from "./server"; import { ASTNodeManager } from "./ast/nodeManager"; import { URI } from "vscode-uri"; @@ -182,13 +182,13 @@ export abstract class LanguageServiceBase { export abstract class ServerContext { protected connection: Connection; - protected workflowDocuments: WorkflowDocuments; + protected documentsCache: DocumentsCache; protected workflowLanguageService: LanguageServiceBase; protected server: GalaxyWorkflowLanguageServer; constructor(server: GalaxyWorkflowLanguageServer) { this.server = server; - this.workflowDocuments = server.workflowDocuments; + this.documentsCache = server.documentsCache; this.workflowLanguageService = server.workflowLanguageService; this.connection = server.connection; } diff --git a/server/packages/server-common/src/models/documentsCache.ts b/server/packages/server-common/src/models/documentsCache.ts new file mode 100644 index 0000000..c01432c --- /dev/null +++ b/server/packages/server-common/src/models/documentsCache.ts @@ -0,0 +1,43 @@ +import { DocumentContext } from "../languageTypes"; + +export class DocumentsCache { + private cache: Map; + + /** + * Document URI schemes that represent temporal or readonly documents + * that should not be cached. + */ + public static schemesToSkip = ["temp", "galaxy-clean-workflow"]; + + constructor() { + this.cache = new Map(); + } + + public get(documentUri: string): DocumentContext | undefined { + return this.cache.get(documentUri); + } + + public all(): DocumentContext[] { + return Array.from(this.cache.values()); + } + + public addOrReplaceDocument(documentContext: DocumentContext): void { + if (DocumentsCache.schemesToSkip.includes(documentContext.uri.scheme)) { + return; + } + this.cache.set(documentContext.uri.toString(), documentContext); + // console.debug("Registering: ", document.uri.toString()); + // console.debug("Files registered: ", this.cache.size); + } + + public removeDocument(documentUri: string): void { + this.cache.delete(documentUri); + // console.debug("Un-registering: ", documentUri); + // console.debug("Files registered: ", this.cache.size); + } + + public dispose(): void { + this.cache.clear(); + //console.debug("Documents cache cleared"); + } +} diff --git a/server/packages/server-common/src/models/workflowDocuments.ts b/server/packages/server-common/src/models/workflowDocuments.ts deleted file mode 100644 index 3f3f3e0..0000000 --- a/server/packages/server-common/src/models/workflowDocuments.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { WorkflowDocument } from "./workflowDocument"; - -export class WorkflowDocuments { - private _documentsCache: Map; - - /** - * Workflow document URI schemes that represent temporal or readonly documents. - */ - public static schemesToSkip = ["temp", "galaxy-clean-workflow"]; - - constructor() { - this._documentsCache = new Map(); - } - - public get(documentUri: string): WorkflowDocument | undefined { - return this._documentsCache.get(documentUri); - } - - public all(): WorkflowDocument[] { - return Array.from(this._documentsCache.values()); - } - - public addOrReplaceWorkflowDocument(document: WorkflowDocument): void { - if (WorkflowDocuments.schemesToSkip.includes(document.uri.scheme)) { - return; - } - this._documentsCache.set(document.uri.toString(), document); - // console.debug("Registering: ", document.uri.toString()); - // console.debug("workflow files registered: ", this._documentsCache.size); - } - - public removeWorkflowDocument(documentUri: string): void { - this._documentsCache.delete(documentUri); - // console.debug("Un-registering: ", documentUri); - // console.debug("workflow files registered: ", this._documentsCache.size); - } - - public dispose(): void { - this._documentsCache.clear(); - //console.debug("workflow document cache cleared"); - } -} diff --git a/server/packages/server-common/src/providers/completionProvider.ts b/server/packages/server-common/src/providers/completionProvider.ts index e32bb6c..9e0d710 100644 --- a/server/packages/server-common/src/providers/completionProvider.ts +++ b/server/packages/server-common/src/providers/completionProvider.ts @@ -12,7 +12,7 @@ export class CompletionProvider extends Provider { this.connection.onCompletion(async (params) => this.onCompletion(params)); } private async onCompletion(params: CompletionParams): Promise { - const workflowDocument = this.workflowDocuments.get(params.textDocument.uri); + const workflowDocument = this.documentsCache.get(params.textDocument.uri); if (workflowDocument) { const result = await this.workflowLanguageService.doComplete(workflowDocument, params.position); return result; diff --git a/server/packages/server-common/src/providers/formattingProvider.ts b/server/packages/server-common/src/providers/formattingProvider.ts index 6775458..70184e0 100644 --- a/server/packages/server-common/src/providers/formattingProvider.ts +++ b/server/packages/server-common/src/providers/formattingProvider.ts @@ -30,7 +30,7 @@ export class FormattingProvider extends Provider { } private onFormat(documentUri: string, range: Range | undefined, options: FormattingOptions): TextEdit[] { - const workflowDocument = this.workflowDocuments.get(documentUri); + const workflowDocument = this.documentsCache.get(documentUri); if (workflowDocument) { const edits = this.workflowLanguageService.format( workflowDocument.textDocument, diff --git a/server/packages/server-common/src/providers/hover/hoverProvider.ts b/server/packages/server-common/src/providers/hover/hoverProvider.ts index 8e96123..91b18ce 100644 --- a/server/packages/server-common/src/providers/hover/hoverProvider.ts +++ b/server/packages/server-common/src/providers/hover/hoverProvider.ts @@ -19,7 +19,7 @@ export class HoverProvider extends Provider { } private async onHover(params: HoverParams): Promise { - const workflowDocument = this.workflowDocuments.get(params.textDocument.uri); + const workflowDocument = this.documentsCache.get(params.textDocument.uri); if (!workflowDocument) { return null; } diff --git a/server/packages/server-common/src/providers/symbolsProvider.ts b/server/packages/server-common/src/providers/symbolsProvider.ts index 1d1f5a1..18a2a9d 100644 --- a/server/packages/server-common/src/providers/symbolsProvider.ts +++ b/server/packages/server-common/src/providers/symbolsProvider.ts @@ -16,7 +16,7 @@ export class SymbolsProvider extends Provider { } public onDocumentSymbol(params: DocumentSymbolParams): DocumentSymbol[] { - const workflowDocument = this.workflowDocuments.get(params.textDocument.uri); + const workflowDocument = this.documentsCache.get(params.textDocument.uri); if (workflowDocument) { const symbols = this.getSymbols(workflowDocument); return symbols; diff --git a/server/packages/server-common/src/server.ts b/server/packages/server-common/src/server.ts index 00e1df2..d18d648 100644 --- a/server/packages/server-common/src/server.ts +++ b/server/packages/server-common/src/server.ts @@ -7,7 +7,7 @@ import { WorkspaceFolder, } from "vscode-languageserver"; import { TextDocument, WorkflowDocument, LanguageServiceBase } from "./languageTypes"; -import { WorkflowDocuments } from "./models/workflowDocuments"; +import { DocumentsCache } from "./models/documentsCache"; import { FormattingProvider } from "./providers/formattingProvider"; import { HoverProvider } from "./providers/hover/hoverProvider"; import { SymbolsProvider } from "./providers/symbolsProvider"; @@ -21,7 +21,7 @@ export class GalaxyWorkflowLanguageServer { public readonly workflowLanguageService: LanguageServiceBase; public readonly configService: ConfigService; public readonly documents = new TextDocuments(TextDocument); - public readonly workflowDocuments = new WorkflowDocuments(); + public readonly documentsCache = new DocumentsCache(); protected workspaceFolders: WorkspaceFolder[] | null | undefined; constructor( @@ -89,28 +89,28 @@ export class GalaxyWorkflowLanguageServer { */ private onDidChangeContent(textDocument: TextDocument): void { const workflowDocument = this.workflowLanguageService.parseDocument(textDocument); - this.workflowDocuments.addOrReplaceWorkflowDocument(workflowDocument); + this.documentsCache.addOrReplaceDocument(workflowDocument); this.validateWorkflow(workflowDocument); } private onDidClose(textDocument: TextDocument): void { - this.workflowDocuments.removeWorkflowDocument(textDocument.uri); + this.documentsCache.removeDocument(textDocument.uri); this.configService.onDocumentClose(textDocument.uri); this.clearValidation(textDocument); } private onConfigurationChanged(): void { - this.workflowDocuments.all().forEach((workflowDocument) => { + this.documentsCache.all().forEach((workflowDocument) => { this.validateWorkflow(workflowDocument); }); } private cleanup(): void { - this.workflowDocuments.dispose(); + this.documentsCache.dispose(); } private async validateWorkflow(workflowDocument: WorkflowDocument): Promise { - if (WorkflowDocuments.schemesToSkip.includes(workflowDocument.uri.scheme)) { + if (DocumentsCache.schemesToSkip.includes(workflowDocument.uri.scheme)) { return; } const settings = await this.configService.getDocumentSettings(workflowDocument.textDocument.uri); diff --git a/server/packages/server-common/src/services/cleanWorkflow.ts b/server/packages/server-common/src/services/cleanWorkflow.ts index 23b5204..72e14a7 100644 --- a/server/packages/server-common/src/services/cleanWorkflow.ts +++ b/server/packages/server-common/src/services/cleanWorkflow.ts @@ -65,7 +65,7 @@ export class CleanWorkflowService extends ServiceBase { params: CleanWorkflowDocumentParams ): Promise { try { - const workflowDocument = this.workflowDocuments.get(params.uri); + const workflowDocument = this.documentsCache.get(params.uri); if (workflowDocument) { const settings = await this.server.configService.getDocumentSettings(workflowDocument.textDocument.uri); const edits = this.getTextEditsToCleanWorkflow(workflowDocument, settings.cleaning.cleanableProperties); From c83ee7c13152f2894ed651b03ece24efe070726b Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sat, 28 Oct 2023 21:33:58 +0200 Subject: [PATCH 04/98] Expose language id in document context --- server/packages/server-common/src/languageTypes.ts | 3 +++ server/packages/server-common/src/models/document.ts | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/server/packages/server-common/src/languageTypes.ts b/server/packages/server-common/src/languageTypes.ts index 1558399..d2e8c97 100644 --- a/server/packages/server-common/src/languageTypes.ts +++ b/server/packages/server-common/src/languageTypes.ts @@ -146,6 +146,7 @@ export interface WorkflowValidator { * Provides information about a processed text document. */ export interface DocumentContext { + languageId: string; uri: URI; textDocument: TextDocument; nodeManager: ASTNodeManager; @@ -156,6 +157,8 @@ export interface DocumentContext { * implement to provide assistance for workflow documents editing. */ export abstract class LanguageServiceBase { + constructor(public readonly languageId: string) {} + public abstract format(document: TextDocument, range: Range, options: FormattingOptions): TextEdit[]; public abstract parseDocument(document: TextDocument): T; public abstract doHover(documentContext: T, position: Position): Promise; diff --git a/server/packages/server-common/src/models/document.ts b/server/packages/server-common/src/models/document.ts index 3f942a5..45595a7 100644 --- a/server/packages/server-common/src/models/document.ts +++ b/server/packages/server-common/src/models/document.ts @@ -19,6 +19,10 @@ export abstract class DocumentBase implements DocumentContext { this._documentUri = URI.parse(this._textDocument.uri); } + public get languageId(): string { + return this._textDocument.languageId; + } + public get uri(): URI { return this._documentUri; } From 76eceb7eadecb3993c58287d25db6d4d4e235da1 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sat, 28 Oct 2023 21:36:00 +0200 Subject: [PATCH 05/98] Add package for workflow tests language --- .../package.json | 18 +++++++ .../src/document.ts | 18 +++++++ .../src/languageService.ts | 50 +++++++++++++++++++ .../tsconfig.json | 15 ++++++ 4 files changed, 101 insertions(+) create mode 100644 server/packages/workflow-tests-language-service/package.json create mode 100644 server/packages/workflow-tests-language-service/src/document.ts create mode 100644 server/packages/workflow-tests-language-service/src/languageService.ts create mode 100644 server/packages/workflow-tests-language-service/tsconfig.json diff --git a/server/packages/workflow-tests-language-service/package.json b/server/packages/workflow-tests-language-service/package.json new file mode 100644 index 0000000..5d2176b --- /dev/null +++ b/server/packages/workflow-tests-language-service/package.json @@ -0,0 +1,18 @@ +{ + "name": "@gxwf/workflow-tests-language-service", + "version": "0.1.0", + "description": "Language service implementation for Galaxy workflow tests files. ", + "author": "davelopez", + "license": "MIT", + "dependencies": { + "@gxwf/server-common": "*", + "@gxwf/yaml-language-service": "*", + "vscode-languageserver": "^8.1.0", + "vscode-languageserver-textdocument": "^1.0.8", + "vscode-languageserver-types": "^3.17.3", + "vscode-uri": "^3.0.7" + }, + "scripts": { + "test": "jest" + } +} diff --git a/server/packages/workflow-tests-language-service/src/document.ts b/server/packages/workflow-tests-language-service/src/document.ts new file mode 100644 index 0000000..2fe3c09 --- /dev/null +++ b/server/packages/workflow-tests-language-service/src/document.ts @@ -0,0 +1,18 @@ +import { TextDocument, WorkflowTestsDocument } from "@gxwf/server-common/src/languageTypes"; +import { YAMLDocument } from "@gxwf/yaml-language-service/src"; + +/** + * This class represents (YAML) document containing tests definitions for a Galaxy workflow. + */ +export class GxWorkflowTestsDocument extends WorkflowTestsDocument { + private _yamlDocument: YAMLDocument; + + constructor(textDocument: TextDocument, yamlDocument: YAMLDocument) { + super(textDocument, yamlDocument); + this._yamlDocument = yamlDocument; + } + + public get yamlDocument(): YAMLDocument { + return this._yamlDocument; + } +} diff --git a/server/packages/workflow-tests-language-service/src/languageService.ts b/server/packages/workflow-tests-language-service/src/languageService.ts new file mode 100644 index 0000000..72e4425 --- /dev/null +++ b/server/packages/workflow-tests-language-service/src/languageService.ts @@ -0,0 +1,50 @@ +import { + TextDocument, + Range, + FormattingOptions, + TextEdit, + LanguageServiceBase, + Position, + Hover, + CompletionList, + Diagnostic, + WorkflowTestsDocument, +} from "@gxwf/server-common/src/languageTypes"; +import { YAMLLanguageService, getLanguageService } from "@gxwf/yaml-language-service/src/yamlLanguageService"; +import { GxWorkflowTestsDocument } from "./document"; + +export class GxWorkflowTestsLanguageService extends LanguageServiceBase { + private _yamlLanguageService: YAMLLanguageService; + + constructor() { + super("gxwftests"); + this._yamlLanguageService = getLanguageService(); + } + + public override parseDocument(document: TextDocument): GxWorkflowTestsDocument { + const yamlDocument = this._yamlLanguageService.parseYAMLDocument(document); + return new GxWorkflowTestsDocument(document, yamlDocument); + } + + public override format(document: TextDocument, _: Range, options: FormattingOptions): TextEdit[] { + return this._yamlLanguageService.doFormat(document, options); + } + + public override doHover(documentContext: WorkflowTestsDocument, position: Position): Promise { + // TODO: Implement hover + return Promise.resolve(null); + } + + public override doComplete( + documentContext: WorkflowTestsDocument, + position: Position + ): Promise { + // TODO: Implement completion + return Promise.resolve(null); + } + + protected override async doValidation(documentContext: WorkflowTestsDocument): Promise { + // TODO: Implement validation + return Promise.resolve([]); + } +} diff --git a/server/packages/workflow-tests-language-service/tsconfig.json b/server/packages/workflow-tests-language-service/tsconfig.json new file mode 100644 index 0000000..0ed94f1 --- /dev/null +++ b/server/packages/workflow-tests-language-service/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "es2019", + "lib": ["ES2019", "WebWorker"], + "module": "commonjs", + "moduleResolution": "node", + "resolveJsonModule": true, + "esModuleInterop": true, + "sourceMap": true, + "strict": true, + "composite": true + }, + "include": ["src"], + "exclude": ["node_modules", ".vscode-test-web"] +} From 5a3327f4edf1ee7aed3e9cc3924d7440b5f66408 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sat, 28 Oct 2023 21:39:20 +0200 Subject: [PATCH 06/98] Integrate workflow tests language service + refactor --- server/gx-workflow-ls-format2/package.json | 1 + .../src/browser/server.ts | 6 ++-- .../src/languageService.ts | 17 +++++----- .../gx-workflow-ls-format2/src/node/server.ts | 6 ++-- server/gx-workflow-ls-native/package.json | 3 +- .../src/browser/server.ts | 6 ++-- .../src/languageService.ts | 2 +- .../gx-workflow-ls-native/src/node/server.ts | 4 ++- server/package-lock.json | 32 +++++++++++++++++ .../server-common/src/languageTypes.ts | 12 ++++--- .../src/providers/completionProvider.ts | 7 ++-- .../src/providers/formattingProvider.ts | 11 +++--- .../src/providers/hover/hoverProvider.ts | 9 ++--- .../src/providers/symbolsProvider.ts | 18 +++++----- server/packages/server-common/src/server.ts | 34 +++++++++++++------ .../src/services/cleanWorkflow.ts | 8 +++-- 16 files changed, 122 insertions(+), 54 deletions(-) diff --git a/server/gx-workflow-ls-format2/package.json b/server/gx-workflow-ls-format2/package.json index 94e422d..918d820 100644 --- a/server/gx-workflow-ls-format2/package.json +++ b/server/gx-workflow-ls-format2/package.json @@ -6,6 +6,7 @@ "license": "MIT", "dependencies": { "@gxwf/server-common": "*", + "@gxwf/workflow-tests-language-service": "*", "@gxwf/yaml-language-service": "*", "vscode-languageserver": "^8.1.0", "vscode-languageserver-textdocument": "^1.0.8", diff --git a/server/gx-workflow-ls-format2/src/browser/server.ts b/server/gx-workflow-ls-format2/src/browser/server.ts index 8e4d705..141f999 100644 --- a/server/gx-workflow-ls-format2/src/browser/server.ts +++ b/server/gx-workflow-ls-format2/src/browser/server.ts @@ -1,5 +1,6 @@ import { createConnection, BrowserMessageReader, BrowserMessageWriter } from "vscode-languageserver/browser"; import { GalaxyWorkflowLanguageServer } from "@gxwf/server-common/src/server"; +import { GxWorkflowTestsLanguageService } from "@gxwf/workflow-tests-language-service/src/languageService"; import { GxFormat2WorkflowLanguageService } from "../languageService"; const messageReader = new BrowserMessageReader(self); @@ -7,6 +8,7 @@ const messageWriter = new BrowserMessageWriter(self); const connection = createConnection(messageReader, messageWriter); -const languageService = new GxFormat2WorkflowLanguageService(); -const server = new GalaxyWorkflowLanguageServer(connection, languageService); +const workflowLanguageService = new GxFormat2WorkflowLanguageService(); +const workflowTestsLanguageService = new GxWorkflowTestsLanguageService(); +const server = new GalaxyWorkflowLanguageServer(connection, workflowLanguageService, workflowTestsLanguageService); server.start(); diff --git a/server/gx-workflow-ls-format2/src/languageService.ts b/server/gx-workflow-ls-format2/src/languageService.ts index 1ec5294..fe4e942 100644 --- a/server/gx-workflow-ls-format2/src/languageService.ts +++ b/server/gx-workflow-ls-format2/src/languageService.ts @@ -28,8 +28,9 @@ export class GxFormat2WorkflowLanguageService extends LanguageServiceBase { - return this._hoverService.doHover(workflowDocument.textDocument, position, workflowDocument.nodeManager); + public override doHover(documentContext: WorkflowDocument, position: Position): Promise { + return this._hoverService.doHover(documentContext.textDocument, position, documentContext.nodeManager); } public override async doComplete( - workflowDocument: WorkflowDocument, + documentContext: WorkflowDocument, position: Position ): Promise { - return this._completionService.doComplete(workflowDocument.textDocument, position, workflowDocument.nodeManager); + return this._completionService.doComplete(documentContext.textDocument, position, documentContext.nodeManager); } - protected override async doValidation(workflowDocument: WorkflowDocument): Promise { - const format2WorkflowDocument = workflowDocument as GxFormat2WorkflowDocument; + protected override async doValidation(documentContext: WorkflowDocument): Promise { + const format2WorkflowDocument = documentContext as GxFormat2WorkflowDocument; const diagnostics = await this._yamlLanguageService.doValidation(format2WorkflowDocument.yamlDocument); for (const validator of this._validationServices) { - const results = await validator.doValidation(workflowDocument); + const results = await validator.doValidation(documentContext); diagnostics.push(...results); } return diagnostics; diff --git a/server/gx-workflow-ls-format2/src/node/server.ts b/server/gx-workflow-ls-format2/src/node/server.ts index 2ec4627..cc1c1e0 100644 --- a/server/gx-workflow-ls-format2/src/node/server.ts +++ b/server/gx-workflow-ls-format2/src/node/server.ts @@ -1,9 +1,11 @@ import { createConnection } from "vscode-languageserver/node"; import { GalaxyWorkflowLanguageServer } from "@gxwf/server-common/src/server"; import { GxFormat2WorkflowLanguageService } from "../languageService"; +import { GxWorkflowTestsLanguageService } from "@gxwf/workflow-tests-language-service/src/languageService"; const connection = createConnection(); -const languageService = new GxFormat2WorkflowLanguageService(); -const server = new GalaxyWorkflowLanguageServer(connection, languageService); +const workflowLanguageService = new GxFormat2WorkflowLanguageService(); +const workflowTestsLanguageService = new GxWorkflowTestsLanguageService(); +const server = new GalaxyWorkflowLanguageServer(connection, workflowLanguageService, workflowTestsLanguageService); server.start(); diff --git a/server/gx-workflow-ls-native/package.json b/server/gx-workflow-ls-native/package.json index 1b62fa4..ae97cdd 100644 --- a/server/gx-workflow-ls-native/package.json +++ b/server/gx-workflow-ls-native/package.json @@ -6,11 +6,12 @@ "license": "MIT", "dependencies": { "@gxwf/server-common": "*", + "@gxwf/workflow-tests-language-service": "*", "jsonc-parser": "^3.2.0", "vscode-json-languageservice": "^5.3.6", + "vscode-languageserver": "^8.1.0", "vscode-languageserver-textdocument": "^1.0.8", "vscode-languageserver-types": "^3.17.3", - "vscode-languageserver": "^8.1.0", "vscode-uri": "^3.0.7" }, "scripts": { diff --git a/server/gx-workflow-ls-native/src/browser/server.ts b/server/gx-workflow-ls-native/src/browser/server.ts index 4cf84d4..2f6c178 100644 --- a/server/gx-workflow-ls-native/src/browser/server.ts +++ b/server/gx-workflow-ls-native/src/browser/server.ts @@ -1,12 +1,14 @@ import { createConnection, BrowserMessageReader, BrowserMessageWriter } from "vscode-languageserver/browser"; import { NativeWorkflowLanguageService } from "../languageService"; import { GalaxyWorkflowLanguageServer } from "@gxwf/server-common/src/server"; +import { GxWorkflowTestsLanguageService } from "@gxwf/workflow-tests-language-service/src/languageService"; const messageReader = new BrowserMessageReader(self); const messageWriter = new BrowserMessageWriter(self); const connection = createConnection(messageReader, messageWriter); -const languageService = new NativeWorkflowLanguageService(); -const server = new GalaxyWorkflowLanguageServer(connection, languageService); +const workflowLanguageService = new NativeWorkflowLanguageService(); +const workflowTestsLanguageService = new GxWorkflowTestsLanguageService(); +const server = new GalaxyWorkflowLanguageServer(connection, workflowLanguageService, workflowTestsLanguageService); server.start(); diff --git a/server/gx-workflow-ls-native/src/languageService.ts b/server/gx-workflow-ls-native/src/languageService.ts index cdb0b27..cdaf99f 100644 --- a/server/gx-workflow-ls-native/src/languageService.ts +++ b/server/gx-workflow-ls-native/src/languageService.ts @@ -31,7 +31,7 @@ export class NativeWorkflowLanguageService extends LanguageServiceBase { export abstract class ServerContext { protected connection: Connection; protected documentsCache: DocumentsCache; - protected workflowLanguageService: LanguageServiceBase; protected server: GalaxyWorkflowLanguageServer; constructor(server: GalaxyWorkflowLanguageServer) { this.server = server; this.documentsCache = server.documentsCache; - this.workflowLanguageService = server.workflowLanguageService; this.connection = server.connection; } + + protected getLanguageServiceById(languageId: string): LanguageServiceBase { + return this.server.getLanguageServiceById(languageId); + } } diff --git a/server/packages/server-common/src/providers/completionProvider.ts b/server/packages/server-common/src/providers/completionProvider.ts index 9e0d710..7210a13 100644 --- a/server/packages/server-common/src/providers/completionProvider.ts +++ b/server/packages/server-common/src/providers/completionProvider.ts @@ -12,9 +12,10 @@ export class CompletionProvider extends Provider { this.connection.onCompletion(async (params) => this.onCompletion(params)); } private async onCompletion(params: CompletionParams): Promise { - const workflowDocument = this.documentsCache.get(params.textDocument.uri); - if (workflowDocument) { - const result = await this.workflowLanguageService.doComplete(workflowDocument, params.position); + const documentContext = this.documentsCache.get(params.textDocument.uri); + if (documentContext) { + const languageService = this.getLanguageServiceById(documentContext.languageId); + const result = await languageService.doComplete(documentContext, params.position); return result; } return null; diff --git a/server/packages/server-common/src/providers/formattingProvider.ts b/server/packages/server-common/src/providers/formattingProvider.ts index 70184e0..bfd85ae 100644 --- a/server/packages/server-common/src/providers/formattingProvider.ts +++ b/server/packages/server-common/src/providers/formattingProvider.ts @@ -30,11 +30,12 @@ export class FormattingProvider extends Provider { } private onFormat(documentUri: string, range: Range | undefined, options: FormattingOptions): TextEdit[] { - const workflowDocument = this.documentsCache.get(documentUri); - if (workflowDocument) { - const edits = this.workflowLanguageService.format( - workflowDocument.textDocument, - range ?? this.getFullRange(workflowDocument.textDocument), + const documentContext = this.documentsCache.get(documentUri); + if (documentContext) { + const languageService = this.getLanguageServiceById(documentContext.languageId); + const edits = languageService.format( + documentContext.textDocument, + range ?? this.getFullRange(documentContext.textDocument), options ); return edits; diff --git a/server/packages/server-common/src/providers/hover/hoverProvider.ts b/server/packages/server-common/src/providers/hover/hoverProvider.ts index 91b18ce..a191678 100644 --- a/server/packages/server-common/src/providers/hover/hoverProvider.ts +++ b/server/packages/server-common/src/providers/hover/hoverProvider.ts @@ -19,11 +19,12 @@ export class HoverProvider extends Provider { } private async onHover(params: HoverParams): Promise { - const workflowDocument = this.documentsCache.get(params.textDocument.uri); - if (!workflowDocument) { + const documentContext = this.documentsCache.get(params.textDocument.uri); + if (!documentContext) { return null; } - const hover = await this.workflowLanguageService.doHover(workflowDocument, params.position); + const languageService = this.getLanguageServiceById(documentContext.languageId); + const hover = await languageService.doHover(documentContext, params.position); if (!hover) { return null; } @@ -35,7 +36,7 @@ export class HoverProvider extends Provider { : `${hover.contents}`, ]; this.contributors.forEach((contentContributor) => { - const contributedContent = contentContributor.onHoverContent(workflowDocument, params.position); + const contributedContent = contentContributor.onHoverContent(documentContext, params.position); contentSections.push(contributedContent); }); this.setHoverContentSections(hover, contentSections); diff --git a/server/packages/server-common/src/providers/symbolsProvider.ts b/server/packages/server-common/src/providers/symbolsProvider.ts index 18a2a9d..29080ae 100644 --- a/server/packages/server-common/src/providers/symbolsProvider.ts +++ b/server/packages/server-common/src/providers/symbolsProvider.ts @@ -1,5 +1,5 @@ import { ASTNode, ObjectASTNode, PropertyASTNode } from "../ast/types"; -import { DocumentSymbolParams, DocumentSymbol, SymbolKind, WorkflowDocument } from "../languageTypes"; +import { DocumentSymbolParams, DocumentSymbol, SymbolKind, DocumentContext } from "../languageTypes"; import { GalaxyWorkflowLanguageServer } from "../server"; import { Provider } from "./provider"; @@ -16,16 +16,16 @@ export class SymbolsProvider extends Provider { } public onDocumentSymbol(params: DocumentSymbolParams): DocumentSymbol[] { - const workflowDocument = this.documentsCache.get(params.textDocument.uri); - if (workflowDocument) { - const symbols = this.getSymbols(workflowDocument); + const documentContext = this.documentsCache.get(params.textDocument.uri); + if (documentContext) { + const symbols = this.getSymbols(documentContext); return symbols; } return []; } - private getSymbols(workflowDocument: WorkflowDocument): DocumentSymbol[] { - const root = workflowDocument.nodeManager.root; + private getSymbols(documentContext: DocumentContext): DocumentSymbol[] { + const root = documentContext.nodeManager.root; if (!root) { return []; } @@ -41,7 +41,7 @@ export class SymbolsProvider extends Provider { if (IGNORE_SYMBOL_NAMES.has(name)) { return; } - const range = workflowDocument.nodeManager.getNodeRange(node); + const range = documentContext.nodeManager.getNodeRange(node); const selectionRange = range; const symbol = { name, kind: this.getSymbolKind(node.type), range, selectionRange, children: [] }; result.push(symbol); @@ -63,8 +63,8 @@ export class SymbolsProvider extends Provider { if (IGNORE_SYMBOL_NAMES.has(name)) { return; } - const range = workflowDocument.nodeManager.getNodeRange(property); - const selectionRange = workflowDocument.nodeManager.getNodeRange(property.keyNode); + const range = documentContext.nodeManager.getNodeRange(property); + const selectionRange = documentContext.nodeManager.getNodeRange(property.keyNode); const children: DocumentSymbol[] = []; const symbol: DocumentSymbol = { name: name, diff --git a/server/packages/server-common/src/server.ts b/server/packages/server-common/src/server.ts index d18d648..ad9b692 100644 --- a/server/packages/server-common/src/server.ts +++ b/server/packages/server-common/src/server.ts @@ -6,7 +6,7 @@ import { TextDocuments, WorkspaceFolder, } from "vscode-languageserver"; -import { TextDocument, WorkflowDocument, LanguageServiceBase } from "./languageTypes"; +import { TextDocument, WorkflowDocument, LanguageServiceBase, DocumentContext } from "./languageTypes"; import { DocumentsCache } from "./models/documentsCache"; import { FormattingProvider } from "./providers/formattingProvider"; import { HoverProvider } from "./providers/hover/hoverProvider"; @@ -16,19 +16,23 @@ import { CleanWorkflowService } from "./services/cleanWorkflow"; import { ConfigService } from "./configService"; import { CompletionProvider } from "./providers/completionProvider"; import { ValidationProfiles } from "./providers/validation/profiles"; +import { WorkflowTestsDocument } from "./models/workflowTestsDocument"; export class GalaxyWorkflowLanguageServer { - public readonly workflowLanguageService: LanguageServiceBase; public readonly configService: ConfigService; public readonly documents = new TextDocuments(TextDocument); public readonly documentsCache = new DocumentsCache(); protected workspaceFolders: WorkspaceFolder[] | null | undefined; + private languageServiceMapper: Map> = new Map(); constructor( public readonly connection: Connection, - workflowLanguageService: LanguageServiceBase + workflowLanguageService: LanguageServiceBase, + workflowTestsLanguageService: LanguageServiceBase ) { - this.workflowLanguageService = workflowLanguageService; + this.languageServiceMapper.set(workflowLanguageService.languageId, workflowLanguageService); + this.languageServiceMapper.set(workflowTestsLanguageService.languageId, workflowTestsLanguageService); + this.configService = new ConfigService(connection, () => this.onConfigurationChanged()); // Track open, change and close text document events this.trackDocumentChanges(connection); @@ -46,6 +50,14 @@ export class GalaxyWorkflowLanguageServer { this.connection.listen(); } + public getLanguageServiceById(languageId: string): LanguageServiceBase { + const languageService = this.languageServiceMapper.get(languageId); + if (!languageService) { + throw new Error(`Language service not found for languageId: ${languageId}`); + } + return languageService; + } + private async initialize(params: InitializeParams): Promise { this.configService.initialize(params.capabilities); this.workspaceFolders = params.workspaceFolders; @@ -88,9 +100,10 @@ export class GalaxyWorkflowLanguageServer { * An event that fires when a workflow document has been opened or the content changes. */ private onDidChangeContent(textDocument: TextDocument): void { - const workflowDocument = this.workflowLanguageService.parseDocument(textDocument); - this.documentsCache.addOrReplaceDocument(workflowDocument); - this.validateWorkflow(workflowDocument); + const languageService = this.getLanguageServiceById(textDocument.languageId); + const documentContext = languageService.parseDocument(textDocument); + this.documentsCache.addOrReplaceDocument(documentContext); + this.validateDocument(documentContext); } private onDidClose(textDocument: TextDocument): void { @@ -101,7 +114,7 @@ export class GalaxyWorkflowLanguageServer { private onConfigurationChanged(): void { this.documentsCache.all().forEach((workflowDocument) => { - this.validateWorkflow(workflowDocument); + this.validateDocument(workflowDocument); }); } @@ -109,13 +122,14 @@ export class GalaxyWorkflowLanguageServer { this.documentsCache.dispose(); } - private async validateWorkflow(workflowDocument: WorkflowDocument): Promise { + private async validateDocument(workflowDocument: DocumentContext): Promise { if (DocumentsCache.schemesToSkip.includes(workflowDocument.uri.scheme)) { return; } const settings = await this.configService.getDocumentSettings(workflowDocument.textDocument.uri); const validationProfile = ValidationProfiles.get(settings.validation.profile); - this.workflowLanguageService.validate(workflowDocument, validationProfile).then((diagnostics) => { + const languageService = this.getLanguageServiceById(workflowDocument.languageId); + languageService.validate(workflowDocument, validationProfile).then((diagnostics) => { this.connection.sendDiagnostics({ uri: workflowDocument.textDocument.uri, diagnostics }); }); } diff --git a/server/packages/server-common/src/services/cleanWorkflow.ts b/server/packages/server-common/src/services/cleanWorkflow.ts index 72e14a7..bd2cbfb 100644 --- a/server/packages/server-common/src/services/cleanWorkflow.ts +++ b/server/packages/server-common/src/services/cleanWorkflow.ts @@ -48,7 +48,8 @@ export class CleanWorkflowService extends ServiceBase { params: CleanWorkflowContentsParams ): Promise { const tempDocument = this.createTempWorkflowDocumentWithContents(params.contents); - const workflowDocument = this.workflowLanguageService.parseDocument(tempDocument); + const workflowLanguageService = this.getLanguageServiceById(tempDocument.languageId); + const workflowDocument = workflowLanguageService.parseDocument(tempDocument) as WorkflowDocument; if (workflowDocument) { return await this.cleanWorkflowContentsResult(workflowDocument); } @@ -68,7 +69,10 @@ export class CleanWorkflowService extends ServiceBase { const workflowDocument = this.documentsCache.get(params.uri); if (workflowDocument) { const settings = await this.server.configService.getDocumentSettings(workflowDocument.textDocument.uri); - const edits = this.getTextEditsToCleanWorkflow(workflowDocument, settings.cleaning.cleanableProperties); + const edits = this.getTextEditsToCleanWorkflow( + workflowDocument as WorkflowDocument, + settings.cleaning.cleanableProperties + ); const editParams: ApplyWorkspaceEditParams = { label: "Clean workflow", edit: { From 19e796af6bec50b1cb92983439440e05a7967cfd Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sat, 28 Oct 2023 21:40:18 +0200 Subject: [PATCH 07/98] Register language for wf tests in extension --- client/src/common/constants.ts | 1 + client/src/extension.ts | 12 ++++++------ package.json | 15 +++++++++++++++ server/package-lock.json | 4 ++-- .../src/providers/completionProvider.ts | 1 + server/packages/server-common/src/server.ts | 16 ++++++++-------- 6 files changed, 33 insertions(+), 16 deletions(-) diff --git a/client/src/common/constants.ts b/client/src/common/constants.ts index c2b0d8a..2834e5d 100644 --- a/client/src/common/constants.ts +++ b/client/src/common/constants.ts @@ -2,5 +2,6 @@ export namespace Constants { export const NATIVE_WORKFLOW_LANGUAGE_ID = "galaxyworkflow"; export const GXFORMAT2_WORKFLOW_LANGUAGE_ID = "gxformat2"; + export const GXFORMAT2_WORKFLOW_TESTS_LANGUAGE_ID = "gxwftests"; export const CLEAN_WORKFLOW_DOCUMENT_SCHEME = "galaxy-clean-workflow"; } diff --git a/client/src/extension.ts b/client/src/extension.ts index 3c510eb..8eae430 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -10,11 +10,11 @@ let gxFormat2LanguageClient: LanguageClient; export function activate(context: ExtensionContext): void { nativeLanguageClient = buildNodeLanguageClient( - Constants.NATIVE_WORKFLOW_LANGUAGE_ID, + [Constants.NATIVE_WORKFLOW_LANGUAGE_ID], buildNativeServerOptions(context) ); gxFormat2LanguageClient = buildNodeLanguageClient( - Constants.GXFORMAT2_WORKFLOW_LANGUAGE_ID, + [Constants.GXFORMAT2_WORKFLOW_LANGUAGE_ID, Constants.GXFORMAT2_WORKFLOW_TESTS_LANGUAGE_ID], buildGxFormat2ServerOptions(context) ); @@ -26,12 +26,12 @@ export async function deactivate(): Promise { await gxFormat2LanguageClient?.stop(); } -function buildNodeLanguageClient(languageId: string, serverOptions: ServerOptions): LanguageClient { - const documentSelector = [{ language: languageId }]; +function buildNodeLanguageClient(languageIds: string[], serverOptions: ServerOptions): LanguageClient { + const documentSelector = languageIds.map((languageId) => ({ language: languageId })); const clientOptions: LanguageClientOptions = buildBasicLanguageClientOptions(documentSelector); return new LanguageClient( - `${languageId}-language-client`, - `Galaxy Workflows (${languageId})`, + `${languageIds}-language-client`, + `Galaxy Workflows (${languageIds})`, serverOptions, clientOptions ); diff --git a/package.json b/package.json index 9d286db..3b61fca 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,16 @@ ".gxwf.yml" ], "configuration": "./workflow-languages/configurations/yml.language-configuration.json" + }, + { + "id": "gxwftests", + "aliases": [ + "Galaxy Workflow Tests (YAML)" + ], + "extensions": [ + "-tests.yml" + ], + "configuration": "./workflow-languages/configurations/yml.language-configuration.json" } ], "grammars": [ @@ -72,6 +82,11 @@ "language": "gxformat2", "scopeName": "source.gxformat2", "path": "./workflow-languages/syntaxes/yml.tmLanguage.json" + }, + { + "language": "gxwftests", + "scopeName": "source.gxformat2", + "path": "./workflow-languages/syntaxes/yml.tmLanguage.json" } ], "configuration": [ diff --git a/server/package-lock.json b/server/package-lock.json index 36d9582..8768514 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -32,7 +32,7 @@ "license": "MIT", "dependencies": { "@gxwf/server-common": "*", - "@gxwf/workflow-tests-language-service": "^0.1.0", + "@gxwf/workflow-tests-language-service": "*", "jsonc-parser": "^3.2.0", "vscode-json-languageservice": "^5.3.6", "vscode-languageserver": "^8.1.0", @@ -237,7 +237,7 @@ "version": "file:gx-workflow-ls-native", "requires": { "@gxwf/server-common": "*", - "@gxwf/workflow-tests-language-service": "^0.1.0", + "@gxwf/workflow-tests-language-service": "*", "jsonc-parser": "^3.2.0", "vscode-json-languageservice": "^5.3.6", "vscode-languageserver": "^8.1.0", diff --git a/server/packages/server-common/src/providers/completionProvider.ts b/server/packages/server-common/src/providers/completionProvider.ts index 7210a13..4d1fe98 100644 --- a/server/packages/server-common/src/providers/completionProvider.ts +++ b/server/packages/server-common/src/providers/completionProvider.ts @@ -11,6 +11,7 @@ export class CompletionProvider extends Provider { super(server); this.connection.onCompletion(async (params) => this.onCompletion(params)); } + private async onCompletion(params: CompletionParams): Promise { const documentContext = this.documentsCache.get(params.textDocument.uri); if (documentContext) { diff --git a/server/packages/server-common/src/server.ts b/server/packages/server-common/src/server.ts index ad9b692..e893470 100644 --- a/server/packages/server-common/src/server.ts +++ b/server/packages/server-common/src/server.ts @@ -113,8 +113,8 @@ export class GalaxyWorkflowLanguageServer { } private onConfigurationChanged(): void { - this.documentsCache.all().forEach((workflowDocument) => { - this.validateDocument(workflowDocument); + this.documentsCache.all().forEach((documentContext) => { + this.validateDocument(documentContext); }); } @@ -122,15 +122,15 @@ export class GalaxyWorkflowLanguageServer { this.documentsCache.dispose(); } - private async validateDocument(workflowDocument: DocumentContext): Promise { - if (DocumentsCache.schemesToSkip.includes(workflowDocument.uri.scheme)) { + private async validateDocument(documentContext: DocumentContext): Promise { + if (DocumentsCache.schemesToSkip.includes(documentContext.uri.scheme)) { return; } - const settings = await this.configService.getDocumentSettings(workflowDocument.textDocument.uri); + const settings = await this.configService.getDocumentSettings(documentContext.textDocument.uri); const validationProfile = ValidationProfiles.get(settings.validation.profile); - const languageService = this.getLanguageServiceById(workflowDocument.languageId); - languageService.validate(workflowDocument, validationProfile).then((diagnostics) => { - this.connection.sendDiagnostics({ uri: workflowDocument.textDocument.uri, diagnostics }); + const languageService = this.getLanguageServiceById(documentContext.languageId); + languageService.validate(documentContext, validationProfile).then((diagnostics) => { + this.connection.sendDiagnostics({ uri: documentContext.textDocument.uri, diagnostics }); }); } From 8ffc891c383c021188620568fc341a995e55456e Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sun, 29 Oct 2023 17:39:20 +0100 Subject: [PATCH 08/98] Refactor DocumentBase --- .../server-common/src/models/document.ts | 32 ++++++------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/server/packages/server-common/src/models/document.ts b/server/packages/server-common/src/models/document.ts index 45595a7..9905ebd 100644 --- a/server/packages/server-common/src/models/document.ts +++ b/server/packages/server-common/src/models/document.ts @@ -7,32 +7,18 @@ import { ASTNodeManager } from "../ast/nodeManager"; * This class contains basic common handling logic for any kind of known document. */ export abstract class DocumentBase implements DocumentContext { - protected _textDocument: TextDocument; - protected _documentUri: URI; - protected _parsedDocument: ParsedDocument; - protected _nodeManager: ASTNodeManager; + public readonly uri: URI; + public readonly nodeManager: ASTNodeManager; - constructor(textDocument: TextDocument, parsedDocument: ParsedDocument) { - this._textDocument = textDocument; - this._parsedDocument = parsedDocument; - this._nodeManager = new ASTNodeManager(textDocument, parsedDocument); - this._documentUri = URI.parse(this._textDocument.uri); + constructor( + public readonly textDocument: TextDocument, + protected readonly parsedDocument: ParsedDocument + ) { + this.nodeManager = new ASTNodeManager(textDocument, parsedDocument); + this.uri = URI.parse(textDocument.uri); } public get languageId(): string { - return this._textDocument.languageId; - } - - public get uri(): URI { - return this._documentUri; - } - - public get textDocument(): TextDocument { - return this._textDocument; - } - - /** Abstract Syntax Tree Node Manager associated with this document. */ - public get nodeManager(): ASTNodeManager { - return this._nodeManager; + return this.textDocument.languageId; } } From 25400121c9ffb37ee27fe885ea81ad330ac5ce92 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Fri, 10 Nov 2023 12:56:55 +0100 Subject: [PATCH 09/98] Add Inversify IoC container + refactoring --- server/gx-workflow-ls-format2/package.json | 7 +- .../src/browser/server.ts | 27 +- .../src/inversify.config.ts | 26 + .../src/languageService.ts | 26 +- .../gx-workflow-ls-format2/src/node/server.ts | 13 +- .../src/schema/schemaLoader.ts | 11 +- .../src/schema/schemaNodeResolver.ts | 9 +- server/gx-workflow-ls-format2/tsconfig.json | 3 + server/gx-workflow-ls-native/package.json | 5 + .../src/browser/server.ts | 27 +- .../src/inversify.config.ts | 22 + .../src/languageService.ts | 23 +- .../gx-workflow-ls-native/src/node/server.ts | 13 +- server/gx-workflow-ls-native/tsconfig.json | 3 + server/package-lock.json | 1148 +++++++++++++++++ server/packages/server-common/package.json | 9 +- .../server-common/src/configService.ts | 25 +- .../server-common/src/inversify.config.ts | 10 + .../server-common/src/languageTypes.ts | 67 +- .../src/models/documentsCache.ts | 14 +- .../src/providers/completionProvider.ts | 8 +- .../src/providers/formattingProvider.ts | 10 +- .../src/providers/hover/hoverProvider.ts | 16 +- .../server-common/src/providers/provider.ts | 9 +- .../src/providers/symbolsProvider.ts | 13 +- server/packages/server-common/src/server.ts | 36 +- .../src/services/cleanWorkflow.ts | 13 +- .../server-common/src/services/index.ts | 8 +- server/packages/server-common/tsconfig.json | 3 + .../package.json | 5 + .../src/inversify.config.ts | 9 + .../src/languageService.ts | 11 +- .../tsconfig.json | 3 + .../yaml-language-service/package.json | 5 + .../src/inversify.config.ts | 10 + 35 files changed, 1504 insertions(+), 143 deletions(-) create mode 100644 server/gx-workflow-ls-format2/src/inversify.config.ts create mode 100644 server/gx-workflow-ls-native/src/inversify.config.ts create mode 100644 server/packages/server-common/src/inversify.config.ts create mode 100644 server/packages/workflow-tests-language-service/src/inversify.config.ts create mode 100644 server/packages/yaml-language-service/src/inversify.config.ts diff --git a/server/gx-workflow-ls-format2/package.json b/server/gx-workflow-ls-format2/package.json index 918d820..3fbea98 100644 --- a/server/gx-workflow-ls-format2/package.json +++ b/server/gx-workflow-ls-format2/package.json @@ -8,10 +8,15 @@ "@gxwf/server-common": "*", "@gxwf/workflow-tests-language-service": "*", "@gxwf/yaml-language-service": "*", + "inversify": "^6.0.2", + "reflect-metadata": "^0.1.13", "vscode-languageserver": "^8.1.0", "vscode-languageserver-textdocument": "^1.0.8", "vscode-languageserver-types": "^3.17.3", "vscode-uri": "^3.0.7" }, - "scripts": {} + "scripts": {}, + "devDependencies": { + "@types/jest": "^29.5.7" + } } diff --git a/server/gx-workflow-ls-format2/src/browser/server.ts b/server/gx-workflow-ls-format2/src/browser/server.ts index 141f999..76b38fc 100644 --- a/server/gx-workflow-ls-format2/src/browser/server.ts +++ b/server/gx-workflow-ls-format2/src/browser/server.ts @@ -1,14 +1,21 @@ -import { createConnection, BrowserMessageReader, BrowserMessageWriter } from "vscode-languageserver/browser"; -import { GalaxyWorkflowLanguageServer } from "@gxwf/server-common/src/server"; -import { GxWorkflowTestsLanguageService } from "@gxwf/workflow-tests-language-service/src/languageService"; -import { GxFormat2WorkflowLanguageService } from "../languageService"; +import { + createConnection, + BrowserMessageReader, + BrowserMessageWriter, + Connection, +} from "vscode-languageserver/browser"; +import { TYPES, container } from "../inversify.config"; +import { GalaxyWorkflowLanguageServer } from "@gxwf/server-common/src/languageTypes"; -const messageReader = new BrowserMessageReader(self); -const messageWriter = new BrowserMessageWriter(self); +function createBrowserConnection(): Connection { + const messageReader = new BrowserMessageReader(self); + const messageWriter = new BrowserMessageWriter(self); -const connection = createConnection(messageReader, messageWriter); + const connection = createConnection(messageReader, messageWriter); + return connection; +} -const workflowLanguageService = new GxFormat2WorkflowLanguageService(); -const workflowTestsLanguageService = new GxWorkflowTestsLanguageService(); -const server = new GalaxyWorkflowLanguageServer(connection, workflowLanguageService, workflowTestsLanguageService); +container.bind(TYPES.Connection).toConstantValue(createBrowserConnection()); + +const server = container.get(TYPES.GalaxyWorkflowLanguageServer); server.start(); diff --git a/server/gx-workflow-ls-format2/src/inversify.config.ts b/server/gx-workflow-ls-format2/src/inversify.config.ts new file mode 100644 index 0000000..e84c1a2 --- /dev/null +++ b/server/gx-workflow-ls-format2/src/inversify.config.ts @@ -0,0 +1,26 @@ +import { container } from "@gxwf/server-common/src/inversify.config"; +import { GxFormat2WorkflowLanguageServiceImpl } from "./languageService"; +import { WorkflowTestsLanguageServiceContainerModule } from "@gxwf/workflow-tests-language-service/src/inversify.config"; +import { GalaxyWorkflowLanguageServer, WorkflowLanguageService } from "@gxwf/server-common/src/languageTypes"; +import { GalaxyWorkflowLanguageServerImpl } from "@gxwf/server-common/src/server"; +import { TYPES as COMMON_TYPES } from "@gxwf/server-common/src/languageTypes"; +import { YAMLLanguageServiceContainerModule } from "@gxwf/yaml-language-service/src/inversify.config"; + +export const TYPES = { + ...COMMON_TYPES, +}; + +container.load(YAMLLanguageServiceContainerModule); +container.load(WorkflowTestsLanguageServiceContainerModule); + +container + .bind(TYPES.WorkflowLanguageService) + .to(GxFormat2WorkflowLanguageServiceImpl) + .inSingletonScope(); + +container + .bind(TYPES.GalaxyWorkflowLanguageServer) + .to(GalaxyWorkflowLanguageServerImpl) + .inSingletonScope(); + +export { container }; diff --git a/server/gx-workflow-ls-format2/src/languageService.ts b/server/gx-workflow-ls-format2/src/languageService.ts index fe4e942..41350f5 100644 --- a/server/gx-workflow-ls-format2/src/languageService.ts +++ b/server/gx-workflow-ls-format2/src/languageService.ts @@ -3,36 +3,44 @@ import { Range, FormattingOptions, TextEdit, - WorkflowDocument, LanguageServiceBase, Position, Hover, CompletionList, Diagnostic, WorkflowValidator, + LanguageService, } from "@gxwf/server-common/src/languageTypes"; -import { YAMLLanguageService, getLanguageService } from "@gxwf/yaml-language-service/src/yamlLanguageService"; +import { YAMLLanguageService } from "@gxwf/yaml-language-service/src/yamlLanguageService"; import { GxFormat2WorkflowDocument } from "./gxFormat2WorkflowDocument"; import { GalaxyWorkflowFormat2SchemaLoader } from "./schema"; import { GxFormat2CompletionService } from "./services/completionService"; import { GxFormat2HoverService } from "./services/hoverService"; import { GxFormat2SchemaValidationService, WorkflowValidationService } from "./services/validation"; +import { inject, injectable } from "inversify"; +import { TYPES as YAML_TYPES } from "@gxwf/yaml-language-service/src/inversify.config"; + +export interface GxFormat2WorkflowLanguageService extends LanguageService {} /** * A wrapper around the YAML Language Service to support language features * for gxformat2 Galaxy workflow files. */ -export class GxFormat2WorkflowLanguageService extends LanguageServiceBase { +@injectable() +export class GxFormat2WorkflowLanguageServiceImpl + extends LanguageServiceBase + implements GxFormat2WorkflowLanguageService +{ private _yamlLanguageService: YAMLLanguageService; private _schemaLoader: GalaxyWorkflowFormat2SchemaLoader; private _hoverService: GxFormat2HoverService; private _completionService: GxFormat2CompletionService; private _validationServices: WorkflowValidator[]; - constructor() { + constructor(@inject(YAML_TYPES.YAMLLanguageService) yamlLanguageService: YAMLLanguageService) { super("gxformat2"); this._schemaLoader = new GalaxyWorkflowFormat2SchemaLoader(); - this._yamlLanguageService = getLanguageService(); + this._yamlLanguageService = yamlLanguageService; this._hoverService = new GxFormat2HoverService(this._schemaLoader.nodeResolver); this._completionService = new GxFormat2CompletionService(this._schemaLoader.nodeResolver); this._validationServices = [ @@ -41,7 +49,7 @@ export class GxFormat2WorkflowLanguageService extends LanguageServiceBase { + public override doHover(documentContext: GxFormat2WorkflowDocument, position: Position): Promise { return this._hoverService.doHover(documentContext.textDocument, position, documentContext.nodeManager); } public override async doComplete( - documentContext: WorkflowDocument, + documentContext: GxFormat2WorkflowDocument, position: Position ): Promise { return this._completionService.doComplete(documentContext.textDocument, position, documentContext.nodeManager); } - protected override async doValidation(documentContext: WorkflowDocument): Promise { + protected override async doValidation(documentContext: GxFormat2WorkflowDocument): Promise { const format2WorkflowDocument = documentContext as GxFormat2WorkflowDocument; const diagnostics = await this._yamlLanguageService.doValidation(format2WorkflowDocument.yamlDocument); for (const validator of this._validationServices) { diff --git a/server/gx-workflow-ls-format2/src/node/server.ts b/server/gx-workflow-ls-format2/src/node/server.ts index cc1c1e0..34edc9b 100644 --- a/server/gx-workflow-ls-format2/src/node/server.ts +++ b/server/gx-workflow-ls-format2/src/node/server.ts @@ -1,11 +1,8 @@ -import { createConnection } from "vscode-languageserver/node"; -import { GalaxyWorkflowLanguageServer } from "@gxwf/server-common/src/server"; -import { GxFormat2WorkflowLanguageService } from "../languageService"; -import { GxWorkflowTestsLanguageService } from "@gxwf/workflow-tests-language-service/src/languageService"; +import { Connection, createConnection } from "vscode-languageserver/node"; +import { container, TYPES } from "../inversify.config"; +import { GalaxyWorkflowLanguageServer } from "@gxwf/server-common/src/languageTypes"; -const connection = createConnection(); +container.bind(TYPES.Connection).toConstantValue(createConnection()); -const workflowLanguageService = new GxFormat2WorkflowLanguageService(); -const workflowTestsLanguageService = new GxWorkflowTestsLanguageService(); -const server = new GalaxyWorkflowLanguageServer(connection, workflowLanguageService, workflowTestsLanguageService); +const server = container.get(TYPES.GalaxyWorkflowLanguageServer); server.start(); diff --git a/server/gx-workflow-ls-format2/src/schema/schemaLoader.ts b/server/gx-workflow-ls-format2/src/schema/schemaLoader.ts index 0b3acaa..6ffd7be 100644 --- a/server/gx-workflow-ls-format2/src/schema/schemaLoader.ts +++ b/server/gx-workflow-ls-format2/src/schema/schemaLoader.ts @@ -12,10 +12,15 @@ import { SchemaField, SchemaRecord, } from "./definitions"; -import { SchemaNodeResolver } from "./schemaNodeResolver"; +import { SchemaNodeResolver, SchemaNodeResolverImpl } from "./schemaNodeResolver"; import { SCHEMA_DOCS_v19_09_MAP } from "./versions"; -export class GalaxyWorkflowFormat2SchemaLoader { +export interface GalaxyWorkflowSchemaLoader { + readonly definitions: SchemaDefinitions; + readonly nodeResolver: SchemaNodeResolver; +} + +export class GalaxyWorkflowFormat2SchemaLoader implements GalaxyWorkflowSchemaLoader { private _documentEntryMap = new Map>(); private _rawSchemaEntries = new Map(); private _namespaces = new Map(); @@ -250,7 +255,7 @@ export class GalaxyWorkflowFormat2SchemaLoader { } private createNodeResolver(): SchemaNodeResolver { - return new SchemaNodeResolver(this.definitions, this._root); + return new SchemaNodeResolverImpl(this.definitions, this._root); } /** Expands all entries with the types defined in the extended types.*/ diff --git a/server/gx-workflow-ls-format2/src/schema/schemaNodeResolver.ts b/server/gx-workflow-ls-format2/src/schema/schemaNodeResolver.ts index aa8ba31..bf2f12d 100644 --- a/server/gx-workflow-ls-format2/src/schema/schemaNodeResolver.ts +++ b/server/gx-workflow-ls-format2/src/schema/schemaNodeResolver.ts @@ -1,7 +1,14 @@ import { NodePath, Segment } from "@gxwf/server-common/src/ast/types"; import { RecordSchemaNode, SchemaDefinitions, SchemaNode, SchemaRecord } from "./definitions"; -export class SchemaNodeResolver { +export interface SchemaNodeResolver { + rootNode: SchemaNode; + definitions: SchemaDefinitions; + resolveSchemaContext(path: NodePath): SchemaNode | undefined; + getSchemaNodeByTypeRef(typeRef: string): SchemaNode | undefined; +} + +export class SchemaNodeResolverImpl implements SchemaNodeResolver { public readonly rootNode: SchemaNode; constructor( public readonly definitions: SchemaDefinitions, diff --git a/server/gx-workflow-ls-format2/tsconfig.json b/server/gx-workflow-ls-format2/tsconfig.json index 3d42a4a..44fa97c 100644 --- a/server/gx-workflow-ls-format2/tsconfig.json +++ b/server/gx-workflow-ls-format2/tsconfig.json @@ -3,6 +3,9 @@ "target": "es2019", "lib": ["ES2019", "WebWorker"], "module": "commonjs", + "types": ["reflect-metadata", "jest", "node"], + "experimentalDecorators": true, + "emitDecoratorMetadata": true, "moduleResolution": "node", "resolveJsonModule": true, "esModuleInterop": true, diff --git a/server/gx-workflow-ls-native/package.json b/server/gx-workflow-ls-native/package.json index ae97cdd..2345de4 100644 --- a/server/gx-workflow-ls-native/package.json +++ b/server/gx-workflow-ls-native/package.json @@ -7,7 +7,9 @@ "dependencies": { "@gxwf/server-common": "*", "@gxwf/workflow-tests-language-service": "*", + "inversify": "^6.0.2", "jsonc-parser": "^3.2.0", + "reflect-metadata": "^0.1.13", "vscode-json-languageservice": "^5.3.6", "vscode-languageserver": "^8.1.0", "vscode-languageserver-textdocument": "^1.0.8", @@ -18,5 +20,8 @@ "webpack": "webpack", "watch": "webpack --watch --progress", "test-unit": "jest" + }, + "devDependencies": { + "@types/jest": "^29.5.7" } } diff --git a/server/gx-workflow-ls-native/src/browser/server.ts b/server/gx-workflow-ls-native/src/browser/server.ts index 2f6c178..e134244 100644 --- a/server/gx-workflow-ls-native/src/browser/server.ts +++ b/server/gx-workflow-ls-native/src/browser/server.ts @@ -1,14 +1,21 @@ -import { createConnection, BrowserMessageReader, BrowserMessageWriter } from "vscode-languageserver/browser"; -import { NativeWorkflowLanguageService } from "../languageService"; -import { GalaxyWorkflowLanguageServer } from "@gxwf/server-common/src/server"; -import { GxWorkflowTestsLanguageService } from "@gxwf/workflow-tests-language-service/src/languageService"; +import { + createConnection, + BrowserMessageReader, + BrowserMessageWriter, + Connection, +} from "vscode-languageserver/browser"; +import { container } from "../inversify.config"; +import { GalaxyWorkflowLanguageServer, TYPES } from "@gxwf/server-common/src/languageTypes"; -const messageReader = new BrowserMessageReader(self); -const messageWriter = new BrowserMessageWriter(self); +function createBrowserConnection(): Connection { + const messageReader = new BrowserMessageReader(self); + const messageWriter = new BrowserMessageWriter(self); -const connection = createConnection(messageReader, messageWriter); + const connection = createConnection(messageReader, messageWriter); + return connection; +} -const workflowLanguageService = new NativeWorkflowLanguageService(); -const workflowTestsLanguageService = new GxWorkflowTestsLanguageService(); -const server = new GalaxyWorkflowLanguageServer(connection, workflowLanguageService, workflowTestsLanguageService); +container.bind(TYPES.Connection).toConstantValue(createBrowserConnection()); + +const server = container.get(TYPES.GalaxyWorkflowLanguageServer); server.start(); diff --git a/server/gx-workflow-ls-native/src/inversify.config.ts b/server/gx-workflow-ls-native/src/inversify.config.ts new file mode 100644 index 0000000..2312ea0 --- /dev/null +++ b/server/gx-workflow-ls-native/src/inversify.config.ts @@ -0,0 +1,22 @@ +import { container } from "@gxwf/server-common/src/inversify.config"; +import { NativeWorkflowLanguageServiceImpl } from "./languageService"; +import { WorkflowTestsLanguageServiceContainerModule } from "@gxwf/workflow-tests-language-service/src/inversify.config"; +import { GalaxyWorkflowLanguageServer, WorkflowLanguageService } from "@gxwf/server-common/src/languageTypes"; +import { GalaxyWorkflowLanguageServerImpl } from "@gxwf/server-common/src/server"; +import { TYPES } from "@gxwf/server-common/src/languageTypes"; +import { YAMLLanguageServiceContainerModule } from "@gxwf/yaml-language-service/src/inversify.config"; + +container.load(YAMLLanguageServiceContainerModule); +container.load(WorkflowTestsLanguageServiceContainerModule); + +container + .bind(TYPES.WorkflowLanguageService) + .to(NativeWorkflowLanguageServiceImpl) + .inSingletonScope(); + +container + .bind(TYPES.GalaxyWorkflowLanguageServer) + .to(GalaxyWorkflowLanguageServerImpl) + .inSingletonScope(); + +export { container }; diff --git a/server/gx-workflow-ls-native/src/languageService.ts b/server/gx-workflow-ls-native/src/languageService.ts index cdaf99f..745d28c 100644 --- a/server/gx-workflow-ls-native/src/languageService.ts +++ b/server/gx-workflow-ls-native/src/languageService.ts @@ -2,7 +2,7 @@ import { DocumentLanguageSettings, getLanguageService, JSONSchema, - LanguageService, + LanguageService as JSONLanguageService, LanguageServiceParams, LanguageSettings, SchemaConfiguration, @@ -16,18 +16,25 @@ import { Range, TextDocument, TextEdit, - WorkflowDocument, LanguageServiceBase, + LanguageService, } from "@gxwf/server-common/src/languageTypes"; import NativeWorkflowSchema from "../../../workflow-languages/schemas/native.schema.json"; import { NativeWorkflowDocument } from "./nativeWorkflowDocument"; +import { injectable } from "inversify"; + +export interface NativeWorkflowLanguageService extends LanguageService {} /** * A wrapper around the JSON Language Service to support language features * for native Galaxy workflow files AKA '.ga' workflows. */ -export class NativeWorkflowLanguageService extends LanguageServiceBase { - private _jsonLanguageService: LanguageService; +@injectable() +export class NativeWorkflowLanguageServiceImpl + extends LanguageServiceBase + implements NativeWorkflowLanguageService +{ + private _jsonLanguageService: JSONLanguageService; private _documentSettings: DocumentLanguageSettings = { schemaValidation: "error" }; constructor() { @@ -42,7 +49,7 @@ export class NativeWorkflowLanguageService extends LanguageServiceBase { + public override async doHover(workflowDocument: NativeWorkflowDocument, position: Position): Promise { const nativeWorkflowDocument = workflowDocument as NativeWorkflowDocument; const hover = await this._jsonLanguageService.doHover( nativeWorkflowDocument.textDocument, @@ -62,7 +69,7 @@ export class NativeWorkflowLanguageService extends LanguageServiceBase { const nativeWorkflowDocument = workflowDocument as NativeWorkflowDocument; @@ -74,7 +81,7 @@ export class NativeWorkflowLanguageService extends LanguageServiceBase { + protected override async doValidation(workflowDocument: NativeWorkflowDocument): Promise { const nativeWorkflowDocument = workflowDocument as NativeWorkflowDocument; const schemaValidationResults = await this._jsonLanguageService.doValidation( nativeWorkflowDocument.textDocument, diff --git a/server/gx-workflow-ls-native/src/node/server.ts b/server/gx-workflow-ls-native/src/node/server.ts index 9e96519..5f939b5 100644 --- a/server/gx-workflow-ls-native/src/node/server.ts +++ b/server/gx-workflow-ls-native/src/node/server.ts @@ -1,11 +1,8 @@ -import { createConnection } from "vscode-languageserver/node"; -import { GalaxyWorkflowLanguageServer } from "@gxwf/server-common/src/server"; -import { NativeWorkflowLanguageService } from "../languageService"; -import { GxWorkflowTestsLanguageService } from "@gxwf/workflow-tests-language-service/src/languageService"; +import { Connection, createConnection } from "vscode-languageserver/node"; +import { container } from "../inversify.config"; +import { GalaxyWorkflowLanguageServer, TYPES } from "@gxwf/server-common/src/languageTypes"; -const connection = createConnection(); +container.bind(TYPES.Connection).toConstantValue(createConnection()); -const languageService = new NativeWorkflowLanguageService(); -const workflowTestsLanguageService = new GxWorkflowTestsLanguageService(); -const server = new GalaxyWorkflowLanguageServer(connection, languageService, workflowTestsLanguageService); +const server = container.get(TYPES.GalaxyWorkflowLanguageServer); server.start(); diff --git a/server/gx-workflow-ls-native/tsconfig.json b/server/gx-workflow-ls-native/tsconfig.json index a2c3b2d..79c4cdb 100644 --- a/server/gx-workflow-ls-native/tsconfig.json +++ b/server/gx-workflow-ls-native/tsconfig.json @@ -3,6 +3,9 @@ "target": "es2019", "lib": ["ES2019", "WebWorker"], "module": "commonjs", + "types": ["reflect-metadata", "jest", "node"], + "experimentalDecorators": true, + "emitDecoratorMetadata": true, "moduleResolution": "node", "resolveJsonModule": true, "esModuleInterop": true, diff --git a/server/package-lock.json b/server/package-lock.json index 8768514..8ef611f 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -21,10 +21,15 @@ "@gxwf/server-common": "*", "@gxwf/workflow-tests-language-service": "*", "@gxwf/yaml-language-service": "*", + "inversify": "^6.0.2", + "reflect-metadata": "^0.1.13", "vscode-languageserver": "^8.1.0", "vscode-languageserver-textdocument": "^1.0.8", "vscode-languageserver-types": "^3.17.3", "vscode-uri": "^3.0.7" + }, + "devDependencies": { + "@types/jest": "^29.5.7" } }, "gx-workflow-ls-native": { @@ -33,12 +38,195 @@ "dependencies": { "@gxwf/server-common": "*", "@gxwf/workflow-tests-language-service": "*", + "inversify": "^6.0.2", "jsonc-parser": "^3.2.0", + "reflect-metadata": "^0.1.13", "vscode-json-languageservice": "^5.3.6", "vscode-languageserver": "^8.1.0", "vscode-languageserver-textdocument": "^1.0.8", "vscode-languageserver-types": "^3.17.3", "vscode-uri": "^3.0.7" + }, + "devDependencies": { + "@types/jest": "^29.5.7" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" } }, "node_modules/@gxwf/server-common": { @@ -53,16 +241,246 @@ "resolved": "packages/yaml-language-service", "link": true }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-zONci81DZYCZjiLe0r6equvZut0b+dBRPBN5kBDjsONnutYNtJMoWQ9uR2RkL1gLG9NMTzvf+29e5RFfPbeKhQ==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.2.tgz", + "integrity": "sha512-8toY6FgdltSdONav1XtUHl4LN1yTmLza+EuDazb/fEmRNCwjyqNVIQWs2IfC74IqjHkREs/nQ2FWq5kZU9IC0w==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.3.tgz", + "integrity": "sha512-1nESsePMBlf0RPRffLZi5ujYh7IH1BWL4y9pr+Bn3cJBdxz+RTP8bUFljLz9HvzhhOSWKdyBZ4DIivdL6rvgZg==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.7", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.7.tgz", + "integrity": "sha512-HLyetab6KVPSiF+7pFcUyMeLsx25LDNDemw9mGsJBkai/oouwrjTycocSDYopMEwFhN2Y4s9oPyOCZNofgSt2g==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, "node_modules/@types/node": { "version": "20.5.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.7.tgz", "integrity": "sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==" }, + "node_modules/@types/stack-utils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.2.tgz", + "integrity": "sha512-g7CK9nHdwjK2n0ymT2CW698FuWJRIx+RP6embAzZ2Qi8/ilIrA1Imt2LVSeHUzKvpoi7BhmmQcXz95eS0f2JXw==", + "dev": true + }, + "node_modules/@types/yargs": { + "version": "17.0.29", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.29.tgz", + "integrity": "sha512-nacjqA3ee9zRF/++a3FUY1suHTFKZeHba2n8WeDw9cCVdmzmHpIxyzOJBcpHvvEmS8E9KqWlSnWHUkOrkhWcvA==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.2", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.2.tgz", + "integrity": "sha512-5qcvofLPbfjmBfKaLfj/+f+Sbd6pN4zl7w7VSVI5uz7m9QZTuB2aZAa2uo1wHFBNN2x6g/SoTkXmd8mQnQF2Cw==", + "dev": true + }, "node_modules/@vscode/l10n": { "version": "0.0.16", "resolved": "https://registry.npmjs.org/@vscode/l10n/-/l10n-0.0.16.tgz", "integrity": "sha512-JT5CvrIYYCrmB+dCana8sUqJEcGB1ZDXNLMQ2+42bW995WmNoenijWMUdZfwmuQUTQcEVVIa2OecZzTYWUW9Cg==" }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, "node_modules/gx-workflow-ls-format2": { "resolved": "gx-workflow-ls-format2", "link": true @@ -71,11 +489,223 @@ "resolved": "gx-workflow-ls-native", "link": true }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inversify": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/inversify/-/inversify-6.0.2.tgz", + "integrity": "sha512-i9m8j/7YIv4mDuYXUAcrpKPSaju/CIly9AHK5jvCBeoiM/2KEsuCQTTP+rzSWWpLYWRukdXFSl6ZTk2/uumbiA==" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, "node_modules/jsonc-parser": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/vscode-json-languageservice": { "version": "5.3.6", "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-5.3.6.tgz", @@ -145,11 +775,16 @@ "license": "MIT", "dependencies": { "@types/node": "^20.5.7", + "inversify": "^6.0.2", + "reflect-metadata": "^0.1.13", "vscode-json-languageservice": "^5.3.6", "vscode-languageserver": "^8.1.0", "vscode-languageserver-textdocument": "^1.0.8", "vscode-languageserver-types": "^3.17.3", "vscode-uri": "^3.0.7" + }, + "devDependencies": { + "@types/jest": "^29.5.7" } }, "packages/workflow-tests-language-service": { @@ -159,10 +794,15 @@ "dependencies": { "@gxwf/server-common": "*", "@gxwf/yaml-language-service": "*", + "inversify": "^6.0.2", + "reflect-metadata": "^0.1.13", "vscode-languageserver": "^8.1.0", "vscode-languageserver-textdocument": "^1.0.8", "vscode-languageserver-types": "^3.17.3", "vscode-uri": "^3.0.7" + }, + "devDependencies": { + "@types/jest": "^29.5.7" } }, "packages/yaml-language-service": { @@ -171,18 +811,169 @@ "license": "MIT", "dependencies": { "@gxwf/server-common": "*", + "inversify": "^6.0.2", + "reflect-metadata": "^0.1.13", "vscode-languageserver-textdocument": "^1.0.8", "vscode-languageserver-types": "^3.17.3", "vscode-uri": "^3.0.7", "yaml": "^2.3.2" + }, + "devDependencies": { + "@types/jest": "^29.5.7" } } }, "dependencies": { + "@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, + "requires": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true + }, + "@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, "@gxwf/server-common": { "version": "file:packages/server-common", "requires": { + "@types/jest": "^29.5.7", "@types/node": "^20.5.7", + "inversify": "^6.0.2", + "reflect-metadata": "^0.1.13", "vscode-json-languageservice": "^5.3.6", "vscode-languageserver": "^8.1.0", "vscode-languageserver-textdocument": "^1.0.8", @@ -195,6 +986,9 @@ "requires": { "@gxwf/server-common": "*", "@gxwf/yaml-language-service": "*", + "@types/jest": "^29.5.7", + "inversify": "^6.0.2", + "reflect-metadata": "^0.1.13", "vscode-languageserver": "^8.1.0", "vscode-languageserver-textdocument": "^1.0.8", "vscode-languageserver-types": "^3.17.3", @@ -205,28 +999,216 @@ "version": "file:packages/yaml-language-service", "requires": { "@gxwf/server-common": "*", + "@types/jest": "^29.5.7", + "inversify": "^6.0.2", + "reflect-metadata": "^0.1.13", "vscode-languageserver-textdocument": "^1.0.8", "vscode-languageserver-types": "^3.17.3", "vscode-uri": "^3.0.7", "yaml": "^2.3.2" } }, + "@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "requires": { + "jest-get-type": "^29.6.3" + } + }, + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-zONci81DZYCZjiLe0r6equvZut0b+dBRPBN5kBDjsONnutYNtJMoWQ9uR2RkL1gLG9NMTzvf+29e5RFfPbeKhQ==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.2.tgz", + "integrity": "sha512-8toY6FgdltSdONav1XtUHl4LN1yTmLza+EuDazb/fEmRNCwjyqNVIQWs2IfC74IqjHkREs/nQ2FWq5kZU9IC0w==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.3.tgz", + "integrity": "sha512-1nESsePMBlf0RPRffLZi5ujYh7IH1BWL4y9pr+Bn3cJBdxz+RTP8bUFljLz9HvzhhOSWKdyBZ4DIivdL6rvgZg==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/jest": { + "version": "29.5.7", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.7.tgz", + "integrity": "sha512-HLyetab6KVPSiF+7pFcUyMeLsx25LDNDemw9mGsJBkai/oouwrjTycocSDYopMEwFhN2Y4s9oPyOCZNofgSt2g==", + "dev": true, + "requires": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, "@types/node": { "version": "20.5.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.7.tgz", "integrity": "sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==" }, + "@types/stack-utils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.2.tgz", + "integrity": "sha512-g7CK9nHdwjK2n0ymT2CW698FuWJRIx+RP6embAzZ2Qi8/ilIrA1Imt2LVSeHUzKvpoi7BhmmQcXz95eS0f2JXw==", + "dev": true + }, + "@types/yargs": { + "version": "17.0.29", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.29.tgz", + "integrity": "sha512-nacjqA3ee9zRF/++a3FUY1suHTFKZeHba2n8WeDw9cCVdmzmHpIxyzOJBcpHvvEmS8E9KqWlSnWHUkOrkhWcvA==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "21.0.2", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.2.tgz", + "integrity": "sha512-5qcvofLPbfjmBfKaLfj/+f+Sbd6pN4zl7w7VSVI5uz7m9QZTuB2aZAa2uo1wHFBNN2x6g/SoTkXmd8mQnQF2Cw==", + "dev": true + }, "@vscode/l10n": { "version": "0.0.16", "resolved": "https://registry.npmjs.org/@vscode/l10n/-/l10n-0.0.16.tgz", "integrity": "sha512-JT5CvrIYYCrmB+dCana8sUqJEcGB1ZDXNLMQ2+42bW995WmNoenijWMUdZfwmuQUTQcEVVIa2OecZzTYWUW9Cg==" }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true + }, + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true + }, + "expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "requires": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, "gx-workflow-ls-format2": { "version": "file:gx-workflow-ls-format2", "requires": { "@gxwf/server-common": "*", "@gxwf/workflow-tests-language-service": "*", "@gxwf/yaml-language-service": "*", + "@types/jest": "^29.5.7", + "inversify": "^6.0.2", + "reflect-metadata": "^0.1.13", "vscode-languageserver": "^8.1.0", "vscode-languageserver-textdocument": "^1.0.8", "vscode-languageserver-types": "^3.17.3", @@ -238,7 +1220,10 @@ "requires": { "@gxwf/server-common": "*", "@gxwf/workflow-tests-language-service": "*", + "@types/jest": "^29.5.7", + "inversify": "^6.0.2", "jsonc-parser": "^3.2.0", + "reflect-metadata": "^0.1.13", "vscode-json-languageservice": "^5.3.6", "vscode-languageserver": "^8.1.0", "vscode-languageserver-textdocument": "^1.0.8", @@ -246,11 +1231,174 @@ "vscode-uri": "^3.0.7" } }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "inversify": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/inversify/-/inversify-6.0.2.tgz", + "integrity": "sha512-i9m8j/7YIv4mDuYXUAcrpKPSaju/CIly9AHK5jvCBeoiM/2KEsuCQTTP+rzSWWpLYWRukdXFSl6ZTk2/uumbiA==" + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true + }, + "jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, "jsonc-parser": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "requires": { + "escape-string-regexp": "^2.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, "vscode-json-languageservice": { "version": "5.3.6", "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-5.3.6.tgz", diff --git a/server/packages/server-common/package.json b/server/packages/server-common/package.json index fc506a3..150c8b4 100644 --- a/server/packages/server-common/package.json +++ b/server/packages/server-common/package.json @@ -7,11 +7,16 @@ "type": "module", "dependencies": { "@types/node": "^20.5.7", + "inversify": "^6.0.2", + "reflect-metadata": "^0.1.13", "vscode-json-languageservice": "^5.3.6", + "vscode-languageserver": "^8.1.0", "vscode-languageserver-textdocument": "^1.0.8", "vscode-languageserver-types": "^3.17.3", - "vscode-languageserver": "^8.1.0", "vscode-uri": "^3.0.7" }, - "scripts": {} + "scripts": {}, + "devDependencies": { + "@types/jest": "^29.5.7" + } } diff --git a/server/packages/server-common/src/configService.ts b/server/packages/server-common/src/configService.ts index eadc403..e22a096 100644 --- a/server/packages/server-common/src/configService.ts +++ b/server/packages/server-common/src/configService.ts @@ -1,9 +1,11 @@ +import { inject, injectable } from "inversify"; import { ClientCapabilities, Connection, DidChangeConfigurationNotification, DidChangeConfigurationParams, } from "vscode-languageserver"; +import { TYPES } from "./languageTypes"; /** Represents all the available settings of the extension. */ interface ExtensionSettings { @@ -41,20 +43,27 @@ let globalSettings: ExtensionSettings = defaultSettings; // Cache the settings of all open documents const documentSettingsCache: Map = new Map(); -export class ConfigService { +export interface ConfigService { + readonly connection: Connection; + initialize(capabilities: ClientCapabilities, onConfigurationChanged: () => void): void; + getDocumentSettings(uri: string): Promise; + onDocumentClose(uri: string): void; +} + +@injectable() +export class ConfigServiceImpl implements ConfigService { protected hasConfigurationCapability = false; + private onConfigurationChanged: () => void = () => { + return; + }; - constructor( - public readonly connection: Connection, - private readonly onConfigurationChanged: () => void = () => { - return; - } - ) { + constructor(@inject(TYPES.Connection) public readonly connection: Connection) { this.connection.onInitialized(() => this.onInitialized()); this.connection.onDidChangeConfiguration((params) => this.onDidChangeConfiguration(params)); } - public initialize(capabilities: ClientCapabilities): void { + public initialize(capabilities: ClientCapabilities, onConfigurationChanged: () => void): void { + this.onConfigurationChanged = onConfigurationChanged; this.hasConfigurationCapability = !!(capabilities.workspace && !!capabilities.workspace.configuration); } diff --git a/server/packages/server-common/src/inversify.config.ts b/server/packages/server-common/src/inversify.config.ts new file mode 100644 index 0000000..2f80697 --- /dev/null +++ b/server/packages/server-common/src/inversify.config.ts @@ -0,0 +1,10 @@ +import { Container } from "inversify"; +import { TYPES, DocumentsCache } from "./languageTypes"; +import { DocumentsCacheImpl } from "./models/documentsCache"; +import { ConfigService, ConfigServiceImpl } from "./configService"; + +const container = new Container(); +container.bind(TYPES.ConfigService).to(ConfigServiceImpl).inSingletonScope(); +container.bind(TYPES.DocumentsCache).to(DocumentsCacheImpl).inSingletonScope(); + +export { container }; diff --git a/server/packages/server-common/src/languageTypes.ts b/server/packages/server-common/src/languageTypes.ts index 0111310..d270083 100644 --- a/server/packages/server-common/src/languageTypes.ts +++ b/server/packages/server-common/src/languageTypes.ts @@ -1,3 +1,4 @@ +import "reflect-metadata"; import { Range, Position, @@ -48,11 +49,11 @@ import { DocumentSymbolParams, } from "vscode-languageserver/browser"; import { WorkflowDocument } from "./models/workflowDocument"; -import { DocumentsCache } from "./models/documentsCache"; -import { GalaxyWorkflowLanguageServer } from "./server"; import { ASTNodeManager } from "./ast/nodeManager"; import { URI } from "vscode-uri"; import { WorkflowTestsDocument } from "./models/workflowTestsDocument"; +import { injectable, unmanaged } from "inversify"; +import { ConfigService } from "./configService"; export { TextDocument, @@ -154,15 +155,31 @@ export interface DocumentContext { nodeManager: ASTNodeManager; } +export interface LanguageService { + readonly languageId: string; + + parseDocument(document: TextDocument): T; + format(document: TextDocument, range: Range, options: FormattingOptions): TextEdit[]; + doHover(documentContext: T, position: Position): Promise; + doComplete(documentContext: T, position: Position): Promise; + + /** + * Validates the document and reports all the diagnostics found. + * An optional validation profile can be used to provide additional custom diagnostics. + */ + validate(documentContext: T, useProfile?: ValidationProfile): Promise; +} + /** * Abstract service defining the base functionality that a workflow language must * implement to provide assistance for workflow documents editing. */ -export abstract class LanguageServiceBase { - constructor(public readonly languageId: string) {} +@injectable() +export abstract class LanguageServiceBase implements LanguageService { + constructor(@unmanaged() public readonly languageId: string) {} - public abstract format(document: TextDocument, range: Range, options: FormattingOptions): TextEdit[]; public abstract parseDocument(document: TextDocument): T; + public abstract format(document: TextDocument, range: Range, options: FormattingOptions): TextEdit[]; public abstract doHover(documentContext: T, position: Position): Promise; public abstract doComplete(documentContext: T, position: Position): Promise; @@ -185,18 +202,34 @@ export abstract class LanguageServiceBase { } } -export abstract class ServerContext { - protected connection: Connection; - protected documentsCache: DocumentsCache; - protected server: GalaxyWorkflowLanguageServer; +export interface WorkflowLanguageService extends LanguageService {} +export interface WorkflowTestsLanguageService extends LanguageService {} - constructor(server: GalaxyWorkflowLanguageServer) { - this.server = server; - this.documentsCache = server.documentsCache; - this.connection = server.connection; - } +export interface GalaxyWorkflowLanguageServer { + connection: Connection; + documentsCache: DocumentsCache; + configService: ConfigService; + start(): void; + getLanguageServiceById(languageId: string): LanguageService; +} - protected getLanguageServiceById(languageId: string): LanguageServiceBase { - return this.server.getLanguageServiceById(languageId); - } +export interface DocumentsCache { + get(documentUri: string): DocumentContext | undefined; + all(): DocumentContext[]; + addOrReplaceDocument(documentContext: DocumentContext): void; + removeDocument(documentUri: string): void; + dispose(): void; + + get schemesToSkip(): string[]; } + +const TYPES = { + DocumentsCache: Symbol.for("DocumentsCache"), + ConfigService: Symbol.for("ConfigService"), + Connection: Symbol.for("Connection"), + WorkflowLanguageService: Symbol.for("WorkflowLanguageService"), + WorkflowTestsLanguageService: Symbol.for("WorkflowTestsLanguageService"), + GalaxyWorkflowLanguageServer: Symbol.for("GalaxyWorkflowLanguageServer"), +}; + +export { TYPES }; diff --git a/server/packages/server-common/src/models/documentsCache.ts b/server/packages/server-common/src/models/documentsCache.ts index c01432c..9071906 100644 --- a/server/packages/server-common/src/models/documentsCache.ts +++ b/server/packages/server-common/src/models/documentsCache.ts @@ -1,18 +1,24 @@ -import { DocumentContext } from "../languageTypes"; +import { injectable } from "inversify"; +import { DocumentContext, DocumentsCache } from "../languageTypes"; -export class DocumentsCache { +@injectable() +export class DocumentsCacheImpl implements DocumentsCache { private cache: Map; /** * Document URI schemes that represent temporal or readonly documents * that should not be cached. */ - public static schemesToSkip = ["temp", "galaxy-clean-workflow"]; + private static schemesToSkip = ["temp", "galaxy-clean-workflow"]; constructor() { this.cache = new Map(); } + get schemesToSkip(): string[] { + return DocumentsCacheImpl.schemesToSkip; + } + public get(documentUri: string): DocumentContext | undefined { return this.cache.get(documentUri); } @@ -22,7 +28,7 @@ export class DocumentsCache { } public addOrReplaceDocument(documentContext: DocumentContext): void { - if (DocumentsCache.schemesToSkip.includes(documentContext.uri.scheme)) { + if (this.schemesToSkip.includes(documentContext.uri.scheme)) { return; } this.cache.set(documentContext.uri.toString(), documentContext); diff --git a/server/packages/server-common/src/providers/completionProvider.ts b/server/packages/server-common/src/providers/completionProvider.ts index 4d1fe98..0bca9a4 100644 --- a/server/packages/server-common/src/providers/completionProvider.ts +++ b/server/packages/server-common/src/providers/completionProvider.ts @@ -1,6 +1,6 @@ import { CompletionList, CompletionParams } from "vscode-languageserver"; -import { GalaxyWorkflowLanguageServer } from "../server"; import { Provider } from "./provider"; +import { GalaxyWorkflowLanguageServer } from "../languageTypes"; export class CompletionProvider extends Provider { public static register(server: GalaxyWorkflowLanguageServer): CompletionProvider { @@ -9,13 +9,13 @@ export class CompletionProvider extends Provider { constructor(server: GalaxyWorkflowLanguageServer) { super(server); - this.connection.onCompletion(async (params) => this.onCompletion(params)); + this.server.connection.onCompletion(async (params) => this.onCompletion(params)); } private async onCompletion(params: CompletionParams): Promise { - const documentContext = this.documentsCache.get(params.textDocument.uri); + const documentContext = this.server.documentsCache.get(params.textDocument.uri); if (documentContext) { - const languageService = this.getLanguageServiceById(documentContext.languageId); + const languageService = this.server.getLanguageServiceById(documentContext.languageId); const result = await languageService.doComplete(documentContext, params.position); return result; } diff --git a/server/packages/server-common/src/providers/formattingProvider.ts b/server/packages/server-common/src/providers/formattingProvider.ts index bfd85ae..2f4de4d 100644 --- a/server/packages/server-common/src/providers/formattingProvider.ts +++ b/server/packages/server-common/src/providers/formattingProvider.ts @@ -6,8 +6,8 @@ import { Range, DocumentFormattingParams, DocumentRangeFormattingParams, + GalaxyWorkflowLanguageServer, } from "../languageTypes"; -import { GalaxyWorkflowLanguageServer } from "../server"; import { Provider } from "./provider"; export class FormattingProvider extends Provider { @@ -17,8 +17,8 @@ export class FormattingProvider extends Provider { constructor(server: GalaxyWorkflowLanguageServer) { super(server); - this.connection.onDocumentFormatting((params) => this.onDocumentFormatting(params)); - this.connection.onDocumentRangeFormatting((params) => this.onDocumentRangeFormatting(params)); + this.server.connection.onDocumentFormatting((params) => this.onDocumentFormatting(params)); + this.server.connection.onDocumentRangeFormatting((params) => this.onDocumentRangeFormatting(params)); } public onDocumentFormatting(params: DocumentFormattingParams): TextEdit[] { @@ -30,9 +30,9 @@ export class FormattingProvider extends Provider { } private onFormat(documentUri: string, range: Range | undefined, options: FormattingOptions): TextEdit[] { - const documentContext = this.documentsCache.get(documentUri); + const documentContext = this.server.documentsCache.get(documentUri); if (documentContext) { - const languageService = this.getLanguageServiceById(documentContext.languageId); + const languageService = this.server.getLanguageServiceById(documentContext.languageId); const edits = languageService.format( documentContext.textDocument, range ?? this.getFullRange(documentContext.textDocument), diff --git a/server/packages/server-common/src/providers/hover/hoverProvider.ts b/server/packages/server-common/src/providers/hover/hoverProvider.ts index a191678..4e30e43 100644 --- a/server/packages/server-common/src/providers/hover/hoverProvider.ts +++ b/server/packages/server-common/src/providers/hover/hoverProvider.ts @@ -1,5 +1,11 @@ -import { Hover, HoverParams, MarkupKind, MarkupContent, HoverContentContributor } from "../../languageTypes"; -import { GalaxyWorkflowLanguageServer } from "../../server"; +import { + Hover, + HoverParams, + MarkupKind, + MarkupContent, + HoverContentContributor, + GalaxyWorkflowLanguageServer, +} from "../../languageTypes"; import { Provider } from "../provider"; export class HoverProvider extends Provider { @@ -15,15 +21,15 @@ export class HoverProvider extends Provider { constructor(server: GalaxyWorkflowLanguageServer, contributors?: HoverContentContributor[]) { super(server); this.contributors = contributors ?? []; - this.connection.onHover((params) => this.onHover(params)); + this.server.connection.onHover((params) => this.onHover(params)); } private async onHover(params: HoverParams): Promise { - const documentContext = this.documentsCache.get(params.textDocument.uri); + const documentContext = this.server.documentsCache.get(params.textDocument.uri); if (!documentContext) { return null; } - const languageService = this.getLanguageServiceById(documentContext.languageId); + const languageService = this.server.getLanguageServiceById(documentContext.languageId); const hover = await languageService.doHover(documentContext, params.position); if (!hover) { return null; diff --git a/server/packages/server-common/src/providers/provider.ts b/server/packages/server-common/src/providers/provider.ts index 72064e2..4125c3f 100644 --- a/server/packages/server-common/src/providers/provider.ts +++ b/server/packages/server-common/src/providers/provider.ts @@ -1,8 +1,5 @@ -import { ServerContext } from "../languageTypes"; -import { GalaxyWorkflowLanguageServer } from "../server"; +import { GalaxyWorkflowLanguageServer } from "../languageTypes"; -export abstract class Provider extends ServerContext { - constructor(server: GalaxyWorkflowLanguageServer) { - super(server); - } +export abstract class Provider { + constructor(public server: GalaxyWorkflowLanguageServer) {} } diff --git a/server/packages/server-common/src/providers/symbolsProvider.ts b/server/packages/server-common/src/providers/symbolsProvider.ts index 29080ae..bec53c7 100644 --- a/server/packages/server-common/src/providers/symbolsProvider.ts +++ b/server/packages/server-common/src/providers/symbolsProvider.ts @@ -1,6 +1,11 @@ import { ASTNode, ObjectASTNode, PropertyASTNode } from "../ast/types"; -import { DocumentSymbolParams, DocumentSymbol, SymbolKind, DocumentContext } from "../languageTypes"; -import { GalaxyWorkflowLanguageServer } from "../server"; +import { + DocumentSymbolParams, + DocumentSymbol, + SymbolKind, + DocumentContext, + GalaxyWorkflowLanguageServer, +} from "../languageTypes"; import { Provider } from "./provider"; const IGNORE_SYMBOL_NAMES = new Set(["a_galaxy_workflow", "position", "uuid", "errors", "format-version", "version"]); @@ -12,11 +17,11 @@ export class SymbolsProvider extends Provider { constructor(server: GalaxyWorkflowLanguageServer) { super(server); - this.connection.onDocumentSymbol((params) => this.onDocumentSymbol(params)); + this.server.connection.onDocumentSymbol((params) => this.onDocumentSymbol(params)); } public onDocumentSymbol(params: DocumentSymbolParams): DocumentSymbol[] { - const documentContext = this.documentsCache.get(params.textDocument.uri); + const documentContext = this.server.documentsCache.get(params.textDocument.uri); if (documentContext) { const symbols = this.getSymbols(documentContext); return symbols; diff --git a/server/packages/server-common/src/server.ts b/server/packages/server-common/src/server.ts index e893470..ab416fc 100644 --- a/server/packages/server-common/src/server.ts +++ b/server/packages/server-common/src/server.ts @@ -6,8 +6,16 @@ import { TextDocuments, WorkspaceFolder, } from "vscode-languageserver"; -import { TextDocument, WorkflowDocument, LanguageServiceBase, DocumentContext } from "./languageTypes"; -import { DocumentsCache } from "./models/documentsCache"; +import { + TextDocument, + LanguageService, + DocumentContext, + DocumentsCache, + TYPES, + GalaxyWorkflowLanguageServer, + WorkflowLanguageService, + WorkflowTestsLanguageService, +} from "./languageTypes"; import { FormattingProvider } from "./providers/formattingProvider"; import { HoverProvider } from "./providers/hover/hoverProvider"; import { SymbolsProvider } from "./providers/symbolsProvider"; @@ -16,24 +24,24 @@ import { CleanWorkflowService } from "./services/cleanWorkflow"; import { ConfigService } from "./configService"; import { CompletionProvider } from "./providers/completionProvider"; import { ValidationProfiles } from "./providers/validation/profiles"; -import { WorkflowTestsDocument } from "./models/workflowTestsDocument"; +import { injectable, inject } from "inversify"; -export class GalaxyWorkflowLanguageServer { - public readonly configService: ConfigService; +@injectable() +export class GalaxyWorkflowLanguageServerImpl implements GalaxyWorkflowLanguageServer { public readonly documents = new TextDocuments(TextDocument); - public readonly documentsCache = new DocumentsCache(); protected workspaceFolders: WorkspaceFolder[] | null | undefined; - private languageServiceMapper: Map> = new Map(); + private languageServiceMapper: Map> = new Map(); constructor( - public readonly connection: Connection, - workflowLanguageService: LanguageServiceBase, - workflowTestsLanguageService: LanguageServiceBase + @inject(TYPES.Connection) public readonly connection: Connection, + @inject(TYPES.DocumentsCache) public readonly documentsCache: DocumentsCache, + @inject(TYPES.ConfigService) public readonly configService: ConfigService, + @inject(TYPES.WorkflowLanguageService) public readonly workflowLanguageService: WorkflowLanguageService, + @inject(TYPES.WorkflowTestsLanguageService) workflowTestsLanguageService: WorkflowTestsLanguageService ) { this.languageServiceMapper.set(workflowLanguageService.languageId, workflowLanguageService); this.languageServiceMapper.set(workflowTestsLanguageService.languageId, workflowTestsLanguageService); - this.configService = new ConfigService(connection, () => this.onConfigurationChanged()); // Track open, change and close text document events this.trackDocumentChanges(connection); @@ -50,7 +58,7 @@ export class GalaxyWorkflowLanguageServer { this.connection.listen(); } - public getLanguageServiceById(languageId: string): LanguageServiceBase { + public getLanguageServiceById(languageId: string): LanguageService { const languageService = this.languageServiceMapper.get(languageId); if (!languageService) { throw new Error(`Language service not found for languageId: ${languageId}`); @@ -59,7 +67,7 @@ export class GalaxyWorkflowLanguageServer { } private async initialize(params: InitializeParams): Promise { - this.configService.initialize(params.capabilities); + this.configService.initialize(params.capabilities, this.onConfigurationChanged); this.workspaceFolders = params.workspaceFolders; const capabilities: ServerCapabilities = { @@ -123,7 +131,7 @@ export class GalaxyWorkflowLanguageServer { } private async validateDocument(documentContext: DocumentContext): Promise { - if (DocumentsCache.schemesToSkip.includes(documentContext.uri.scheme)) { + if (this.documentsCache.schemesToSkip.includes(documentContext.uri.scheme)) { return; } const settings = await this.configService.getDocumentSettings(documentContext.textDocument.uri); diff --git a/server/packages/server-common/src/services/cleanWorkflow.ts b/server/packages/server-common/src/services/cleanWorkflow.ts index bd2cbfb..f9f1012 100644 --- a/server/packages/server-common/src/services/cleanWorkflow.ts +++ b/server/packages/server-common/src/services/cleanWorkflow.ts @@ -1,7 +1,6 @@ import { ApplyWorkspaceEditParams, Range, TextDocumentEdit, TextEdit } from "vscode-languageserver"; import { TextDocument } from "vscode-languageserver-textdocument"; -import { WorkflowDocument } from "../languageTypes"; -import { GalaxyWorkflowLanguageServer } from "../server"; +import { GalaxyWorkflowLanguageServer, WorkflowDocument } from "../languageTypes"; import { ServiceBase } from "."; import { CleanWorkflowContentsParams, @@ -30,10 +29,10 @@ export class CleanWorkflowService extends ServiceBase { } protected listenToRequests(): void { - this.connection.onRequest(CleanWorkflowContentsRequest.type, (params) => + this.server.connection.onRequest(CleanWorkflowContentsRequest.type, (params) => this.onCleanWorkflowContentsRequest(params) ); - this.connection.onRequest(CleanWorkflowDocumentRequest.type, (params) => + this.server.connection.onRequest(CleanWorkflowDocumentRequest.type, (params) => this.onCleanWorkflowDocumentRequest(params) ); } @@ -48,7 +47,7 @@ export class CleanWorkflowService extends ServiceBase { params: CleanWorkflowContentsParams ): Promise { const tempDocument = this.createTempWorkflowDocumentWithContents(params.contents); - const workflowLanguageService = this.getLanguageServiceById(tempDocument.languageId); + const workflowLanguageService = this.server.getLanguageServiceById(tempDocument.languageId); const workflowDocument = workflowLanguageService.parseDocument(tempDocument) as WorkflowDocument; if (workflowDocument) { return await this.cleanWorkflowContentsResult(workflowDocument); @@ -66,7 +65,7 @@ export class CleanWorkflowService extends ServiceBase { params: CleanWorkflowDocumentParams ): Promise { try { - const workflowDocument = this.documentsCache.get(params.uri); + const workflowDocument = this.server.documentsCache.get(params.uri); if (workflowDocument) { const settings = await this.server.configService.getDocumentSettings(workflowDocument.textDocument.uri); const edits = this.getTextEditsToCleanWorkflow( @@ -87,7 +86,7 @@ export class CleanWorkflowService extends ServiceBase { ], }, }; - this.connection.workspace.applyEdit(editParams); + this.server.connection.workspace.applyEdit(editParams); } return { error: "" }; } catch (error) { diff --git a/server/packages/server-common/src/services/index.ts b/server/packages/server-common/src/services/index.ts index 83bb32d..d657a44 100644 --- a/server/packages/server-common/src/services/index.ts +++ b/server/packages/server-common/src/services/index.ts @@ -1,9 +1,7 @@ -import { ServerContext } from "../languageTypes"; -import { GalaxyWorkflowLanguageServer } from "../server"; +import { GalaxyWorkflowLanguageServer } from "../languageTypes"; -export abstract class ServiceBase extends ServerContext { - constructor(server: GalaxyWorkflowLanguageServer) { - super(server); +export abstract class ServiceBase { + constructor(public server: GalaxyWorkflowLanguageServer) { this.listenToRequests(); } diff --git a/server/packages/server-common/tsconfig.json b/server/packages/server-common/tsconfig.json index eae02d0..300dd9a 100644 --- a/server/packages/server-common/tsconfig.json +++ b/server/packages/server-common/tsconfig.json @@ -3,6 +3,9 @@ "target": "es2019", "lib": ["ES2019", "WebWorker"], "module": "commonjs", + "types": ["reflect-metadata", "jest", "node"], + "experimentalDecorators": true, + "emitDecoratorMetadata": true, "moduleResolution": "node", "resolveJsonModule": true, "esModuleInterop": true, diff --git a/server/packages/workflow-tests-language-service/package.json b/server/packages/workflow-tests-language-service/package.json index 5d2176b..b038125 100644 --- a/server/packages/workflow-tests-language-service/package.json +++ b/server/packages/workflow-tests-language-service/package.json @@ -7,6 +7,8 @@ "dependencies": { "@gxwf/server-common": "*", "@gxwf/yaml-language-service": "*", + "inversify": "^6.0.2", + "reflect-metadata": "^0.1.13", "vscode-languageserver": "^8.1.0", "vscode-languageserver-textdocument": "^1.0.8", "vscode-languageserver-types": "^3.17.3", @@ -14,5 +16,8 @@ }, "scripts": { "test": "jest" + }, + "devDependencies": { + "@types/jest": "^29.5.7" } } diff --git a/server/packages/workflow-tests-language-service/src/inversify.config.ts b/server/packages/workflow-tests-language-service/src/inversify.config.ts new file mode 100644 index 0000000..42a38eb --- /dev/null +++ b/server/packages/workflow-tests-language-service/src/inversify.config.ts @@ -0,0 +1,9 @@ +import { ContainerModule } from "inversify"; +import { GxWorkflowTestsLanguageServiceImpl } from "./languageService"; +import { TYPES, WorkflowTestsLanguageService } from "@gxwf/server-common/src/languageTypes"; + +export const WorkflowTestsLanguageServiceContainerModule = new ContainerModule((bind) => { + bind(TYPES.WorkflowTestsLanguageService) + .to(GxWorkflowTestsLanguageServiceImpl) + .inSingletonScope(); +}); diff --git a/server/packages/workflow-tests-language-service/src/languageService.ts b/server/packages/workflow-tests-language-service/src/languageService.ts index 72e4425..c61dd9b 100644 --- a/server/packages/workflow-tests-language-service/src/languageService.ts +++ b/server/packages/workflow-tests-language-service/src/languageService.ts @@ -10,15 +10,18 @@ import { Diagnostic, WorkflowTestsDocument, } from "@gxwf/server-common/src/languageTypes"; -import { YAMLLanguageService, getLanguageService } from "@gxwf/yaml-language-service/src/yamlLanguageService"; +import { YAMLLanguageService } from "@gxwf/yaml-language-service/src/yamlLanguageService"; import { GxWorkflowTestsDocument } from "./document"; +import { inject, injectable } from "inversify"; +import { TYPES as YAML_TYPES } from "@gxwf/yaml-language-service/src/inversify.config"; -export class GxWorkflowTestsLanguageService extends LanguageServiceBase { +@injectable() +export class GxWorkflowTestsLanguageServiceImpl extends LanguageServiceBase { private _yamlLanguageService: YAMLLanguageService; - constructor() { + constructor(@inject(YAML_TYPES.YAMLLanguageService) yamlLanguageService: YAMLLanguageService) { super("gxwftests"); - this._yamlLanguageService = getLanguageService(); + this._yamlLanguageService = yamlLanguageService; } public override parseDocument(document: TextDocument): GxWorkflowTestsDocument { diff --git a/server/packages/workflow-tests-language-service/tsconfig.json b/server/packages/workflow-tests-language-service/tsconfig.json index 0ed94f1..cabf4b8 100644 --- a/server/packages/workflow-tests-language-service/tsconfig.json +++ b/server/packages/workflow-tests-language-service/tsconfig.json @@ -3,6 +3,9 @@ "target": "es2019", "lib": ["ES2019", "WebWorker"], "module": "commonjs", + "types": ["reflect-metadata"], + "experimentalDecorators": true, + "emitDecoratorMetadata": true, "moduleResolution": "node", "resolveJsonModule": true, "esModuleInterop": true, diff --git a/server/packages/yaml-language-service/package.json b/server/packages/yaml-language-service/package.json index 7a999a3..04a64d8 100644 --- a/server/packages/yaml-language-service/package.json +++ b/server/packages/yaml-language-service/package.json @@ -6,6 +6,8 @@ "license": "MIT", "dependencies": { "@gxwf/server-common": "*", + "inversify": "^6.0.2", + "reflect-metadata": "^0.1.13", "vscode-languageserver-textdocument": "^1.0.8", "vscode-languageserver-types": "^3.17.3", "vscode-uri": "^3.0.7", @@ -13,5 +15,8 @@ }, "scripts": { "test": "jest" + }, + "devDependencies": { + "@types/jest": "^29.5.7" } } diff --git a/server/packages/yaml-language-service/src/inversify.config.ts b/server/packages/yaml-language-service/src/inversify.config.ts new file mode 100644 index 0000000..cef7a66 --- /dev/null +++ b/server/packages/yaml-language-service/src/inversify.config.ts @@ -0,0 +1,10 @@ +import { ContainerModule } from "inversify"; +import { YAMLLanguageService, getLanguageService } from "./yamlLanguageService"; + +export const TYPES = { + YAMLLanguageService: Symbol.for("YAMLLanguageService"), +}; + +export const YAMLLanguageServiceContainerModule = new ContainerModule((bind) => { + bind(TYPES.YAMLLanguageService).toConstantValue(getLanguageService()); +}); From 6dde51c3f07fb1c3686359bf59ce1f4cf9792874 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Fri, 10 Nov 2023 12:58:57 +0100 Subject: [PATCH 10/98] Bump some dependencies --- package-lock.json | 403 ++++++++++++++++++++++++++++++---------------- 1 file changed, 265 insertions(+), 138 deletions(-) diff --git a/package-lock.json b/package-lock.json index b523f4d..d606d9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64,18 +64,89 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.6.tgz", - "integrity": "sha512-ZJhac6FkEd1yhG2AHOmfcXG4ceoLltoCVJjN5XsWN9BifBQr+cHJbWi0h68HZuSORq+3WtJ2z0hwF2NG1b5kcA==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dev": true, "dependencies": { - "@babel/highlight": "^7.24.6", - "picocolors": "^1.0.0" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/compat-data": { "version": "7.17.10", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.10.tgz", @@ -125,14 +196,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.6.tgz", - "integrity": "sha512-S7m4eNa6YAPJRHmKsLHIDJhNAGNKoWNiWefz1MBbpnt8g9lvMDl1hir4P9bo/57bQEmuwEhnRU/AMWsD0G/Fbg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.3.tgz", + "integrity": "sha512-keeZWAV4LU3tW0qRi19HRpabC/ilM0HRBBzf9/k8FFiG4KVpiv0FIy4hHfLfFQZNhziCTPTmd59zoyv6DNISzg==", "dev": true, "dependencies": { - "@babel/types": "^7.24.6", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@babel/types": "^7.23.3", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" }, "engines": { @@ -140,14 +211,14 @@ } }, "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", "dev": true, "dependencies": { - "@jridgewell/set-array": "^1.2.1", + "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" + "@jridgewell/trace-mapping": "^0.3.9" }, "engines": { "node": ">=6.0.0" @@ -181,34 +252,34 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.6.tgz", - "integrity": "sha512-Y50Cg3k0LKLMjxdPjIl40SdJgMB85iXn27Vk/qbHZCFx/o5XO3PSnpi675h1KEmmDb6OFArfd5SCQEQ5Q4H88g==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.6.tgz", - "integrity": "sha512-xpeLqeeRkbxhnYimfr2PC+iA0Q7ljX/d1eZ9/inYbmfG2jpl8Lu3DyXvpOAnrS5kxkfOWJjioIMQsaMBXFI05w==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "dependencies": { - "@babel/template": "^7.24.6", - "@babel/types": "^7.24.6" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.6.tgz", - "integrity": "sha512-SF/EMrC3OD7dSta1bLJIlrsVxwtd0UpjRJqLno6125epQMJ/kyFmpTT4pbvPbdQHzCHg+biQ7Syo8lnDtbR+uA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "dependencies": { - "@babel/types": "^7.24.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -267,12 +338,12 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.6.tgz", - "integrity": "sha512-CvLSkwXGWnYlF9+J3iZUvwgAxKiYzK3BWuo+mLzD/MDGOZDj7Gq8+hqaOkMxmJwmlv0iu86uH5fdADd9Hxkymw==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "dependencies": { - "@babel/types": "^7.24.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -288,9 +359,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.6.tgz", - "integrity": "sha512-4yA7s865JHaqUdRbnaxarZREuPTHrjpDT+pXoAZ1yhyo6uFnIEpS8VMu16siFOHDpZNKYv5BObhsB//ycbICyw==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true, "engines": { "node": ">=6.9.0" @@ -320,15 +391,14 @@ } }, "node_modules/@babel/highlight": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.6.tgz", - "integrity": "sha512-2YnuOp4HAk2BsBrJJvYCbItHx0zWscI1C3zgWkz+wDyD9I7GIVrfnLyrR4Y1VR+7p+chAEcrgRQYZAGIKMV7vQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.24.6", + "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "js-tokens": "^4.0.0" }, "engines": { "node": ">=6.9.0" @@ -406,9 +476,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.6.tgz", - "integrity": "sha512-eNZXdfU35nJC2h24RznROuOpO94h6x8sg9ju0tT9biNtLZ2vuP8SduLqqV+/8+cebSLV9SJEAN5Z3zQbJG/M+Q==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.3.tgz", + "integrity": "sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -607,34 +677,34 @@ } }, "node_modules/@babel/template": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.6.tgz", - "integrity": "sha512-3vgazJlLwNXi9jhrR1ef8qiB65L1RK90+lEQwv4OxveHnqC3BfmnHdgySwRLzf6akhlOYenT+b7AfWq+a//AHw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.24.6", - "@babel/parser": "^7.24.6", - "@babel/types": "^7.24.6" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.6.tgz", - "integrity": "sha512-OsNjaJwT9Zn8ozxcfoBc+RaHdj3gFmCmYoQLUII1o6ZrUwku0BMg80FoOTPx+Gi6XhcQxAYE4xyjPTo4SxEQqw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.24.6", - "@babel/generator": "^7.24.6", - "@babel/helper-environment-visitor": "^7.24.6", - "@babel/helper-function-name": "^7.24.6", - "@babel/helper-hoist-variables": "^7.24.6", - "@babel/helper-split-export-declaration": "^7.24.6", - "@babel/parser": "^7.24.6", - "@babel/types": "^7.24.6", - "debug": "^4.3.1", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.3.tgz", + "integrity": "sha512-+K0yF1/9yR0oHdE0StHuEj3uTPzwwbrLGfNOndVJVV2TqA5+j3oljJUb4nmB954FLGjNem976+B+eDuLIjesiQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.3", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.3", + "@babel/types": "^7.23.3", + "debug": "^4.1.0", "globals": "^11.1.0" }, "engines": { @@ -651,13 +721,13 @@ } }, "node_modules/@babel/types": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.6.tgz", - "integrity": "sha512-WaMsgi6Q8zMgMth93GvWPXkhAIEobfsIkLTacoVZoK1J0CevIPGYY2Vo5YvJGqyHqXM6P4ppOYGsIRU8MM9pFQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.3.tgz", + "integrity": "sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.24.6", - "@babel/helper-validator-identifier": "^7.24.6", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -7622,13 +7692,71 @@ } }, "@babel/code-frame": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.6.tgz", - "integrity": "sha512-ZJhac6FkEd1yhG2AHOmfcXG4ceoLltoCVJjN5XsWN9BifBQr+cHJbWi0h68HZuSORq+3WtJ2z0hwF2NG1b5kcA==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dev": true, "requires": { - "@babel/highlight": "^7.24.6", - "picocolors": "^1.0.0" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } } }, "@babel/compat-data": { @@ -7669,26 +7797,26 @@ } }, "@babel/generator": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.6.tgz", - "integrity": "sha512-S7m4eNa6YAPJRHmKsLHIDJhNAGNKoWNiWefz1MBbpnt8g9lvMDl1hir4P9bo/57bQEmuwEhnRU/AMWsD0G/Fbg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.3.tgz", + "integrity": "sha512-keeZWAV4LU3tW0qRi19HRpabC/ilM0HRBBzf9/k8FFiG4KVpiv0FIy4hHfLfFQZNhziCTPTmd59zoyv6DNISzg==", "dev": true, "requires": { - "@babel/types": "^7.24.6", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@babel/types": "^7.23.3", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" }, "dependencies": { "@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", "dev": true, "requires": { - "@jridgewell/set-array": "^1.2.1", + "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" + "@jridgewell/trace-mapping": "^0.3.9" } } } @@ -7714,28 +7842,28 @@ } }, "@babel/helper-environment-visitor": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.6.tgz", - "integrity": "sha512-Y50Cg3k0LKLMjxdPjIl40SdJgMB85iXn27Vk/qbHZCFx/o5XO3PSnpi675h1KEmmDb6OFArfd5SCQEQ5Q4H88g==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true }, "@babel/helper-function-name": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.6.tgz", - "integrity": "sha512-xpeLqeeRkbxhnYimfr2PC+iA0Q7ljX/d1eZ9/inYbmfG2jpl8Lu3DyXvpOAnrS5kxkfOWJjioIMQsaMBXFI05w==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "requires": { - "@babel/template": "^7.24.6", - "@babel/types": "^7.24.6" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" } }, "@babel/helper-hoist-variables": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.6.tgz", - "integrity": "sha512-SF/EMrC3OD7dSta1bLJIlrsVxwtd0UpjRJqLno6125epQMJ/kyFmpTT4pbvPbdQHzCHg+biQ7Syo8lnDtbR+uA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "requires": { - "@babel/types": "^7.24.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-module-imports": { @@ -7779,12 +7907,12 @@ } }, "@babel/helper-split-export-declaration": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.6.tgz", - "integrity": "sha512-CvLSkwXGWnYlF9+J3iZUvwgAxKiYzK3BWuo+mLzD/MDGOZDj7Gq8+hqaOkMxmJwmlv0iu86uH5fdADd9Hxkymw==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "requires": { - "@babel/types": "^7.24.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-string-parser": { @@ -7794,9 +7922,9 @@ "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.6.tgz", - "integrity": "sha512-4yA7s865JHaqUdRbnaxarZREuPTHrjpDT+pXoAZ1yhyo6uFnIEpS8VMu16siFOHDpZNKYv5BObhsB//ycbICyw==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true }, "@babel/helper-validator-option": { @@ -7817,15 +7945,14 @@ } }, "@babel/highlight": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.6.tgz", - "integrity": "sha512-2YnuOp4HAk2BsBrJJvYCbItHx0zWscI1C3zgWkz+wDyD9I7GIVrfnLyrR4Y1VR+7p+chAEcrgRQYZAGIKMV7vQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.24.6", + "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "js-tokens": "^4.0.0" }, "dependencies": { "ansi-styles": { @@ -7887,9 +8014,9 @@ } }, "@babel/parser": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.6.tgz", - "integrity": "sha512-eNZXdfU35nJC2h24RznROuOpO94h6x8sg9ju0tT9biNtLZ2vuP8SduLqqV+/8+cebSLV9SJEAN5Z3zQbJG/M+Q==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.3.tgz", + "integrity": "sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==", "dev": true }, "@babel/plugin-syntax-async-generators": { @@ -8028,31 +8155,31 @@ } }, "@babel/template": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.6.tgz", - "integrity": "sha512-3vgazJlLwNXi9jhrR1ef8qiB65L1RK90+lEQwv4OxveHnqC3BfmnHdgySwRLzf6akhlOYenT+b7AfWq+a//AHw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "requires": { - "@babel/code-frame": "^7.24.6", - "@babel/parser": "^7.24.6", - "@babel/types": "^7.24.6" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" } }, "@babel/traverse": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.6.tgz", - "integrity": "sha512-OsNjaJwT9Zn8ozxcfoBc+RaHdj3gFmCmYoQLUII1o6ZrUwku0BMg80FoOTPx+Gi6XhcQxAYE4xyjPTo4SxEQqw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.24.6", - "@babel/generator": "^7.24.6", - "@babel/helper-environment-visitor": "^7.24.6", - "@babel/helper-function-name": "^7.24.6", - "@babel/helper-hoist-variables": "^7.24.6", - "@babel/helper-split-export-declaration": "^7.24.6", - "@babel/parser": "^7.24.6", - "@babel/types": "^7.24.6", - "debug": "^4.3.1", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.3.tgz", + "integrity": "sha512-+K0yF1/9yR0oHdE0StHuEj3uTPzwwbrLGfNOndVJVV2TqA5+j3oljJUb4nmB954FLGjNem976+B+eDuLIjesiQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.3", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.3", + "@babel/types": "^7.23.3", + "debug": "^4.1.0", "globals": "^11.1.0" }, "dependencies": { @@ -8065,13 +8192,13 @@ } }, "@babel/types": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.6.tgz", - "integrity": "sha512-WaMsgi6Q8zMgMth93GvWPXkhAIEobfsIkLTacoVZoK1J0CevIPGYY2Vo5YvJGqyHqXM6P4ppOYGsIRU8MM9pFQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.3.tgz", + "integrity": "sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==", "dev": true, "requires": { - "@babel/helper-string-parser": "^7.24.6", - "@babel/helper-validator-identifier": "^7.24.6", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } }, From cd7f18429f8884705b35c3f489358cefbfb677a3 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Mon, 13 Nov 2023 23:16:37 +0100 Subject: [PATCH 11/98] Fix schema import in native server --- server/gx-workflow-ls-native/src/languageService.ts | 2 +- server/gx-workflow-ls-native/tsconfig.json | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/server/gx-workflow-ls-native/src/languageService.ts b/server/gx-workflow-ls-native/src/languageService.ts index 745d28c..65a2342 100644 --- a/server/gx-workflow-ls-native/src/languageService.ts +++ b/server/gx-workflow-ls-native/src/languageService.ts @@ -19,7 +19,7 @@ import { LanguageServiceBase, LanguageService, } from "@gxwf/server-common/src/languageTypes"; -import NativeWorkflowSchema from "../../../workflow-languages/schemas/native.schema.json"; +import NativeWorkflowSchema from "@schemas/native.schema.json"; import { NativeWorkflowDocument } from "./nativeWorkflowDocument"; import { injectable } from "inversify"; diff --git a/server/gx-workflow-ls-native/tsconfig.json b/server/gx-workflow-ls-native/tsconfig.json index 79c4cdb..0929ac2 100644 --- a/server/gx-workflow-ls-native/tsconfig.json +++ b/server/gx-workflow-ls-native/tsconfig.json @@ -11,7 +11,10 @@ "esModuleInterop": true, "sourceMap": true, "strict": true, - "rootDirs": ["src", "tests"] + "rootDirs": ["src", "tests"], + "paths": { + "@schemas/*": ["../../workflow-languages/schemas/*"] + } }, "include": ["src/**/*.ts", "tests/**/*.ts"] } From 6650dfdc4f225df96a994849420572b4d864cb07 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Wed, 29 Nov 2023 17:19:40 +0100 Subject: [PATCH 12/98] Remove unnecessary reference and import in versions.ts --- server/gx-workflow-ls-format2/src/schema/versions.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/server/gx-workflow-ls-format2/src/schema/versions.ts b/server/gx-workflow-ls-format2/src/schema/versions.ts index ed737d6..06736f2 100644 --- a/server/gx-workflow-ls-format2/src/schema/versions.ts +++ b/server/gx-workflow-ls-format2/src/schema/versions.ts @@ -1,7 +1,3 @@ -//TODO: remove this reference when https://github.com/sumwatshade/jest-transform-yaml/pull/20 is merged and upgrade to jest 28 -// eslint-disable-next-line @typescript-eslint/triple-slash-reference -/// - import schema_v19_09_workflows from "@schemas/gxformat2/v19_09/workflows.yaml"; import schema_common_metaschema_base from "@schemas/gxformat2/common/metaschema/metaschema_base.yaml"; import schema_v19_09_process from "@schemas/gxformat2/v19_09/process.yaml"; From b670e174894fbdff3e1b3ae97c4b26e5a062f338 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Wed, 29 Nov 2023 17:29:11 +0100 Subject: [PATCH 13/98] Rever mapped path import for JSON schema For some reason webpack doesn't seem to find the mapping... --- server/gx-workflow-ls-native/src/languageService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/gx-workflow-ls-native/src/languageService.ts b/server/gx-workflow-ls-native/src/languageService.ts index 65a2342..745d28c 100644 --- a/server/gx-workflow-ls-native/src/languageService.ts +++ b/server/gx-workflow-ls-native/src/languageService.ts @@ -19,7 +19,7 @@ import { LanguageServiceBase, LanguageService, } from "@gxwf/server-common/src/languageTypes"; -import NativeWorkflowSchema from "@schemas/native.schema.json"; +import NativeWorkflowSchema from "../../../workflow-languages/schemas/native.schema.json"; import { NativeWorkflowDocument } from "./nativeWorkflowDocument"; import { injectable } from "inversify"; From 047fecba4b79329e32af13b43f41ca648704497f Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Wed, 29 Nov 2023 17:30:09 +0100 Subject: [PATCH 14/98] Update @types/jest to version 29.5.8 --- package-lock.json | 14 ++++---- package.json | 2 +- server/gx-workflow-ls-format2/package.json | 2 +- server/gx-workflow-ls-native/package.json | 2 +- server/package-lock.json | 32 +++++++++---------- server/packages/server-common/package.json | 2 +- .../package.json | 2 +- .../yaml-language-service/package.json | 2 +- 8 files changed, 29 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index d606d9b..b32f4ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "hasInstallScript": true, "license": "MIT", "devDependencies": { - "@types/jest": "^29.5.4", + "@types/jest": "^29.5.8", "@types/mocha": "^10.0.1", "@types/vscode": "^1.81.0", "@types/webpack-env": "^1.18.1", @@ -1635,9 +1635,9 @@ } }, "node_modules/@types/jest": { - "version": "29.5.4", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.4.tgz", - "integrity": "sha512-PhglGmhWeD46FYOVLt3X7TiWjzwuVGW9wG/4qocPevXMjCmrIc5b6db9WjeGE4QYVpUAWMDv3v0IiBwObY289A==", + "version": "29.5.8", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.8.tgz", + "integrity": "sha512-fXEFTxMV2Co8ZF5aYFJv+YeA08RTYJfhtN5c9JSv/mFEMe+xxjufCb+PHL+bJcMs/ebPUsBu+UNTEz+ydXrR6g==", "dev": true, "dependencies": { "expect": "^29.0.0", @@ -8919,9 +8919,9 @@ } }, "@types/jest": { - "version": "29.5.4", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.4.tgz", - "integrity": "sha512-PhglGmhWeD46FYOVLt3X7TiWjzwuVGW9wG/4qocPevXMjCmrIc5b6db9WjeGE4QYVpUAWMDv3v0IiBwObY289A==", + "version": "29.5.8", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.8.tgz", + "integrity": "sha512-fXEFTxMV2Co8ZF5aYFJv+YeA08RTYJfhtN5c9JSv/mFEMe+xxjufCb+PHL+bJcMs/ebPUsBu+UNTEz+ydXrR6g==", "dev": true, "requires": { "expect": "^29.0.0", diff --git a/package.json b/package.json index 3b61fca..39b4418 100644 --- a/package.json +++ b/package.json @@ -230,7 +230,7 @@ "test:e2e": "node ./client/out/e2e/runTests.js" }, "devDependencies": { - "@types/jest": "^29.5.4", + "@types/jest": "^29.5.8", "@types/mocha": "^10.0.1", "@types/vscode": "^1.81.0", "@types/webpack-env": "^1.18.1", diff --git a/server/gx-workflow-ls-format2/package.json b/server/gx-workflow-ls-format2/package.json index 3fbea98..0f21b4c 100644 --- a/server/gx-workflow-ls-format2/package.json +++ b/server/gx-workflow-ls-format2/package.json @@ -17,6 +17,6 @@ }, "scripts": {}, "devDependencies": { - "@types/jest": "^29.5.7" + "@types/jest": "^29.5.8" } } diff --git a/server/gx-workflow-ls-native/package.json b/server/gx-workflow-ls-native/package.json index 2345de4..5f28d56 100644 --- a/server/gx-workflow-ls-native/package.json +++ b/server/gx-workflow-ls-native/package.json @@ -22,6 +22,6 @@ "test-unit": "jest" }, "devDependencies": { - "@types/jest": "^29.5.7" + "@types/jest": "^29.5.8" } } diff --git a/server/package-lock.json b/server/package-lock.json index 8ef611f..c6c1955 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -29,7 +29,7 @@ "vscode-uri": "^3.0.7" }, "devDependencies": { - "@types/jest": "^29.5.7" + "@types/jest": "^29.5.8" } }, "gx-workflow-ls-native": { @@ -48,7 +48,7 @@ "vscode-uri": "^3.0.7" }, "devDependencies": { - "@types/jest": "^29.5.7" + "@types/jest": "^29.5.8" } }, "node_modules/@babel/code-frame": { @@ -313,9 +313,9 @@ } }, "node_modules/@types/jest": { - "version": "29.5.7", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.7.tgz", - "integrity": "sha512-HLyetab6KVPSiF+7pFcUyMeLsx25LDNDemw9mGsJBkai/oouwrjTycocSDYopMEwFhN2Y4s9oPyOCZNofgSt2g==", + "version": "29.5.8", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.8.tgz", + "integrity": "sha512-fXEFTxMV2Co8ZF5aYFJv+YeA08RTYJfhtN5c9JSv/mFEMe+xxjufCb+PHL+bJcMs/ebPUsBu+UNTEz+ydXrR6g==", "dev": true, "dependencies": { "expect": "^29.0.0", @@ -784,7 +784,7 @@ "vscode-uri": "^3.0.7" }, "devDependencies": { - "@types/jest": "^29.5.7" + "@types/jest": "^29.5.8" } }, "packages/workflow-tests-language-service": { @@ -802,7 +802,7 @@ "vscode-uri": "^3.0.7" }, "devDependencies": { - "@types/jest": "^29.5.7" + "@types/jest": "^29.5.8" } }, "packages/yaml-language-service": { @@ -819,7 +819,7 @@ "yaml": "^2.3.2" }, "devDependencies": { - "@types/jest": "^29.5.7" + "@types/jest": "^29.5.8" } } }, @@ -970,7 +970,7 @@ "@gxwf/server-common": { "version": "file:packages/server-common", "requires": { - "@types/jest": "^29.5.7", + "@types/jest": "^29.5.8", "@types/node": "^20.5.7", "inversify": "^6.0.2", "reflect-metadata": "^0.1.13", @@ -986,7 +986,7 @@ "requires": { "@gxwf/server-common": "*", "@gxwf/yaml-language-service": "*", - "@types/jest": "^29.5.7", + "@types/jest": "^29.5.8", "inversify": "^6.0.2", "reflect-metadata": "^0.1.13", "vscode-languageserver": "^8.1.0", @@ -999,7 +999,7 @@ "version": "file:packages/yaml-language-service", "requires": { "@gxwf/server-common": "*", - "@types/jest": "^29.5.7", + "@types/jest": "^29.5.8", "inversify": "^6.0.2", "reflect-metadata": "^0.1.13", "vscode-languageserver-textdocument": "^1.0.8", @@ -1071,9 +1071,9 @@ } }, "@types/jest": { - "version": "29.5.7", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.7.tgz", - "integrity": "sha512-HLyetab6KVPSiF+7pFcUyMeLsx25LDNDemw9mGsJBkai/oouwrjTycocSDYopMEwFhN2Y4s9oPyOCZNofgSt2g==", + "version": "29.5.8", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.8.tgz", + "integrity": "sha512-fXEFTxMV2Co8ZF5aYFJv+YeA08RTYJfhtN5c9JSv/mFEMe+xxjufCb+PHL+bJcMs/ebPUsBu+UNTEz+ydXrR6g==", "dev": true, "requires": { "expect": "^29.0.0", @@ -1206,7 +1206,7 @@ "@gxwf/server-common": "*", "@gxwf/workflow-tests-language-service": "*", "@gxwf/yaml-language-service": "*", - "@types/jest": "^29.5.7", + "@types/jest": "^29.5.8", "inversify": "^6.0.2", "reflect-metadata": "^0.1.13", "vscode-languageserver": "^8.1.0", @@ -1220,7 +1220,7 @@ "requires": { "@gxwf/server-common": "*", "@gxwf/workflow-tests-language-service": "*", - "@types/jest": "^29.5.7", + "@types/jest": "^29.5.8", "inversify": "^6.0.2", "jsonc-parser": "^3.2.0", "reflect-metadata": "^0.1.13", diff --git a/server/packages/server-common/package.json b/server/packages/server-common/package.json index 150c8b4..2ba2876 100644 --- a/server/packages/server-common/package.json +++ b/server/packages/server-common/package.json @@ -17,6 +17,6 @@ }, "scripts": {}, "devDependencies": { - "@types/jest": "^29.5.7" + "@types/jest": "^29.5.8" } } diff --git a/server/packages/workflow-tests-language-service/package.json b/server/packages/workflow-tests-language-service/package.json index b038125..c1a1c8b 100644 --- a/server/packages/workflow-tests-language-service/package.json +++ b/server/packages/workflow-tests-language-service/package.json @@ -18,6 +18,6 @@ "test": "jest" }, "devDependencies": { - "@types/jest": "^29.5.7" + "@types/jest": "^29.5.8" } } diff --git a/server/packages/yaml-language-service/package.json b/server/packages/yaml-language-service/package.json index 04a64d8..6c7bb09 100644 --- a/server/packages/yaml-language-service/package.json +++ b/server/packages/yaml-language-service/package.json @@ -17,6 +17,6 @@ "test": "jest" }, "devDependencies": { - "@types/jest": "^29.5.7" + "@types/jest": "^29.5.8" } } From a6263971f40a0cf1d8e128034f0ed1306483f33b Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Wed, 29 Nov 2023 17:31:16 +0100 Subject: [PATCH 15/98] Fix issue with jest and decorators Add experimental decorators and emit decorator metadata to tsconfig.json --- server/tsconfig.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/server/tsconfig.json b/server/tsconfig.json index 5200861..244f2a1 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -3,6 +3,8 @@ "target": "es2019", "lib": ["ES2019", "WebWorker"], "module": "commonjs", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, "moduleResolution": "node", "resolveJsonModule": true, "esModuleInterop": true, @@ -13,9 +15,6 @@ "@schemas/*": ["../workflow-languages/schemas/*"] } }, - "files": [], - "include": [], - "exclude": [], "references": [ { "path": "packages/server-common" From 0dfe36c8a556c93d820b6cd5ae2096ef25b31d18 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Mon, 4 Dec 2023 14:49:27 +0100 Subject: [PATCH 16/98] Fix onConfigurationChanged event triggering --- server/packages/server-common/src/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/packages/server-common/src/server.ts b/server/packages/server-common/src/server.ts index ab416fc..2df10d9 100644 --- a/server/packages/server-common/src/server.ts +++ b/server/packages/server-common/src/server.ts @@ -67,7 +67,7 @@ export class GalaxyWorkflowLanguageServerImpl implements GalaxyWorkflowLanguageS } private async initialize(params: InitializeParams): Promise { - this.configService.initialize(params.capabilities, this.onConfigurationChanged); + this.configService.initialize(params.capabilities, () => this.onConfigurationChanged()); this.workspaceFolders = params.workspaceFolders; const capabilities: ServerCapabilities = { From 5df6adf81b355443acc4cc572346930db858d050 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sat, 9 Dec 2023 00:07:36 +0100 Subject: [PATCH 17/98] Add provisional schema for wf tests --- workflow-languages/schemas/tests.schema.json | 2490 ++++++++++++++++++ 1 file changed, 2490 insertions(+) create mode 100644 workflow-languages/schemas/tests.schema.json diff --git a/workflow-languages/schemas/tests.schema.json b/workflow-languages/schemas/tests.schema.json new file mode 100644 index 0000000..8b1d936 --- /dev/null +++ b/workflow-languages/schemas/tests.schema.json @@ -0,0 +1,2490 @@ +{ + "$defs": { + "AssertAttributeIs": { + "additionalProperties": false, + "properties": { + "path": { + "description": "Path to check for. Valid paths are the simplified subsets of XPath implemented by lxml.etree; https://lxml.de/xpathxslt.html for more information.", + "title": "Path", + "type": "string" + }, + "text": { + "description": "Text to check for.", + "title": "Text", + "type": "string" + }, + "negate": { + "allOf": [ + { + "$ref": "#/$defs/PermissiveBoolean" + } + ], + "default": "false", + "description": "Negate the outcome of the assertion." + } + }, + "required": ["path", "text"], + "title": "AssertAttributeIs", + "type": "object" + }, + "AssertAttributeMatches": { + "additionalProperties": false, + "properties": { + "path": { + "description": "Path to check for. Valid paths are the simplified subsets of XPath implemented by lxml.etree; https://lxml.de/xpathxslt.html for more information.", + "title": "Path", + "type": "string" + }, + "expression": { + "description": "The regular expression to use.", + "title": "Expression", + "type": "string" + }, + "negate": { + "allOf": [ + { + "$ref": "#/$defs/PermissiveBoolean" + } + ], + "default": "false", + "description": "Negate the outcome of the assertion." + } + }, + "required": ["path", "expression"], + "title": "AssertAttributeMatches", + "type": "object" + }, + "AssertElementText": { + "additionalProperties": false, + "properties": { + "has_text": { + "items": { + "$ref": "#/$defs/AssertHasText" + }, + "title": "Has Text", + "type": "array" + }, + "not_has_text": { + "items": { + "$ref": "#/$defs/AssertNotHasText" + }, + "title": "Not Has Text", + "type": "array" + }, + "has_text_matching": { + "items": { + "$ref": "#/$defs/AssertHasTextMatching" + }, + "title": "Has Text Matching", + "type": "array" + }, + "has_line": { + "items": { + "$ref": "#/$defs/AssertHasLine" + }, + "title": "Has Line", + "type": "array" + }, + "has_line_matching": { + "items": { + "$ref": "#/$defs/AssertHasLineMatching" + }, + "title": "Has Line Matching", + "type": "array" + }, + "has_n_lines": { + "items": { + "$ref": "#/$defs/AssertHasNlines" + }, + "title": "Has N Lines", + "type": "array" + }, + "path": { + "description": "Path to check for. Valid paths are the simplified subsets of XPath implemented by lxml.etree; https://lxml.de/xpathxslt.html for more information.", + "title": "Path", + "type": "string" + }, + "negate": { + "allOf": [ + { + "$ref": "#/$defs/PermissiveBoolean" + } + ], + "default": "false", + "description": "Negate the outcome of the assertion." + } + }, + "required": ["path"], + "title": "AssertElementText", + "type": "object" + }, + "AssertElementTextIs": { + "additionalProperties": false, + "properties": { + "path": { + "description": "Path to check for. Valid paths are the simplified subsets of XPath implemented by lxml.etree; https://lxml.de/xpathxslt.html for more information.", + "title": "Path", + "type": "string" + }, + "text": { + "description": "Text to check for.", + "title": "Text", + "type": "string" + }, + "negate": { + "allOf": [ + { + "$ref": "#/$defs/PermissiveBoolean" + } + ], + "default": "false", + "description": "Negate the outcome of the assertion." + } + }, + "required": ["path", "text"], + "title": "AssertElementTextIs", + "type": "object" + }, + "AssertElementTextMatches": { + "additionalProperties": false, + "properties": { + "path": { + "description": "Path to check for. Valid paths are the simplified subsets of XPath implemented by lxml.etree; https://lxml.de/xpathxslt.html for more information.", + "title": "Path", + "type": "string" + }, + "expression": { + "description": "The regular expression to use.", + "title": "Expression", + "type": "string" + }, + "negate": { + "allOf": [ + { + "$ref": "#/$defs/PermissiveBoolean" + } + ], + "default": "false", + "description": "Negate the outcome of the assertion." + } + }, + "required": ["path", "expression"], + "title": "AssertElementTextMatches", + "type": "object" + }, + "AssertHasArchiveMember": { + "additionalProperties": false, + "properties": { + "has_size": { + "items": { + "$ref": "#/$defs/AssertHasSize" + }, + "title": "Has Size", + "type": "array" + }, + "has_text": { + "items": { + "$ref": "#/$defs/AssertHasText" + }, + "title": "Has Text", + "type": "array" + }, + "not_has_text": { + "items": { + "$ref": "#/$defs/AssertNotHasText" + }, + "title": "Not Has Text", + "type": "array" + }, + "has_text_matching": { + "items": { + "$ref": "#/$defs/AssertHasTextMatching" + }, + "title": "Has Text Matching", + "type": "array" + }, + "has_line": { + "items": { + "$ref": "#/$defs/AssertHasLine" + }, + "title": "Has Line", + "type": "array" + }, + "has_line_matching": { + "items": { + "$ref": "#/$defs/AssertHasLineMatching" + }, + "title": "Has Line Matching", + "type": "array" + }, + "has_n_lines": { + "items": { + "$ref": "#/$defs/AssertHasNlines" + }, + "title": "Has N Lines", + "type": "array" + }, + "has_n_columns": { + "items": { + "$ref": "#/$defs/AssertHasNcolumns" + }, + "title": "Has N Columns", + "type": "array" + }, + "has_json_property_with_value": { + "items": { + "$ref": "#/$defs/AssertHasJsonPropertyWithValue" + }, + "title": "Has Json Property With Value", + "type": "array" + }, + "has_json_property_with_text": { + "items": { + "$ref": "#/$defs/AssertHasJsonPropertyWithText" + }, + "title": "Has Json Property With Text", + "type": "array" + }, + "is_valid_xml": { + "items": { + "$ref": "#/$defs/AssertIsValidXml" + }, + "title": "Is Valid Xml", + "type": "array" + }, + "xml_element": { + "items": { + "$ref": "#/$defs/AssertXmlelement" + }, + "title": "Xml Element", + "type": "array" + }, + "has_element_with_path": { + "items": { + "$ref": "#/$defs/AssertHasElementWithPath" + }, + "title": "Has Element With Path", + "type": "array" + }, + "has_n_elements_with_path": { + "items": { + "$ref": "#/$defs/AssertHasNelementsWithPath" + }, + "title": "Has N Elements With Path", + "type": "array" + }, + "element_text_matches": { + "items": { + "$ref": "#/$defs/AssertElementTextMatches" + }, + "title": "Element Text Matches", + "type": "array" + }, + "element_text_is": { + "items": { + "$ref": "#/$defs/AssertElementTextIs" + }, + "title": "Element Text Is", + "type": "array" + }, + "attribute_matches": { + "items": { + "$ref": "#/$defs/AssertAttributeMatches" + }, + "title": "Attribute Matches", + "type": "array" + }, + "attribute_is": { + "items": { + "$ref": "#/$defs/AssertAttributeIs" + }, + "title": "Attribute Is", + "type": "array" + }, + "element_text": { + "items": { + "$ref": "#/$defs/AssertElementText" + }, + "title": "Element Text", + "type": "array" + }, + "has_h5_keys": { + "items": { + "$ref": "#/$defs/AssertHasH5Keys" + }, + "title": "Has H5 Keys", + "type": "array" + }, + "has_h5_attribute": { + "items": { + "$ref": "#/$defs/AssertHasH5Attribute" + }, + "title": "Has H5 Attribute", + "type": "array" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The regular expression specifying the archive member.", + "title": "Path" + }, + "all": { + "allOf": [ + { + "$ref": "#/$defs/PermissiveBoolean" + } + ], + "default": "false", + "description": "Check the sub-assertions for all paths matching the path. Default: false, i.e. only the first" + }, + "n": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Desired number, can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "N" + }, + "delta": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Allowed difference with respect to n (default: 0), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Delta" + }, + "min": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Minimum number (default: -infinity), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Min" + }, + "max": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Maximum number (default: infinity), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Max" + } + }, + "title": "AssertHasArchiveMember", + "type": "object" + }, + "AssertHasElementWithPath": { + "additionalProperties": false, + "properties": { + "path": { + "description": "Path to check for. Valid paths are the simplified subsets of XPath implemented by lxml.etree; https://lxml.de/xpathxslt.html for more information.", + "title": "Path", + "type": "string" + }, + "negate": { + "allOf": [ + { + "$ref": "#/$defs/PermissiveBoolean" + } + ], + "default": "false", + "description": "Negate the outcome of the assertion." + } + }, + "required": ["path"], + "title": "AssertHasElementWithPath", + "type": "object" + }, + "AssertHasH5Attribute": { + "additionalProperties": false, + "properties": { + "key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "HDF5 attribute to check value of.", + "title": "Key" + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Expected value of HDF5 attribute to check.", + "title": "Value" + } + }, + "title": "AssertHasH5Attribute", + "type": "object" + }, + "AssertHasH5Keys": { + "additionalProperties": false, + "properties": { + "keys": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Comma-separated list of HDF5 attributes to check for.", + "title": "Keys" + } + }, + "title": "AssertHasH5Keys", + "type": "object" + }, + "AssertHasJsonPropertyWithText": { + "additionalProperties": false, + "properties": { + "property": { + "description": "JSON property to search the target for.", + "title": "Property", + "type": "string" + }, + "text": { + "description": "Text value to search for.", + "title": "Text", + "type": "string" + } + }, + "required": ["property", "text"], + "title": "AssertHasJsonPropertyWithText", + "type": "object" + }, + "AssertHasJsonPropertyWithValue": { + "additionalProperties": false, + "properties": { + "property": { + "description": "JSON property to search the target for.", + "title": "Property", + "type": "string" + }, + "value": { + "description": "JSON-ified value to search for. This will be converted from an XML string to JSON with Python's json.loads function.", + "title": "Value", + "type": "string" + } + }, + "required": ["property", "value"], + "title": "AssertHasJsonPropertyWithValue", + "type": "object" + }, + "AssertHasLine": { + "additionalProperties": false, + "properties": { + "line": { + "description": "The line to check for", + "title": "Line", + "type": "string" + }, + "n": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Desired number, can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "N" + }, + "delta": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Allowed difference with respect to n (default: 0), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Delta" + }, + "min": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Minimum number (default: -infinity), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Min" + }, + "max": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Maximum number (default: infinity), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Max" + }, + "negate": { + "allOf": [ + { + "$ref": "#/$defs/PermissiveBoolean" + } + ], + "default": "false", + "description": "Negate the outcome of the assertion." + } + }, + "required": ["line"], + "title": "AssertHasLine", + "type": "object" + }, + "AssertHasLineMatching": { + "additionalProperties": false, + "properties": { + "expression": { + "description": "Regular expression to check for", + "title": "Expression", + "type": "string" + }, + "n": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Desired number, can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "N" + }, + "delta": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Allowed difference with respect to n (default: 0), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Delta" + }, + "min": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Minimum number (default: -infinity), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Min" + }, + "max": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Maximum number (default: infinity), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Max" + }, + "negate": { + "allOf": [ + { + "$ref": "#/$defs/PermissiveBoolean" + } + ], + "default": "false", + "description": "Negate the outcome of the assertion." + } + }, + "required": ["expression"], + "title": "AssertHasLineMatching", + "type": "object" + }, + "AssertHasNcolumns": { + "additionalProperties": false, + "properties": { + "n": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Desired number, can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "N" + }, + "delta": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Allowed difference with respect to n (default: 0), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Delta" + }, + "min": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Minimum number (default: -infinity), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Min" + }, + "max": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Maximum number (default: infinity), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Max" + }, + "negate": { + "allOf": [ + { + "$ref": "#/$defs/PermissiveBoolean" + } + ], + "default": "false", + "description": "Negate the outcome of the assertion." + }, + "sep": { + "default": " ", + "description": "Separator defining columns, default: tab", + "title": "Sep", + "type": "string" + }, + "comment": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Comment character(s) used to skip comment lines (which should not be used for counting columns)", + "title": "Comment" + } + }, + "title": "AssertHasNcolumns", + "type": "object" + }, + "AssertHasNelementsWithPath": { + "additionalProperties": false, + "properties": { + "path": { + "description": "Path to check for. Valid paths are the simplified subsets of XPath implemented by lxml.etree; https://lxml.de/xpathxslt.html for more information.", + "title": "Path", + "type": "string" + }, + "n": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Desired number, can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "N" + }, + "delta": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Allowed difference with respect to n (default: 0), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Delta" + }, + "min": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Minimum number (default: -infinity), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Min" + }, + "max": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Maximum number (default: infinity), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Max" + }, + "negate": { + "allOf": [ + { + "$ref": "#/$defs/PermissiveBoolean" + } + ], + "default": "false", + "description": "Negate the outcome of the assertion." + } + }, + "required": ["path"], + "title": "AssertHasNelementsWithPath", + "type": "object" + }, + "AssertHasNlines": { + "additionalProperties": false, + "properties": { + "n": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Desired number, can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "N" + }, + "delta": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Allowed difference with respect to n (default: 0), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Delta" + }, + "min": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Minimum number (default: -infinity), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Min" + }, + "max": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Maximum number (default: infinity), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Max" + }, + "negate": { + "allOf": [ + { + "$ref": "#/$defs/PermissiveBoolean" + } + ], + "default": "false", + "description": "Negate the outcome of the assertion." + } + }, + "title": "AssertHasNlines", + "type": "object" + }, + "AssertHasSize": { + "additionalProperties": false, + "properties": { + "value": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Desired size of the output (in bytes), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Value" + }, + "delta": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Maximum allowed size difference (default is 0). The observed size has to be in the range ``value +- delta``. Can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Delta" + }, + "min": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Minimum expected size, can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Min" + }, + "max": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Maximum expected size, can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Max" + }, + "negate": { + "allOf": [ + { + "$ref": "#/$defs/PermissiveBoolean" + } + ], + "default": "false", + "description": "Negate the outcome of the assertion." + } + }, + "title": "AssertHasSize", + "type": "object" + }, + "AssertHasText": { + "additionalProperties": false, + "properties": { + "text": { + "description": "Text to check for", + "title": "Text", + "type": "string" + }, + "n": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Desired number, can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "N" + }, + "delta": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Allowed difference with respect to n (default: 0), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Delta" + }, + "min": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Minimum number (default: -infinity), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Min" + }, + "max": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Maximum number (default: infinity), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Max" + }, + "negate": { + "allOf": [ + { + "$ref": "#/$defs/PermissiveBoolean" + } + ], + "default": "false", + "description": "Negate the outcome of the assertion." + } + }, + "required": ["text"], + "title": "AssertHasText", + "type": "object" + }, + "AssertHasTextMatching": { + "additionalProperties": false, + "properties": { + "expression": { + "description": "Regular expression to check for", + "title": "Expression", + "type": "string" + }, + "n": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Desired number, can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "N" + }, + "delta": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Allowed difference with respect to n (default: 0), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Delta" + }, + "min": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Minimum number (default: -infinity), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Min" + }, + "max": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Maximum number (default: infinity), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Max" + }, + "negate": { + "allOf": [ + { + "$ref": "#/$defs/PermissiveBoolean" + } + ], + "default": "false", + "description": "Negate the outcome of the assertion." + } + }, + "required": ["expression"], + "title": "AssertHasTextMatching", + "type": "object" + }, + "AssertIsValidXml": { + "additionalProperties": false, + "properties": {}, + "title": "AssertIsValidXml", + "type": "object" + }, + "AssertNotHasText": { + "additionalProperties": false, + "properties": { + "text": { + "description": "Text to check for", + "title": "Text", + "type": "string" + } + }, + "required": ["text"], + "title": "AssertNotHasText", + "type": "object" + }, + "AssertXmlelement": { + "additionalProperties": false, + "properties": { + "path": { + "description": "Path to check for. Valid paths are the simplified subsets of XPath implemented by lxml.etree; https://lxml.de/xpathxslt.html for more information.", + "title": "Path", + "type": "string" + }, + "all": { + "allOf": [ + { + "$ref": "#/$defs/PermissiveBoolean" + } + ], + "default": "false", + "description": "Check the sub-assertions for all paths matching the path. Default: false, i.e. only the first" + }, + "attribute": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The name of the attribute to apply sub-assertion on. If not given then the element text is used", + "title": "Attribute" + }, + "n": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Desired number, can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "N" + }, + "delta": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Allowed difference with respect to n (default: 0), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Delta" + }, + "min": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Minimum number (default: -infinity), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Min" + }, + "max": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Maximum number (default: infinity), can be suffixed by ``(k|M|G|T|P|E)i?``", + "title": "Max" + }, + "negate": { + "allOf": [ + { + "$ref": "#/$defs/PermissiveBoolean" + } + ], + "default": "false", + "description": "Negate the outcome of the assertion." + } + }, + "required": ["path"], + "title": "AssertXmlelement", + "type": "object" + }, + "PermissiveBoolean": { + "description": "Documentation for PermissiveBoolean.", + "enum": ["0", "1", "true", "false", "True", "False", "yes", "no"], + "title": "PermissiveBoolean", + "type": "string" + }, + "Test": { + "additionalProperties": false, + "properties": { + "doc": { + "title": "Doc", + "type": "string" + }, + "job": { + "title": "Job", + "type": "string" + }, + "outputs": { + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/TestOutput" + }, + { + "$ref": "#/$defs/TestOutputCollection" + } + ] + }, + "title": "Outputs", + "type": "object" + } + }, + "required": ["doc", "job", "outputs"], + "title": "Test", + "type": "object" + }, + "TestAssertions": { + "additionalProperties": false, + "properties": { + "has_size": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasSize" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasSize" + } + ], + "title": "Has Size" + }, + "has_text": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasText" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasText" + } + ], + "title": "Has Text" + }, + "not_has_text": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertNotHasText" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertNotHasText" + } + ], + "title": "Not Has Text" + }, + "has_text_matching": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasTextMatching" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasTextMatching" + } + ], + "title": "Has Text Matching" + }, + "has_line": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasLine" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasLine" + } + ], + "title": "Has Line" + }, + "has_line_matching": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasLineMatching" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasLineMatching" + } + ], + "title": "Has Line Matching" + }, + "has_n_lines": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasNlines" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasNlines" + } + ], + "title": "Has N Lines" + }, + "has_n_columns": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasNcolumns" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasNcolumns" + } + ], + "title": "Has N Columns" + }, + "has_archive_member": { + "items": { + "$ref": "#/$defs/AssertHasArchiveMember" + }, + "title": "Has Archive Member", + "type": "array" + }, + "is_valid_xml": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertIsValidXml" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertIsValidXml" + } + ], + "title": "Is Valid Xml" + }, + "xml_element": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertXmlelement" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertXmlelement" + } + ], + "title": "Xml Element" + }, + "has_element_with_path": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasElementWithPath" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasElementWithPath" + } + ], + "title": "Has Element With Path" + }, + "has_n_elements_with_path": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasNelementsWithPath" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasNelementsWithPath" + } + ], + "title": "Has N Elements With Path" + }, + "element_text_matches": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertElementTextMatches" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertElementTextMatches" + } + ], + "title": "Element Text Matches" + }, + "element_text_is": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertElementTextIs" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertElementTextIs" + } + ], + "title": "Element Text Is" + }, + "attribute_matches": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertAttributeMatches" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertAttributeMatches" + } + ], + "title": "Attribute Matches" + }, + "attribute_is": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertAttributeIs" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertAttributeIs" + } + ], + "title": "Attribute Is" + }, + "element_text": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertElementText" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertElementText" + } + ], + "title": "Element Text" + }, + "has_json_property_with_value": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasJsonPropertyWithValue" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasJsonPropertyWithValue" + } + ], + "title": "Has Json Property With Value" + }, + "has_json_property_with_text": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasJsonPropertyWithText" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasJsonPropertyWithText" + } + ], + "title": "Has Json Property With Text" + }, + "has_h5_keys": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasH5Keys" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasH5Keys" + } + ], + "title": "Has H5 Keys" + }, + "has_h5_attribute": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasH5Attribute" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasH5Attribute" + } + ], + "title": "Has H5 Attribute" + } + }, + "title": "TestAssertions", + "type": "object" + }, + "TestDiscoveredDataset": { + "additionalProperties": false, + "properties": { + "elements": { + "items": { + "$ref": "#/$defs/TestOutput" + }, + "title": "Elements", + "type": "array" + }, + "discovered_dataset": { + "items": { + "$ref": "#/$defs/TestDiscoveredDataset" + }, + "title": "Discovered Dataset", + "type": "array" + }, + "asserts": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/TestAssertions" + }, + "type": "array" + }, + { + "$ref": "#/$defs/TestAssertions" + } + ], + "description": "$assertions\n### Examples\nThe following demonstrates a wide variety of text-based and tabular\nassertion statements.\n```xml\n<output name=\"out_file1\">\n<assert_contents>\n<has_text text=\"chr7\" />\n<not_has_text text=\"chr8\" />\n<has_text_matching expression=\"1274\\d+53\" />\n<has_line_matching expression=\".*\\s+127489808\\s+127494553\" />\n<!-- &#009; is XML escape code for tab -->\n<has_line line=\"chr7&#009;127471195&#009;127489808\" />\n<has_n_columns n=\"3\" />\n</assert_contents>\n</output>\n```\nThe following demonstrates a wide variety of XML assertion statements.\n```xml\n<output name=\"out_file1\">\n<assert_contents>\n<is_valid_xml />\n<has_element_with_path path=\"BlastOutput_param/Parameters/Parameters_matrix\" />\n<has_n_elements_with_path n=\"9\" path=\"BlastOutput_iterations/Iteration/Iteration_hits/Hit/Hit_num\" />\n<element_text_matches path=\"BlastOutput_version\" expression=\"BLASTP\\s+2\\.2.*\" />\n<element_text_is path=\"BlastOutput_program\" text=\"blastp\" />\n<element_text path=\"BlastOutput_iterations/Iteration/Iteration_hits/Hit/Hit_def\">\n<not_has_text text=\"EDK72998.1\" />\n<has_text_matching expression=\"ABK[\\d\\.]+\" />\n</element_text>\n</assert_contents>\n</output>\n```\nThe following demonstrates verifying XML content with XPath-like expressions.\n```xml\n<output name=\"out_file1\">\n<assert_contents>\n<attribute_is path=\"outerElement/innerElement1\" attribute=\"foo\" text=\"bar\" />\n<attribute_matches path=\"outerElement/innerElement2\" attribute=\"foo2\" expression=\"bar\\d+\" />\n</assert_contents>\n</output>\n```", + "title": "Asserts" + }, + "extra_files": { + "items": { + "$ref": "#/$defs/TestExtraFile" + }, + "title": "Extra Files", + "type": "array" + }, + "metadata": { + "items": { + "$ref": "#/$defs/TestOutputMetadata" + }, + "title": "Metadata", + "type": "array" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "This value is the same as the value of the ``name`` attribute of the ``<data>``\ntag set contained within the tool's ``<outputs>`` tag set.", + "title": "Name" + }, + "file": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, this value is the name of the output file stored in the target\n``test-data`` directory which will be used to compare the results of executing\nthe tool via the functional test framework.", + "title": "File" + }, + "value_json": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, this value will be loaded as JSON and compared against the output\ngenerated as JSON. This can be useful for testing tool outputs that are not files.", + "title": "Value Json" + }, + "ftype": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, this value will be checked against the corresponding output's\ndata type. If these do not match, the test will fail.", + "title": "Ftype" + }, + "sort": { + "anyOf": [ + { + "$ref": "#/$defs/PermissiveBoolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Applies only if ``compare`` is ``diff``, ``re_match`` or ``re_match_multiline``. This flag causes the lines of the history data set to be sorted before the comparison. In case of ``diff`` and ``re_match`` also the local file is sorted. This could be\nuseful for non-deterministic output." + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "An alias for ``file``.", + "title": "Value" + }, + "md5": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, the target output's MD5 hash should match the value specified\nhere. For large static files it may be inconvenient to upload the entiry file\nand this can be used instead.", + "title": "Md5" + }, + "checksum": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, the target output's checksum should match the value specified\nhere. This value should have the form ``hash_type$hash_value``\n(e.g. ``sha1$8156d7ca0f46ed7abac98f82e36cfaddb2aca041``). For large static files\nit may be inconvenient to upload the entiry file and this can be used instead.", + "title": "Checksum" + }, + "compare": { + "anyOf": [ + { + "$ref": "#/$defs/TestOutputCompareType" + }, + { + "type": "null" + } + ], + "default": null + }, + "lines_diff": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Applies only if ``compare`` is set to ``diff``, ``re_match``, and ``contains``. If ``compare`` is set to ``diff``, the number of lines of difference to allow (each line with a modification is a line added and a line removed so this counts as two lines).", + "title": "Lines Diff" + }, + "decompress": { + "anyOf": [ + { + "$ref": "#/$defs/PermissiveBoolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If this attribute is true then try to decompress files if needed. This applies to\ntest assertions expressed with ``assert_contents`` or ``compare`` set to anything\nbut ``sim_size``.\nThis flag is useful for testing compressed outputs that are non-deterministic\ndespite having deterministic decompressed contents. By default, only files compressed\nwith bz2, gzip and zip will be automatically decompressed.\nNote, for specifying assertions for compressed as well as decompressed output\nthe corresponding output tag can be specified multiple times.\nThis is available in Galaxy since release 17.05 and was introduced in [pull request #3550](https://github.com/galaxyproject/galaxy/pull/3550)." + }, + "delta": { + "default": 10000, + "description": "If ``compare`` is set to ``sim_size``, this is the maximum allowed absolute size difference (in bytes) between the data set that is generated in the test and the file in ``test-data/`` that is referenced by the ``file`` attribute. Default value is 10000 bytes. Can be combined with ``delta_frac``.", + "title": "Delta", + "type": "integer" + }, + "delta_frac": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If ``compare`` is set to ``sim_size``, this is the maximum allowed relative size difference between the data set that is generated in the test and the file in ``test-data/`` that is referenced by the ``file`` attribute. A value of 0.1 means that the file that is generated in the test can differ by at most 10% of the file in ``test-data``. The default is not to check for relative size difference. Can be combined with ``delta``.", + "title": "Delta Frac" + }, + "count": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Number or datasets for this output. Should be used for outputs with ``discover_datasets``", + "title": "Count" + }, + "location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "URL that points to a remote output file that will downloaded and used for output comparison.\nPlease use this option only when is not possible to include the files in the `test-data` folder, since\nthis is more error prone due to external factors like remote availability.\nYou can use it in two ways:\n- In combination with `file` it will look for the output file in the `test-data` folder, if it's not available on disk it will\ndownload the file pointed by `location` using the same name as in `file` (or `value`).\n- Specifiying the `location` without a `file` (or `value`), it will download the file and use it as an alias of `file`. The name of the file\nwill be infered from the last component of the location URL. For example, `location=\"https://my_url/my_file.txt\"` will be equivalent to `file=\"my_file.txt\"`.\nIf you specify a `checksum`, it will be also used to check the integrity of the download.", + "title": "Location" + }, + "designation": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The designation of the discovered dataset.", + "title": "Designation" + } + }, + "title": "TestDiscoveredDataset", + "type": "object" + }, + "TestExtraFile": { + "additionalProperties": false, + "properties": { + "elements": { + "items": { + "$ref": "#/$defs/TestOutput" + }, + "title": "Elements", + "type": "array" + }, + "discovered_dataset": { + "items": { + "$ref": "#/$defs/TestDiscoveredDataset" + }, + "title": "Discovered Dataset", + "type": "array" + }, + "asserts": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/TestAssertions" + }, + "type": "array" + }, + { + "$ref": "#/$defs/TestAssertions" + } + ], + "description": "$assertions\n### Examples\nThe following demonstrates a wide variety of text-based and tabular\nassertion statements.\n```xml\n<output name=\"out_file1\">\n<assert_contents>\n<has_text text=\"chr7\" />\n<not_has_text text=\"chr8\" />\n<has_text_matching expression=\"1274\\d+53\" />\n<has_line_matching expression=\".*\\s+127489808\\s+127494553\" />\n<!-- &#009; is XML escape code for tab -->\n<has_line line=\"chr7&#009;127471195&#009;127489808\" />\n<has_n_columns n=\"3\" />\n</assert_contents>\n</output>\n```\nThe following demonstrates a wide variety of XML assertion statements.\n```xml\n<output name=\"out_file1\">\n<assert_contents>\n<is_valid_xml />\n<has_element_with_path path=\"BlastOutput_param/Parameters/Parameters_matrix\" />\n<has_n_elements_with_path n=\"9\" path=\"BlastOutput_iterations/Iteration/Iteration_hits/Hit/Hit_num\" />\n<element_text_matches path=\"BlastOutput_version\" expression=\"BLASTP\\s+2\\.2.*\" />\n<element_text_is path=\"BlastOutput_program\" text=\"blastp\" />\n<element_text path=\"BlastOutput_iterations/Iteration/Iteration_hits/Hit/Hit_def\">\n<not_has_text text=\"EDK72998.1\" />\n<has_text_matching expression=\"ABK[\\d\\.]+\" />\n</element_text>\n</assert_contents>\n</output>\n```\nThe following demonstrates verifying XML content with XPath-like expressions.\n```xml\n<output name=\"out_file1\">\n<assert_contents>\n<attribute_is path=\"outerElement/innerElement1\" attribute=\"foo\" text=\"bar\" />\n<attribute_matches path=\"outerElement/innerElement2\" attribute=\"foo2\" expression=\"bar\\d+\" />\n</assert_contents>\n</output>\n```", + "title": "Asserts" + }, + "extra_files": { + "items": { + "$ref": "#/$defs/TestExtraFile" + }, + "title": "Extra Files", + "type": "array" + }, + "metadata": { + "items": { + "$ref": "#/$defs/TestOutputMetadata" + }, + "title": "Metadata", + "type": "array" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "This value is the same as the value of the ``name`` attribute of the ``<data>``\ntag set contained within the tool's ``<outputs>`` tag set.", + "title": "Name" + }, + "file": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, this value is the name of the output file stored in the target\n``test-data`` directory which will be used to compare the results of executing\nthe tool via the functional test framework.", + "title": "File" + }, + "value_json": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, this value will be loaded as JSON and compared against the output\ngenerated as JSON. This can be useful for testing tool outputs that are not files.", + "title": "Value Json" + }, + "ftype": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, this value will be checked against the corresponding output's\ndata type. If these do not match, the test will fail.", + "title": "Ftype" + }, + "sort": { + "anyOf": [ + { + "$ref": "#/$defs/PermissiveBoolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Applies only if ``compare`` is ``diff``, ``re_match`` or ``re_match_multiline``. This flag causes the lines of the history data set to be sorted before the comparison. In case of ``diff`` and ``re_match`` also the local file is sorted. This could be\nuseful for non-deterministic output." + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "An alias for ``file``.", + "title": "Value" + }, + "md5": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, the target output's MD5 hash should match the value specified\nhere. For large static files it may be inconvenient to upload the entiry file\nand this can be used instead.", + "title": "Md5" + }, + "checksum": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, the target output's checksum should match the value specified\nhere. This value should have the form ``hash_type$hash_value``\n(e.g. ``sha1$8156d7ca0f46ed7abac98f82e36cfaddb2aca041``). For large static files\nit may be inconvenient to upload the entiry file and this can be used instead.", + "title": "Checksum" + }, + "compare": { + "anyOf": [ + { + "$ref": "#/$defs/TestOutputCompareType" + }, + { + "type": "null" + } + ], + "default": null + }, + "lines_diff": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Applies only if ``compare`` is set to ``diff``, ``re_match``, and ``contains``. If ``compare`` is set to ``diff``, the number of lines of difference to allow (each line with a modification is a line added and a line removed so this counts as two lines).", + "title": "Lines Diff" + }, + "decompress": { + "anyOf": [ + { + "$ref": "#/$defs/PermissiveBoolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If this attribute is true then try to decompress files if needed. This applies to\ntest assertions expressed with ``assert_contents`` or ``compare`` set to anything\nbut ``sim_size``.\nThis flag is useful for testing compressed outputs that are non-deterministic\ndespite having deterministic decompressed contents. By default, only files compressed\nwith bz2, gzip and zip will be automatically decompressed.\nNote, for specifying assertions for compressed as well as decompressed output\nthe corresponding output tag can be specified multiple times.\nThis is available in Galaxy since release 17.05 and was introduced in [pull request #3550](https://github.com/galaxyproject/galaxy/pull/3550)." + }, + "delta": { + "default": 10000, + "description": "If ``compare`` is set to ``sim_size``, this is the maximum allowed absolute size difference (in bytes) between the data set that is generated in the test and the file in ``test-data/`` that is referenced by the ``file`` attribute. Default value is 10000 bytes. Can be combined with ``delta_frac``.", + "title": "Delta", + "type": "integer" + }, + "delta_frac": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If ``compare`` is set to ``sim_size``, this is the maximum allowed relative size difference between the data set that is generated in the test and the file in ``test-data/`` that is referenced by the ``file`` attribute. A value of 0.1 means that the file that is generated in the test can differ by at most 10% of the file in ``test-data``. The default is not to check for relative size difference. Can be combined with ``delta``.", + "title": "Delta Frac" + }, + "count": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Number or datasets for this output. Should be used for outputs with ``discover_datasets``", + "title": "Count" + }, + "location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "URL that points to a remote output file that will downloaded and used for output comparison.\nPlease use this option only when is not possible to include the files in the `test-data` folder, since\nthis is more error prone due to external factors like remote availability.\nYou can use it in two ways:\n- In combination with `file` it will look for the output file in the `test-data` folder, if it's not available on disk it will\ndownload the file pointed by `location` using the same name as in `file` (or `value`).\n- Specifiying the `location` without a `file` (or `value`), it will download the file and use it as an alias of `file`. The name of the file\nwill be infered from the last component of the location URL. For example, `location=\"https://my_url/my_file.txt\"` will be equivalent to `file=\"my_file.txt\"`.\nIf you specify a `checksum`, it will be also used to check the integrity of the download.", + "title": "Location" + }, + "type_value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Extra file type (either ``file`` or ``directory``).", + "title": "Type Value" + } + }, + "title": "TestExtraFile", + "type": "object" + }, + "TestOutput": { + "additionalProperties": false, + "properties": { + "elements": { + "items": { + "$ref": "#/$defs/TestOutput" + }, + "title": "Elements", + "type": "array" + }, + "discovered_dataset": { + "items": { + "$ref": "#/$defs/TestDiscoveredDataset" + }, + "title": "Discovered Dataset", + "type": "array" + }, + "asserts": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/TestAssertions" + }, + "type": "array" + }, + { + "$ref": "#/$defs/TestAssertions" + } + ], + "description": "$assertions\n### Examples\nThe following demonstrates a wide variety of text-based and tabular\nassertion statements.\n```xml\n<output name=\"out_file1\">\n<assert_contents>\n<has_text text=\"chr7\" />\n<not_has_text text=\"chr8\" />\n<has_text_matching expression=\"1274\\d+53\" />\n<has_line_matching expression=\".*\\s+127489808\\s+127494553\" />\n<!-- &#009; is XML escape code for tab -->\n<has_line line=\"chr7&#009;127471195&#009;127489808\" />\n<has_n_columns n=\"3\" />\n</assert_contents>\n</output>\n```\nThe following demonstrates a wide variety of XML assertion statements.\n```xml\n<output name=\"out_file1\">\n<assert_contents>\n<is_valid_xml />\n<has_element_with_path path=\"BlastOutput_param/Parameters/Parameters_matrix\" />\n<has_n_elements_with_path n=\"9\" path=\"BlastOutput_iterations/Iteration/Iteration_hits/Hit/Hit_num\" />\n<element_text_matches path=\"BlastOutput_version\" expression=\"BLASTP\\s+2\\.2.*\" />\n<element_text_is path=\"BlastOutput_program\" text=\"blastp\" />\n<element_text path=\"BlastOutput_iterations/Iteration/Iteration_hits/Hit/Hit_def\">\n<not_has_text text=\"EDK72998.1\" />\n<has_text_matching expression=\"ABK[\\d\\.]+\" />\n</element_text>\n</assert_contents>\n</output>\n```\nThe following demonstrates verifying XML content with XPath-like expressions.\n```xml\n<output name=\"out_file1\">\n<assert_contents>\n<attribute_is path=\"outerElement/innerElement1\" attribute=\"foo\" text=\"bar\" />\n<attribute_matches path=\"outerElement/innerElement2\" attribute=\"foo2\" expression=\"bar\\d+\" />\n</assert_contents>\n</output>\n```", + "title": "Asserts" + }, + "extra_files": { + "items": { + "$ref": "#/$defs/TestExtraFile" + }, + "title": "Extra Files", + "type": "array" + }, + "metadata": { + "items": { + "$ref": "#/$defs/TestOutputMetadata" + }, + "title": "Metadata", + "type": "array" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "This value is the same as the value of the ``name`` attribute of the ``<data>``\ntag set contained within the tool's ``<outputs>`` tag set.", + "title": "Name" + }, + "file": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, this value is the name of the output file stored in the target\n``test-data`` directory which will be used to compare the results of executing\nthe tool via the functional test framework.", + "title": "File" + }, + "value_json": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, this value will be loaded as JSON and compared against the output\ngenerated as JSON. This can be useful for testing tool outputs that are not files.", + "title": "Value Json" + }, + "ftype": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, this value will be checked against the corresponding output's\ndata type. If these do not match, the test will fail.", + "title": "Ftype" + }, + "sort": { + "anyOf": [ + { + "$ref": "#/$defs/PermissiveBoolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Applies only if ``compare`` is ``diff``, ``re_match`` or ``re_match_multiline``. This flag causes the lines of the history data set to be sorted before the comparison. In case of ``diff`` and ``re_match`` also the local file is sorted. This could be\nuseful for non-deterministic output." + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "An alias for ``file``.", + "title": "Value" + }, + "md5": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, the target output's MD5 hash should match the value specified\nhere. For large static files it may be inconvenient to upload the entiry file\nand this can be used instead.", + "title": "Md5" + }, + "checksum": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, the target output's checksum should match the value specified\nhere. This value should have the form ``hash_type$hash_value``\n(e.g. ``sha1$8156d7ca0f46ed7abac98f82e36cfaddb2aca041``). For large static files\nit may be inconvenient to upload the entiry file and this can be used instead.", + "title": "Checksum" + }, + "compare": { + "anyOf": [ + { + "$ref": "#/$defs/TestOutputCompareType" + }, + { + "type": "null" + } + ], + "default": null + }, + "lines_diff": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Applies only if ``compare`` is set to ``diff``, ``re_match``, and ``contains``. If ``compare`` is set to ``diff``, the number of lines of difference to allow (each line with a modification is a line added and a line removed so this counts as two lines).", + "title": "Lines Diff" + }, + "decompress": { + "anyOf": [ + { + "$ref": "#/$defs/PermissiveBoolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If this attribute is true then try to decompress files if needed. This applies to\ntest assertions expressed with ``assert_contents`` or ``compare`` set to anything\nbut ``sim_size``.\nThis flag is useful for testing compressed outputs that are non-deterministic\ndespite having deterministic decompressed contents. By default, only files compressed\nwith bz2, gzip and zip will be automatically decompressed.\nNote, for specifying assertions for compressed as well as decompressed output\nthe corresponding output tag can be specified multiple times.\nThis is available in Galaxy since release 17.05 and was introduced in [pull request #3550](https://github.com/galaxyproject/galaxy/pull/3550)." + }, + "delta": { + "default": 10000, + "description": "If ``compare`` is set to ``sim_size``, this is the maximum allowed absolute size difference (in bytes) between the data set that is generated in the test and the file in ``test-data/`` that is referenced by the ``file`` attribute. Default value is 10000 bytes. Can be combined with ``delta_frac``.", + "title": "Delta", + "type": "integer" + }, + "delta_frac": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If ``compare`` is set to ``sim_size``, this is the maximum allowed relative size difference between the data set that is generated in the test and the file in ``test-data/`` that is referenced by the ``file`` attribute. A value of 0.1 means that the file that is generated in the test can differ by at most 10% of the file in ``test-data``. The default is not to check for relative size difference. Can be combined with ``delta``.", + "title": "Delta Frac" + }, + "count": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Number or datasets for this output. Should be used for outputs with ``discover_datasets``", + "title": "Count" + }, + "location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "URL that points to a remote output file that will downloaded and used for output comparison.\nPlease use this option only when is not possible to include the files in the `test-data` folder, since\nthis is more error prone due to external factors like remote availability.\nYou can use it in two ways:\n- In combination with `file` it will look for the output file in the `test-data` folder, if it's not available on disk it will\ndownload the file pointed by `location` using the same name as in `file` (or `value`).\n- Specifiying the `location` without a `file` (or `value`), it will download the file and use it as an alias of `file`. The name of the file\nwill be infered from the last component of the location URL. For example, `location=\"https://my_url/my_file.txt\"` will be equivalent to `file=\"my_file.txt\"`.\nIf you specify a `checksum`, it will be also used to check the integrity of the download.", + "title": "Location" + } + }, + "title": "TestOutput", + "type": "object" + }, + "TestOutputCollection": { + "additionalProperties": false, + "properties": { + "elements": { + "items": { + "$ref": "#/$defs/TestOutput" + }, + "title": "Elements", + "type": "array" + }, + "name": { + "description": "This value is the same as the value of the ``name`` attribute of the\n``<collection>`` tag set contained within the tool's ``<outputs>`` tag set.", + "title": "Name", + "type": "string" + }, + "type_value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Expected collection type (``list`` or ``paired``), nested collections are specified as colon separated list (the most common types are ``list``, ``paired``, ``list:paired``, or ``list:list``).", + "title": "Type Value" + }, + "count": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Number of elements in output collection.", + "title": "Count" + } + }, + "required": ["name"], + "title": "TestOutputCollection", + "type": "object" + }, + "TestOutputCompareType": { + "description": "Type of comparison to use when comparing test generated output files to\nexpected output files.\n\nCurrently valid value are\n``diff`` (the default), ``re_match``, ``re_match_multiline``,\nand ``contains``. In addition there is ``sim_size`` which is discouraged in favour of a ``has_size`` assertion.", + "enum": ["diff", "re_match", "sim_size", "re_match_multiline", "contains"], + "title": "TestOutputCompareType", + "type": "string" + }, + "TestOutputMetadata": { + "additionalProperties": false, + "properties": { + "name": { + "description": "Name of the metadata element to check.", + "title": "Name", + "type": "string" + }, + "value": { + "description": "Expected value (as a string) of metadata value.", + "title": "Value", + "type": "string" + } + }, + "required": ["name", "value"], + "title": "TestOutputMetadata", + "type": "object" + } + }, + "items": { + "$ref": "#/$defs/Test" + }, + "title": "ListOfTests", + "type": "array" +} From 9b175594164826db13293b7445c6c490f909c676 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sat, 9 Dec 2023 00:08:23 +0100 Subject: [PATCH 18/98] Add schema provider --- .../workflow-tests-language-service/src/schema.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 server/packages/workflow-tests-language-service/src/schema.ts diff --git a/server/packages/workflow-tests-language-service/src/schema.ts b/server/packages/workflow-tests-language-service/src/schema.ts new file mode 100644 index 0000000..2bd4ce1 --- /dev/null +++ b/server/packages/workflow-tests-language-service/src/schema.ts @@ -0,0 +1,14 @@ +import { injectable } from "inversify"; +import WorkflowTestsSchema from "../../../../workflow-languages/schemas/tests.schema.json"; +import { JSONSchema } from "vscode-json-languageservice"; + +export interface WorkflowTestsSchemaProvider { + getSchema(): Promise; +} + +@injectable() +export class WorkflowTestsSchemaProviderImpl implements WorkflowTestsSchemaProvider { + public async getSchema(): Promise { + return WorkflowTestsSchema; + } +} From a1af3215d4c0cde22ef9e26b9885010bed95ad84 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sat, 9 Dec 2023 00:09:37 +0100 Subject: [PATCH 19/98] Add hover service interfaces --- .../src/services/hover.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 server/packages/workflow-tests-language-service/src/services/hover.ts diff --git a/server/packages/workflow-tests-language-service/src/services/hover.ts b/server/packages/workflow-tests-language-service/src/services/hover.ts new file mode 100644 index 0000000..7729b98 --- /dev/null +++ b/server/packages/workflow-tests-language-service/src/services/hover.ts @@ -0,0 +1,14 @@ +import { ASTNodeManager } from "@gxwf/server-common/src/ast/nodeManager"; +import { Hover, Position, TextDocument } from "@gxwf/server-common/src/languageTypes"; +import { injectable } from "inversify"; + +export interface WorkflowTestsHoverService { + doHover(document: TextDocument, position: Position, nodeManager: ASTNodeManager): Promise; +} + +@injectable() +export class WorkflowTestsHoverServiceImpl implements WorkflowTestsHoverService { + public async doHover(document: TextDocument, position: Position, nodeManager: ASTNodeManager): Promise { + return null; + } +} From 0544611b2cf3f22d88295192504e8bfe94844bd6 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sat, 9 Dec 2023 00:10:41 +0100 Subject: [PATCH 20/98] Setup services in IoC container --- .../src/inversify.config.ts | 11 +++++++++-- .../workflow-tests-language-service/src/types.ts | 4 ++++ 2 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 server/packages/workflow-tests-language-service/src/types.ts diff --git a/server/packages/workflow-tests-language-service/src/inversify.config.ts b/server/packages/workflow-tests-language-service/src/inversify.config.ts index 42a38eb..0e517f4 100644 --- a/server/packages/workflow-tests-language-service/src/inversify.config.ts +++ b/server/packages/workflow-tests-language-service/src/inversify.config.ts @@ -1,9 +1,16 @@ import { ContainerModule } from "inversify"; import { GxWorkflowTestsLanguageServiceImpl } from "./languageService"; -import { TYPES, WorkflowTestsLanguageService } from "@gxwf/server-common/src/languageTypes"; +import { TYPES as COMMON_TYPES, WorkflowTestsLanguageService } from "@gxwf/server-common/src/languageTypes"; +import { WorkflowTestsHoverService, WorkflowTestsHoverServiceImpl } from "./services/hover"; +import { TYPES } from "./types"; +import { WorkflowTestsSchemaProvider, WorkflowTestsSchemaProviderImpl } from "./schema"; export const WorkflowTestsLanguageServiceContainerModule = new ContainerModule((bind) => { - bind(TYPES.WorkflowTestsLanguageService) + bind(TYPES.WorkflowTestsSchemaProvider) + .to(WorkflowTestsSchemaProviderImpl) + .inSingletonScope(); + bind(TYPES.WorkflowTestsHoverService).to(WorkflowTestsHoverServiceImpl).inSingletonScope(); + bind(COMMON_TYPES.WorkflowTestsLanguageService) .to(GxWorkflowTestsLanguageServiceImpl) .inSingletonScope(); }); diff --git a/server/packages/workflow-tests-language-service/src/types.ts b/server/packages/workflow-tests-language-service/src/types.ts new file mode 100644 index 0000000..58abc1d --- /dev/null +++ b/server/packages/workflow-tests-language-service/src/types.ts @@ -0,0 +1,4 @@ +export const TYPES = { + WorkflowTestsSchemaProvider: Symbol.for("WorkflowTestsSchemaProvider"), + WorkflowTestsHoverService: Symbol.for("WorkflowTestsHoverService"), +}; From d3e27f886e0b310ffa009651efc0c3ddcc7e3449 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sat, 9 Dec 2023 00:11:31 +0100 Subject: [PATCH 21/98] Fix tsconfig for schema import --- .../packages/workflow-tests-language-service/tsconfig.json | 6 ++++-- shared.webpack.config.js | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/server/packages/workflow-tests-language-service/tsconfig.json b/server/packages/workflow-tests-language-service/tsconfig.json index cabf4b8..fe8c62d 100644 --- a/server/packages/workflow-tests-language-service/tsconfig.json +++ b/server/packages/workflow-tests-language-service/tsconfig.json @@ -11,8 +11,10 @@ "esModuleInterop": true, "sourceMap": true, "strict": true, - "composite": true + "composite": true, + "rootDirs": ["src", "tests"], + "rootDir": "../../../" }, - "include": ["src"], + "include": ["src/**/*", "../../../workflow-languages/schemas/*.json"], "exclude": ["node_modules", ".vscode-test-web"] } diff --git a/shared.webpack.config.js b/shared.webpack.config.js index 8bff832..e5d5c07 100644 --- a/shared.webpack.config.js +++ b/shared.webpack.config.js @@ -58,7 +58,7 @@ module.exports = function withDefaults(/**@type WebpackConfig*/ extConfig) { }, output: { filename: "[name].js", - path: path.join(extConfig.context, "dist"), + path: path.join(extConfig.context ?? "", "dist"), libraryTarget: "commonjs", }, devtool: "source-map", From 0c704dae5c441d96010d031eab100aa696b18812 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sat, 9 Dec 2023 00:13:27 +0100 Subject: [PATCH 22/98] Inject hover service --- .../src/languageService.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/server/packages/workflow-tests-language-service/src/languageService.ts b/server/packages/workflow-tests-language-service/src/languageService.ts index c61dd9b..84d54d8 100644 --- a/server/packages/workflow-tests-language-service/src/languageService.ts +++ b/server/packages/workflow-tests-language-service/src/languageService.ts @@ -14,28 +14,30 @@ import { YAMLLanguageService } from "@gxwf/yaml-language-service/src/yamlLanguag import { GxWorkflowTestsDocument } from "./document"; import { inject, injectable } from "inversify"; import { TYPES as YAML_TYPES } from "@gxwf/yaml-language-service/src/inversify.config"; +import { WorkflowTestsHoverService } from "./services/hover"; +import { TYPES } from "./types"; @injectable() export class GxWorkflowTestsLanguageServiceImpl extends LanguageServiceBase { - private _yamlLanguageService: YAMLLanguageService; - - constructor(@inject(YAML_TYPES.YAMLLanguageService) yamlLanguageService: YAMLLanguageService) { + constructor( + @inject(YAML_TYPES.YAMLLanguageService) protected yamlLanguageService: YAMLLanguageService, + @inject(TYPES.WorkflowTestsHoverService) protected hoverService: WorkflowTestsHoverService + ) { super("gxwftests"); - this._yamlLanguageService = yamlLanguageService; } public override parseDocument(document: TextDocument): GxWorkflowTestsDocument { - const yamlDocument = this._yamlLanguageService.parseYAMLDocument(document); + const yamlDocument = this.yamlLanguageService.parseYAMLDocument(document); return new GxWorkflowTestsDocument(document, yamlDocument); } public override format(document: TextDocument, _: Range, options: FormattingOptions): TextEdit[] { - return this._yamlLanguageService.doFormat(document, options); + return this.yamlLanguageService.doFormat(document, options); } public override doHover(documentContext: WorkflowTestsDocument, position: Position): Promise { - // TODO: Implement hover - return Promise.resolve(null); + const hover = this.hoverService.doHover(documentContext.textDocument, position, documentContext.nodeManager); + return hover; } public override doComplete( From 76afee50fc1f474c962bce0b0d44407dd1813239 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sun, 10 Dec 2023 15:48:30 +0100 Subject: [PATCH 23/98] Move schema provider under schema --- .../workflow-tests-language-service/src/inversify.config.ts | 2 +- .../src/{schema.ts => schema/provider.ts} | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) rename server/packages/workflow-tests-language-service/src/{schema.ts => schema/provider.ts} (61%) diff --git a/server/packages/workflow-tests-language-service/src/inversify.config.ts b/server/packages/workflow-tests-language-service/src/inversify.config.ts index 0e517f4..e58578a 100644 --- a/server/packages/workflow-tests-language-service/src/inversify.config.ts +++ b/server/packages/workflow-tests-language-service/src/inversify.config.ts @@ -3,7 +3,7 @@ import { GxWorkflowTestsLanguageServiceImpl } from "./languageService"; import { TYPES as COMMON_TYPES, WorkflowTestsLanguageService } from "@gxwf/server-common/src/languageTypes"; import { WorkflowTestsHoverService, WorkflowTestsHoverServiceImpl } from "./services/hover"; import { TYPES } from "./types"; -import { WorkflowTestsSchemaProvider, WorkflowTestsSchemaProviderImpl } from "./schema"; +import { WorkflowTestsSchemaProvider, WorkflowTestsSchemaProviderImpl } from "./schema/provider"; export const WorkflowTestsLanguageServiceContainerModule = new ContainerModule((bind) => { bind(TYPES.WorkflowTestsSchemaProvider) diff --git a/server/packages/workflow-tests-language-service/src/schema.ts b/server/packages/workflow-tests-language-service/src/schema/provider.ts similarity index 61% rename from server/packages/workflow-tests-language-service/src/schema.ts rename to server/packages/workflow-tests-language-service/src/schema/provider.ts index 2bd4ce1..74e0c85 100644 --- a/server/packages/workflow-tests-language-service/src/schema.ts +++ b/server/packages/workflow-tests-language-service/src/schema/provider.ts @@ -1,14 +1,14 @@ import { injectable } from "inversify"; -import WorkflowTestsSchema from "../../../../workflow-languages/schemas/tests.schema.json"; +import WorkflowTestsSchema from "../../../../../workflow-languages/schemas/tests.schema.json"; import { JSONSchema } from "vscode-json-languageservice"; export interface WorkflowTestsSchemaProvider { - getSchema(): Promise; + getSchema(): JSONSchema; } @injectable() export class WorkflowTestsSchemaProviderImpl implements WorkflowTestsSchemaProvider { - public async getSchema(): Promise { + public getSchema(): JSONSchema { return WorkflowTestsSchema; } } From 14b816fd26c20072d61de153fba90a996f1716be Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sat, 23 Dec 2023 20:21:00 +0100 Subject: [PATCH 24/98] Update schema --- workflow-languages/schemas/tests.schema.json | 2313 +++++++++++++++--- 1 file changed, 2005 insertions(+), 308 deletions(-) diff --git a/workflow-languages/schemas/tests.schema.json b/workflow-languages/schemas/tests.schema.json index 8b1d936..26fa993 100644 --- a/workflow-languages/schemas/tests.schema.json +++ b/workflow-languages/schemas/tests.schema.json @@ -2,6 +2,7 @@ "$defs": { "AssertAttributeIs": { "additionalProperties": false, + "description": "Asserts the XML ``attribute`` for the element (or tag) with the specified\nXPath-like ``path`` is the specified ``text``, e.g. ```xml <attribute_is\npath=\"outerElement/innerElement1\" attribute=\"foo\" text=\"bar\" /> ``` The\nassertion implicitly also asserts that an element matching ``path`` exists.\n\nWith ``negate`` the result of the assertion (on the equality) can be inverted (the\nimplicit assertion on the existence of the path is not affected).\n$attribute_list::5", "properties": { "path": { "description": "Path to check for. Valid paths are the simplified subsets of XPath implemented by lxml.etree; https://lxml.de/xpathxslt.html for more information.", @@ -14,13 +15,17 @@ "type": "string" }, "negate": { - "allOf": [ + "anyOf": [ + { + "type": "boolean" + }, { - "$ref": "#/$defs/PermissiveBoolean" + "$ref": "#/$defs/PermissiveBooleanValue" } ], "default": "false", - "description": "Negate the outcome of the assertion." + "description": "Negate the outcome of the assertion.", + "title": "Negate" } }, "required": ["path", "text"], @@ -29,6 +34,7 @@ }, "AssertAttributeMatches": { "additionalProperties": false, + "description": "Asserts the XML ``attribute`` for the element (or tag) with the specified\nXPath-like ``path`` matches the regular expression specified by ``expression``,\ne.g. ```xml <attribute_matches path=\"outerElement/innerElement2\"\nattribute=\"foo2\" expression=\"bar\\d+\" /> ``` The assertion implicitly also\nasserts that an element matching ``path`` exists.\n\nWith ``negate`` the result of the assertion (on the matching) can be inverted (the\nimplicit assertion on the existence of the path is not affected).\n$attribute_list::5", "properties": { "path": { "description": "Path to check for. Valid paths are the simplified subsets of XPath implemented by lxml.etree; https://lxml.de/xpathxslt.html for more information.", @@ -41,13 +47,17 @@ "type": "string" }, "negate": { - "allOf": [ + "anyOf": [ + { + "type": "boolean" + }, { - "$ref": "#/$defs/PermissiveBoolean" + "$ref": "#/$defs/PermissiveBooleanValue" } ], "default": "false", - "description": "Negate the outcome of the assertion." + "description": "Negate the outcome of the assertion.", + "title": "Negate" } }, "required": ["path", "expression"], @@ -56,6 +66,7 @@ }, "AssertElementText": { "additionalProperties": false, + "description": "This tag allows the developer to recurisively specify additional assertions\nas child elements about just the text contained in the element specified by the\nXPath-like ``path``, e.g. ```xml <element_text\npath=\"BlastOutput_iterations/Iteration/Iteration_hits/Hit/Hit_def\">\n<not_has_text text=\"EDK72998.1\" /> </element_text> ``` The\nassertion implicitly also asserts that an element matching ``path`` exists.\n\nWith ``negate`` the result of the implicit assertions can be inverted.\nThe sub-assertions, which have their own ``negate`` attribute, are not affected\nby ``negate``.\n$attribute_list::5", "properties": { "has_text": { "items": { @@ -105,13 +116,17 @@ "type": "string" }, "negate": { - "allOf": [ + "anyOf": [ + { + "type": "boolean" + }, { - "$ref": "#/$defs/PermissiveBoolean" + "$ref": "#/$defs/PermissiveBooleanValue" } ], "default": "false", - "description": "Negate the outcome of the assertion." + "description": "Negate the outcome of the assertion.", + "title": "Negate" } }, "required": ["path"], @@ -120,6 +135,7 @@ }, "AssertElementTextIs": { "additionalProperties": false, + "description": "Asserts the text of the XML element with the specified XPath-like ``path``\nis the specified ``text``, e.g. ```xml <element_text_is\npath=\"BlastOutput_program\" text=\"blastp\" /> ``` The assertion implicitly\nalso asserts that an element matching ``path`` exists.\n\nWith ``negate`` the result of the assertion (on the equality) can be inverted (the\nimplicit assertion on the existence of the path is not affected).\n$attribute_list::5", "properties": { "path": { "description": "Path to check for. Valid paths are the simplified subsets of XPath implemented by lxml.etree; https://lxml.de/xpathxslt.html for more information.", @@ -132,13 +148,17 @@ "type": "string" }, "negate": { - "allOf": [ + "anyOf": [ + { + "type": "boolean" + }, { - "$ref": "#/$defs/PermissiveBoolean" + "$ref": "#/$defs/PermissiveBooleanValue" } ], "default": "false", - "description": "Negate the outcome of the assertion." + "description": "Negate the outcome of the assertion.", + "title": "Negate" } }, "required": ["path", "text"], @@ -147,6 +167,7 @@ }, "AssertElementTextMatches": { "additionalProperties": false, + "description": "Asserts the text of the XML element with the specified XPath-like ``path``\nmatches the regular expression defined by ``expression``, e.g. ```xml\n<element_text_matches path=\"BlastOutput_version\"\nexpression=\"BLASTP\\s+2\\.2.*\"/> ``` The assertion implicitly also asserts\nthat an element matching ``path`` exists.\n\nWith ``negate`` the result of the assertion (on the matching) can be inverted (the\nimplicit assertion on the existence of the path is not affected).\n$attribute_list::5", "properties": { "path": { "description": "Path to check for. Valid paths are the simplified subsets of XPath implemented by lxml.etree; https://lxml.de/xpathxslt.html for more information.", @@ -159,13 +180,17 @@ "type": "string" }, "negate": { - "allOf": [ + "anyOf": [ + { + "type": "boolean" + }, { - "$ref": "#/$defs/PermissiveBoolean" + "$ref": "#/$defs/PermissiveBooleanValue" } ], "default": "false", - "description": "Negate the outcome of the assertion." + "description": "Negate the outcome of the assertion.", + "title": "Negate" } }, "required": ["path", "expression"], @@ -174,6 +199,7 @@ }, "AssertHasArchiveMember": { "additionalProperties": false, + "description": "This tag allows to check if ``path`` is contained in a compressed file. The\npath is a regular expression that is matched against the full paths of the\nobjects in the compressed file (remember that \"matching\" means it is checked if\na prefix of the full path of an archive member is described by the regular\nexpression). Valid archive formats include ``.zip``, ``.tar``, and ``.tar.gz``.\nNote that.\n\ndepending on the archive creation method:\n- full paths of the members may be prefixed with ``./``\n- directories may be treated as empty files\n```xml\n<has_archive_member path=\"./path/to/my-file.txt\"/>\n```\nWith ``n`` and ``delta`` (or ``min`` and ``max``) assertions on the number of\narchive members matching ``path`` can be expressed. The following could be used,\ne.g., to assert an archive containing n&plusmn;1 elements out of which at least\n4 need to have a ``txt`` extension.\n```xml\n<has_archive_member path=\".*\" n=\"10\" delta=\"1\"/>\n<has_archive_member path=\".*\\.txt\" min=\"4\"/>\n```\nIn addition the tag can contain additional assertions as child elements about\nthe first member in the archive matching the regular expression ``path``. For\ninstance\n```xml\n<has_archive_member path=\".*/my-file.txt\">\n<not_has_text text=\"EDK72998.1\"/>\n</has_archive_member>\n```\nIf the ``all`` attribute is set to ``true`` then all archive members are subject\nto the assertions. Note that, archive members matching the ``path`` are sorted\nalphabetically.\nThe ``negate`` attribute of the ``has_archive_member`` assertion only affects\nthe asserts on the presence and number of matching archive members, but not any\nsub-assertions (which can offer the ``negate`` attribute on their own). The\ncheck if the file is an archive at all, which is also done by the function, is\nnot affected.\n$attribute_list::5", "properties": { "has_size": { "items": { @@ -336,13 +362,17 @@ "title": "Path" }, "all": { - "allOf": [ + "anyOf": [ + { + "type": "boolean" + }, { - "$ref": "#/$defs/PermissiveBoolean" + "$ref": "#/$defs/PermissiveBooleanValue" } ], "default": "false", - "description": "Check the sub-assertions for all paths matching the path. Default: false, i.e. only the first" + "description": "Check the sub-assertions for all paths matching the path. Default: false, i.e. only the first", + "title": "All" }, "n": { "anyOf": [ @@ -414,6 +444,7 @@ }, "AssertHasElementWithPath": { "additionalProperties": false, + "description": "Asserts the XML output contains at least one element (or tag) with the\nspecified XPath-like ``path``, e.g. ```xml <has_element_with_path\npath=\"BlastOutput_param/Parameters/Parameters_matrix\" /> ``` With ``negate``\nthe result of the assertion can be inverted.\n\n$attribute_list::5", "properties": { "path": { "description": "Path to check for. Valid paths are the simplified subsets of XPath implemented by lxml.etree; https://lxml.de/xpathxslt.html for more information.", @@ -421,13 +452,17 @@ "type": "string" }, "negate": { - "allOf": [ + "anyOf": [ + { + "type": "boolean" + }, { - "$ref": "#/$defs/PermissiveBoolean" + "$ref": "#/$defs/PermissiveBooleanValue" } ], "default": "false", - "description": "Negate the outcome of the assertion." + "description": "Negate the outcome of the assertion.", + "title": "Negate" } }, "required": ["path"], @@ -436,6 +471,7 @@ }, "AssertHasH5Attribute": { "additionalProperties": false, + "description": "Asserts HDF5 output contains the specified ``value`` for an attribute (``key``), e.g.\n```xml\n<has_h5_attribute key=\"nchroms\" value=\"15\" />\n```\n$attribute_list::5", "properties": { "key": { "anyOf": [ @@ -469,6 +505,7 @@ }, "AssertHasH5Keys": { "additionalProperties": false, + "description": "Asserts HDF5 output has a set of attributes (``keys``), specified as a\ncomma-separated list, e.g.\n```xml\n<has_h5_keys keys=\"bins,chroms,indexes,pixels,chroms/lengths\" />\n```\n$attribute_list::5", "properties": { "keys": { "anyOf": [ @@ -489,6 +526,7 @@ }, "AssertHasJsonPropertyWithText": { "additionalProperties": false, + "description": "Asserts the JSON document contains a property or key with the specified text\n(i.e. string) value.\n\n```xml\n<has_json_property_with_text property=\"color\" text=\"red\" />\n```\n$attribute_list::5", "properties": { "property": { "description": "JSON property to search the target for.", @@ -507,6 +545,7 @@ }, "AssertHasJsonPropertyWithValue": { "additionalProperties": false, + "description": "Asserts the JSON document contains a property or key with the specified JSON\nvalue.\n\n```xml\n<has_json_property_with_value property=\"skipped_columns\" value=\"[1, 3, 5]\" />\n```\n$attribute_list::5", "properties": { "property": { "description": "JSON property to search the target for.", @@ -525,6 +564,7 @@ }, "AssertHasLine": { "additionalProperties": false, + "description": "Asserts a line matching the specified string (``line``) appears in the\noutput (e.g. ``<has_line line=\"A full example line.\" />``).\n\nIf the ``line`` is expected\nto occur a particular number of times, this value can be specified using ``n``.\nOptionally also with a certain ``delta``. Alternatively the range of expected\noccurences can be specified by ``min`` and/or ``max``.\n$attribute_list::5", "properties": { "line": { "description": "The line to check for", @@ -596,13 +636,17 @@ "title": "Max" }, "negate": { - "allOf": [ + "anyOf": [ + { + "type": "boolean" + }, { - "$ref": "#/$defs/PermissiveBoolean" + "$ref": "#/$defs/PermissiveBooleanValue" } ], "default": "false", - "description": "Negate the outcome of the assertion." + "description": "Negate the outcome of the assertion.", + "title": "Negate" } }, "required": ["line"], @@ -611,6 +655,7 @@ }, "AssertHasLineMatching": { "additionalProperties": false, + "description": "Asserts a line matching the specified regular expression (``expression``)\nappears in the output (e.g. ``<has_line_matching\nexpression=\".*\\s+127489808\\s+127494553\" />``).\n\nIf a particular number of matching lines is expected, this value can be\nspecified using ``n``. Optionally also with ``delta``. Alternatively the range\nof expected occurences can be specified by ``min`` and/or ``max``.\n$attribute_list::5", "properties": { "expression": { "description": "Regular expression to check for", @@ -682,13 +727,17 @@ "title": "Max" }, "negate": { - "allOf": [ + "anyOf": [ + { + "type": "boolean" + }, { - "$ref": "#/$defs/PermissiveBoolean" + "$ref": "#/$defs/PermissiveBooleanValue" } ], "default": "false", - "description": "Negate the outcome of the assertion." + "description": "Negate the outcome of the assertion.", + "title": "Negate" } }, "required": ["expression"], @@ -697,6 +746,7 @@ }, "AssertHasNcolumns": { "additionalProperties": false, + "description": "Asserts tabular output (actually only the first line) contains the specified\nnumber (``n``) of columns (e.g. ``<has_n_columns n=\"3\"/>``) optionally\nalso with ``delta``.\n\nAlternatively the range of expected occurences can be specified by\n``min`` and/or ``max``. Optionally a column separator (``sep``, default is\n``\\t``) `and comment character(s) can be specified (``comment``, default is\nempty string), then the first non-comment line is used for determining the\nnumber of columns.\n$attribute_list::5", "properties": { "n": { "anyOf": [ @@ -763,13 +813,17 @@ "title": "Max" }, "negate": { - "allOf": [ + "anyOf": [ + { + "type": "boolean" + }, { - "$ref": "#/$defs/PermissiveBoolean" + "$ref": "#/$defs/PermissiveBooleanValue" } ], "default": "false", - "description": "Negate the outcome of the assertion." + "description": "Negate the outcome of the assertion.", + "title": "Negate" }, "sep": { "default": " ", @@ -796,6 +850,7 @@ }, "AssertHasNelementsWithPath": { "additionalProperties": false, + "description": "Asserts the XML output contains the specified number (``n``, optionally with\n``delta``) of elements (or tags) with the specified XPath-like ``path``, e.g.\n```xml <has_n_elements_with_path n=\"9\"\npath=\"BlastOutput_iterations/Iteration/Iteration_hits/Hit/Hit_num\" /> ```\nAlternatively to ``n`` and ``delta`` also the ``min`` and ``max`` attributes\ncan be used to specify the range of the expected number of occurences.\n\nWith ``negate`` the result of the assertion can be inverted.\n$attribute_list::5", "properties": { "path": { "description": "Path to check for. Valid paths are the simplified subsets of XPath implemented by lxml.etree; https://lxml.de/xpathxslt.html for more information.", @@ -867,13 +922,17 @@ "title": "Max" }, "negate": { - "allOf": [ + "anyOf": [ + { + "type": "boolean" + }, { - "$ref": "#/$defs/PermissiveBoolean" + "$ref": "#/$defs/PermissiveBooleanValue" } ], "default": "false", - "description": "Negate the outcome of the assertion." + "description": "Negate the outcome of the assertion.", + "title": "Negate" } }, "required": ["path"], @@ -882,6 +941,7 @@ }, "AssertHasNlines": { "additionalProperties": false, + "description": "Asserts that an output contains ``n`` lines, allowing for a difference of\n``delta`` (default is 0), e.g. ``<has_n_lines n=\"3\" delta=\"1\"/>``.\n\nAlternatively the range of expected occurences can be specified by ``min``\nand/or ``max``.\n$attribute_list::5", "properties": { "n": { "anyOf": [ @@ -948,13 +1008,17 @@ "title": "Max" }, "negate": { - "allOf": [ + "anyOf": [ + { + "type": "boolean" + }, { - "$ref": "#/$defs/PermissiveBoolean" + "$ref": "#/$defs/PermissiveBooleanValue" } ], "default": "false", - "description": "Negate the outcome of the assertion." + "description": "Negate the outcome of the assertion.", + "title": "Negate" } }, "title": "AssertHasNlines", @@ -962,6 +1026,7 @@ }, "AssertHasSize": { "additionalProperties": false, + "description": "Asserts the output has a specific size (in bytes) of ``value`` plus minus\n``delta``, e.g. ``<has_size value=\"10000\" delta=\"100\" />``.\n\nAlternatively the range of the expected size can be specified by ``min`` and/or\n``max``.\n$attribute_list::5", "properties": { "value": { "anyOf": [ @@ -1028,13 +1093,17 @@ "title": "Max" }, "negate": { - "allOf": [ + "anyOf": [ + { + "type": "boolean" + }, { - "$ref": "#/$defs/PermissiveBoolean" + "$ref": "#/$defs/PermissiveBooleanValue" } ], "default": "false", - "description": "Negate the outcome of the assertion." + "description": "Negate the outcome of the assertion.", + "title": "Negate" } }, "title": "AssertHasSize", @@ -1042,6 +1111,7 @@ }, "AssertHasText": { "additionalProperties": false, + "description": "Asserts the specified ``text`` appears in the output (e.g. ``<has_text\ntext=\"chr7\">``).\n\nIf the ``text`` is expected to occur a particular number of\ntimes, this value can be specified using ``n``. Optionally also with a certain\n``delta``. Alternatively the range of expected occurences can be specified by\n``min`` and/or ``max``.\n$attribute_list::5", "properties": { "text": { "description": "Text to check for", @@ -1113,13 +1183,17 @@ "title": "Max" }, "negate": { - "allOf": [ + "anyOf": [ + { + "type": "boolean" + }, { - "$ref": "#/$defs/PermissiveBoolean" + "$ref": "#/$defs/PermissiveBooleanValue" } ], "default": "false", - "description": "Negate the outcome of the assertion." + "description": "Negate the outcome of the assertion.", + "title": "Negate" } }, "required": ["text"], @@ -1128,6 +1202,7 @@ }, "AssertHasTextMatching": { "additionalProperties": false, + "description": "Asserts text matching the specified regular expression (``expression``)\nappears in the output (e.g. ``<has_text_matching expression=\"1274\\d+53\"\n/>`` ).\n\nIf the\nregular expression is expected to match a particular number of times, this value\ncan be specified using ``n``. Note only non-overlapping occurences are counted.\nOptionally also with a certain ``delta``. Alternatively the range of expected\noccurences can be specified by ``min`` and/or ``max``.\n$attribute_list::5", "properties": { "expression": { "description": "Regular expression to check for", @@ -1199,13 +1274,17 @@ "title": "Max" }, "negate": { - "allOf": [ + "anyOf": [ + { + "type": "boolean" + }, { - "$ref": "#/$defs/PermissiveBoolean" + "$ref": "#/$defs/PermissiveBooleanValue" } ], "default": "false", - "description": "Negate the outcome of the assertion." + "description": "Negate the outcome of the assertion.", + "title": "Negate" } }, "required": ["expression"], @@ -1214,12 +1293,14 @@ }, "AssertIsValidXml": { "additionalProperties": false, + "description": "Asserts the output is a valid XML file (e.g. ``<is_valid_xml />``).\n\n$attribute_list::5", "properties": {}, "title": "AssertIsValidXml", "type": "object" }, "AssertNotHasText": { "additionalProperties": false, + "description": "Asserts the specified ``text`` does not appear in the output (e.g.\n``<not_has_text text=\"chr8\" />``).\n\n$attribute_list::5", "properties": { "text": { "description": "Text to check for", @@ -1233,6 +1314,7 @@ }, "AssertXmlelement": { "additionalProperties": false, + "description": "Assert if the XML file contains element(s) or tag(s) with the specified\n[XPath-like ``path``](https://lxml.de/xpathxslt.html). If ``n`` and ``delta``\nor ``min`` and ``max`` are given also the number of occurences is checked.\n```xml\n<assert_contents>\n<xml_element path=\"./elem\"/>\n<xml_element path=\"./elem/more[2]\"/>\n<xml_element path=\".//more\" n=\"3\" delta=\"1\"/>\n</assert_contents>\n```\nWith ``negate=\"true\"`` the outcome of the assertions wrt the precence and number\nof ``path`` can be negated. If there are any sub assertions then check them against\n- the content of the attribute ``attribute``\n- the element's text if no attribute is given\n```xml\n<assert_contents>\n<xml_element path=\"./elem/more[2]\" attribute=\"name\">\n<has_text_matching expression=\"foo$\"/>\n</xml_element>\n</assert_contents>\n```\nSub-assertions are not subject to the ``negate`` attribute of ``xml_element``.\nIf ``all`` is ``true`` then the sub assertions are checked for all occurences.\nNote that all other XML assertions can be expressed by this assertion (Galaxy\nalso implements the other assertions by calling this one).\n$attribute_list::5", "properties": { "path": { "description": "Path to check for. Valid paths are the simplified subsets of XPath implemented by lxml.etree; https://lxml.de/xpathxslt.html for more information.", @@ -1240,13 +1322,17 @@ "type": "string" }, "all": { - "allOf": [ + "anyOf": [ + { + "type": "boolean" + }, { - "$ref": "#/$defs/PermissiveBoolean" + "$ref": "#/$defs/PermissiveBooleanValue" } ], "default": "false", - "description": "Check the sub-assertions for all paths matching the path. Default: false, i.e. only the first" + "description": "Check the sub-assertions for all paths matching the path. Default: false, i.e. only the first", + "title": "All" }, "attribute": { "anyOf": [ @@ -1326,372 +1412,1824 @@ "title": "Max" }, "negate": { - "allOf": [ + "anyOf": [ + { + "type": "boolean" + }, { - "$ref": "#/$defs/PermissiveBoolean" + "$ref": "#/$defs/PermissiveBooleanValue" } ], "default": "false", - "description": "Negate the outcome of the assertion." + "description": "Negate the outcome of the assertion.", + "title": "Negate" } }, "required": ["path"], "title": "AssertXmlelement", "type": "object" }, - "PermissiveBoolean": { - "description": "Documentation for PermissiveBoolean.", - "enum": ["0", "1", "true", "false", "True", "False", "yes", "no"], - "title": "PermissiveBoolean", - "type": "string" - }, - "Test": { - "additionalProperties": false, + "Collection": { "properties": { - "doc": { - "title": "Doc", + "class": { + "const": "Collection", + "title": "Class" + }, + "collection_type": { + "default": "list", + "title": "Collection Type", "type": "string" }, - "job": { - "title": "Job", + "elements": { + "items": { + "anyOf": [ + { + "$ref": "#/$defs/CollectionElement" + }, + { + "$ref": "#/$defs/LocationFileElement" + }, + { + "$ref": "#/$defs/PathFileElement" + }, + { + "$ref": "#/$defs/CompositeDataFileElement" + } + ] + }, + "title": "Elements", + "type": "array" + } + }, + "required": ["class", "elements"], + "title": "Collection", + "type": "object" + }, + "CollectionElement": { + "properties": { + "class": { + "const": "Collection", + "title": "Class" + }, + "identifier": { + "title": "Identifier", "type": "string" }, - "outputs": { - "additionalProperties": { + "elements": { + "items": { "anyOf": [ { - "$ref": "#/$defs/TestOutput" + "$ref": "#/$defs/CollectionElement" }, { - "$ref": "#/$defs/TestOutputCollection" + "$ref": "#/$defs/LocationFileElement" + }, + { + "$ref": "#/$defs/PathFileElement" + }, + { + "$ref": "#/$defs/CompositeDataFileElement" } ] }, - "title": "Outputs", - "type": "object" + "title": "Elements", + "type": "array" + }, + "type": { + "default": "list", + "title": "Type", + "type": "string" } }, - "required": ["doc", "job", "outputs"], - "title": "Test", + "required": ["class", "identifier", "elements"], + "title": "CollectionElement", "type": "object" }, - "TestAssertions": { + "CompositeDataFile": { "additionalProperties": false, "properties": { - "has_size": { + "class": { + "const": "File", + "title": "Class" + }, + "filetype": { "anyOf": [ { - "items": { - "$ref": "#/$defs/AssertHasSize" - }, - "type": "array" + "type": "string" }, { - "$ref": "#/$defs/AssertHasSize" + "type": "null" } ], - "title": "Has Size" + "default": null, + "description": "Datatype extension for uploaded dataset.", + "title": "Filetype" }, - "has_text": { + "dbkey": { "anyOf": [ { - "items": { - "$ref": "#/$defs/AssertHasText" - }, - "type": "array" + "type": "string" }, { - "$ref": "#/$defs/AssertHasText" + "type": "null" } ], - "title": "Has Text" + "default": null, + "title": "Dbkey" }, - "not_has_text": { + "decompress": { "anyOf": [ { - "items": { - "$ref": "#/$defs/AssertNotHasText" - }, - "type": "array" + "type": "boolean" }, { - "$ref": "#/$defs/AssertNotHasText" + "type": "null" } ], - "title": "Not Has Text" + "default": false, + "title": "Decompress" }, - "has_text_matching": { + "to_posix_line": { "anyOf": [ { - "items": { - "$ref": "#/$defs/AssertHasTextMatching" - }, - "type": "array" + "type": "boolean" }, { - "$ref": "#/$defs/AssertHasTextMatching" + "type": "null" } ], - "title": "Has Text Matching" + "default": null, + "title": "To Posix Line" }, - "has_line": { + "space_to_tab": { "anyOf": [ { - "items": { - "$ref": "#/$defs/AssertHasLine" - }, - "type": "array" + "type": "boolean" }, { - "$ref": "#/$defs/AssertHasLine" + "type": "null" } ], - "title": "Has Line" + "default": null, + "description": "If set, spaces in text datasets will be converted to tabs.", + "title": "Space To Tab" }, - "has_line_matching": { + "deferred": { "anyOf": [ { - "items": { - "$ref": "#/$defs/AssertHasLineMatching" - }, - "type": "array" + "type": "boolean" }, { - "$ref": "#/$defs/AssertHasLineMatching" + "type": "null" } ], - "title": "Has Line Matching" + "default": null, + "description": "If set, datasets will not be stored on disk, but will be downloaded when used as inputs. Can only be used if a remote URI is used instead of a local file.", + "title": "Deferred" }, - "has_n_lines": { + "name": { "anyOf": [ { - "items": { - "$ref": "#/$defs/AssertHasNlines" - }, - "type": "array" + "type": "string" }, { - "$ref": "#/$defs/AssertHasNlines" + "type": "null" } ], - "title": "Has N Lines" + "default": null, + "description": "Name of dataset in history.", + "title": "Name" }, - "has_n_columns": { + "info": { "anyOf": [ { - "items": { - "$ref": "#/$defs/AssertHasNcolumns" - }, - "type": "array" + "type": "string" }, { - "$ref": "#/$defs/AssertHasNcolumns" + "type": "null" } ], - "title": "Has N Columns" - }, - "has_archive_member": { - "items": { - "$ref": "#/$defs/AssertHasArchiveMember" - }, - "title": "Has Archive Member", - "type": "array" + "default": null, + "title": "Info" }, - "is_valid_xml": { + "tags": { "anyOf": [ { "items": { - "$ref": "#/$defs/AssertIsValidXml" + "type": "string" }, "type": "array" }, { - "$ref": "#/$defs/AssertIsValidXml" + "type": "null" } ], - "title": "Is Valid Xml" + "default": null, + "description": "Tags to apply to uploaded dataset.", + "title": "Tags" }, - "xml_element": { + "location": { "anyOf": [ { - "items": { - "$ref": "#/$defs/AssertXmlelement" - }, - "type": "array" + "type": "string" }, { - "$ref": "#/$defs/AssertXmlelement" + "type": "null" } ], - "title": "Xml Element" + "default": null, + "title": "Location" }, - "has_element_with_path": { + "path": { "anyOf": [ { - "items": { - "$ref": "#/$defs/AssertHasElementWithPath" - }, - "type": "array" + "type": "string" }, { - "$ref": "#/$defs/AssertHasElementWithPath" + "type": "null" } ], - "title": "Has Element With Path" + "default": null, + "title": "Path" }, - "has_n_elements_with_path": { + "composite_data": { + "items": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "title": "Composite Data", + "type": "array" + } + }, + "required": ["class", "composite_data"], + "title": "CompositeDataFile", + "type": "object" + }, + "CompositeDataFileElement": { + "additionalProperties": false, + "properties": { + "class": { + "const": "File", + "title": "Class" + }, + "filetype": { "anyOf": [ { - "items": { - "$ref": "#/$defs/AssertHasNelementsWithPath" - }, - "type": "array" + "type": "string" }, { - "$ref": "#/$defs/AssertHasNelementsWithPath" + "type": "null" } ], - "title": "Has N Elements With Path" + "default": null, + "description": "Datatype extension for uploaded dataset.", + "title": "Filetype" }, - "element_text_matches": { + "dbkey": { "anyOf": [ { - "items": { - "$ref": "#/$defs/AssertElementTextMatches" - }, - "type": "array" + "type": "string" }, { - "$ref": "#/$defs/AssertElementTextMatches" + "type": "null" } ], - "title": "Element Text Matches" + "default": null, + "title": "Dbkey" }, - "element_text_is": { + "decompress": { "anyOf": [ { - "items": { - "$ref": "#/$defs/AssertElementTextIs" - }, - "type": "array" + "type": "boolean" }, { - "$ref": "#/$defs/AssertElementTextIs" + "type": "null" } ], - "title": "Element Text Is" + "default": false, + "title": "Decompress" }, - "attribute_matches": { + "to_posix_line": { "anyOf": [ { - "items": { - "$ref": "#/$defs/AssertAttributeMatches" - }, - "type": "array" + "type": "boolean" }, { - "$ref": "#/$defs/AssertAttributeMatches" + "type": "null" } ], - "title": "Attribute Matches" + "default": null, + "title": "To Posix Line" }, - "attribute_is": { + "space_to_tab": { "anyOf": [ { - "items": { - "$ref": "#/$defs/AssertAttributeIs" - }, - "type": "array" + "type": "boolean" }, { - "$ref": "#/$defs/AssertAttributeIs" + "type": "null" } ], - "title": "Attribute Is" + "default": null, + "description": "If set, spaces in text datasets will be converted to tabs.", + "title": "Space To Tab" }, - "element_text": { + "deferred": { "anyOf": [ { - "items": { - "$ref": "#/$defs/AssertElementText" - }, - "type": "array" + "type": "boolean" }, { - "$ref": "#/$defs/AssertElementText" + "type": "null" } ], - "title": "Element Text" + "default": null, + "description": "If set, datasets will not be stored on disk, but will be downloaded when used as inputs. Can only be used if a remote URI is used instead of a local file.", + "title": "Deferred" }, - "has_json_property_with_value": { + "name": { "anyOf": [ { - "items": { - "$ref": "#/$defs/AssertHasJsonPropertyWithValue" - }, - "type": "array" + "type": "string" }, { - "$ref": "#/$defs/AssertHasJsonPropertyWithValue" + "type": "null" } ], - "title": "Has Json Property With Value" + "default": null, + "description": "Name of dataset in history.", + "title": "Name" }, - "has_json_property_with_text": { + "info": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Info" + }, + "tags": { "anyOf": [ { "items": { - "$ref": "#/$defs/AssertHasJsonPropertyWithText" + "type": "string" }, "type": "array" }, { - "$ref": "#/$defs/AssertHasJsonPropertyWithText" + "type": "null" } ], - "title": "Has Json Property With Text" + "default": null, + "description": "Tags to apply to uploaded dataset.", + "title": "Tags" }, - "has_h5_keys": { + "identifier": { + "title": "Identifier", + "type": "string" + }, + "location": { "anyOf": [ { - "items": { - "$ref": "#/$defs/AssertHasH5Keys" + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Location" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path" + }, + "composite_data": { + "items": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "title": "Composite Data", + "type": "array" + } + }, + "required": ["class", "identifier", "composite_data"], + "title": "CompositeDataFileElement", + "type": "object" + }, + "Job": { + "anyOf": [ + { + "additionalProperties": { + "anyOf": [ + { + "type": "string" }, - "type": "array" + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "$ref": "#/$defs/Collection" + }, + { + "$ref": "#/$defs/LocationFile" + }, + { + "$ref": "#/$defs/PathFile" + }, + { + "$ref": "#/$defs/CompositeDataFile" + }, + { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "$ref": "#/$defs/Collection" + }, + { + "$ref": "#/$defs/LocationFile" + }, + { + "$ref": "#/$defs/PathFile" + }, + { + "$ref": "#/$defs/CompositeDataFile" + } + ] + }, + "type": "array" + }, + { + "type": "null" + } + ] + }, + "type": "object" + }, + { + "type": "string" + } + ], + "title": "Job" + }, + "LocationFile": { + "additionalProperties": false, + "properties": { + "class": { + "const": "File", + "title": "Class" + }, + "filetype": { + "anyOf": [ + { + "type": "string" }, { - "$ref": "#/$defs/AssertHasH5Keys" + "type": "null" } ], - "title": "Has H5 Keys" + "default": null, + "description": "Datatype extension for uploaded dataset.", + "title": "Filetype" }, - "has_h5_attribute": { + "dbkey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Dbkey" + }, + "decompress": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": false, + "title": "Decompress" + }, + "to_posix_line": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "To Posix Line" + }, + "space_to_tab": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If set, spaces in text datasets will be converted to tabs.", + "title": "Space To Tab" + }, + "deferred": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If set, datasets will not be stored on disk, but will be downloaded when used as inputs. Can only be used if a remote URI is used instead of a local file.", + "title": "Deferred" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Name of dataset in history.", + "title": "Name" + }, + "info": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Info" + }, + "tags": { "anyOf": [ { "items": { - "$ref": "#/$defs/AssertHasH5Attribute" + "type": "string" }, "type": "array" }, { - "$ref": "#/$defs/AssertHasH5Attribute" + "type": "null" } ], - "title": "Has H5 Attribute" + "default": null, + "description": "Tags to apply to uploaded dataset.", + "title": "Tags" + }, + "location": { + "title": "Location", + "type": "string" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path" } }, - "title": "TestAssertions", + "required": ["class", "location"], + "title": "LocationFile", "type": "object" }, - "TestDiscoveredDataset": { + "LocationFileElement": { "additionalProperties": false, "properties": { - "elements": { - "items": { - "$ref": "#/$defs/TestOutput" - }, - "title": "Elements", - "type": "array" + "class": { + "const": "File", + "title": "Class" + }, + "filetype": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Datatype extension for uploaded dataset.", + "title": "Filetype" + }, + "dbkey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Dbkey" + }, + "decompress": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": false, + "title": "Decompress" + }, + "to_posix_line": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "To Posix Line" + }, + "space_to_tab": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If set, spaces in text datasets will be converted to tabs.", + "title": "Space To Tab" + }, + "deferred": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If set, datasets will not be stored on disk, but will be downloaded when used as inputs. Can only be used if a remote URI is used instead of a local file.", + "title": "Deferred" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Name of dataset in history.", + "title": "Name" + }, + "info": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Info" + }, + "tags": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Tags to apply to uploaded dataset.", + "title": "Tags" + }, + "identifier": { + "title": "Identifier", + "type": "string" + }, + "location": { + "title": "Location", + "type": "string" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path" + } + }, + "required": ["class", "identifier", "location"], + "title": "LocationFileElement", + "type": "object" + }, + "PathFile": { + "additionalProperties": false, + "properties": { + "class": { + "const": "File", + "title": "Class" + }, + "filetype": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Datatype extension for uploaded dataset.", + "title": "Filetype" + }, + "dbkey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Dbkey" + }, + "decompress": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": false, + "title": "Decompress" + }, + "to_posix_line": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "To Posix Line" + }, + "space_to_tab": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If set, spaces in text datasets will be converted to tabs.", + "title": "Space To Tab" + }, + "deferred": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If set, datasets will not be stored on disk, but will be downloaded when used as inputs. Can only be used if a remote URI is used instead of a local file.", + "title": "Deferred" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Name of dataset in history.", + "title": "Name" + }, + "info": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Info" + }, + "tags": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Tags to apply to uploaded dataset.", + "title": "Tags" + }, + "path": { + "title": "Path", + "type": "string" + } + }, + "required": ["class", "path"], + "title": "PathFile", + "type": "object" + }, + "PathFileElement": { + "additionalProperties": false, + "properties": { + "class": { + "const": "File", + "title": "Class" + }, + "filetype": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Datatype extension for uploaded dataset.", + "title": "Filetype" + }, + "dbkey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Dbkey" + }, + "decompress": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": false, + "title": "Decompress" + }, + "to_posix_line": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "To Posix Line" + }, + "space_to_tab": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If set, spaces in text datasets will be converted to tabs.", + "title": "Space To Tab" + }, + "deferred": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If set, datasets will not be stored on disk, but will be downloaded when used as inputs. Can only be used if a remote URI is used instead of a local file.", + "title": "Deferred" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Name of dataset in history.", + "title": "Name" + }, + "info": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Info" + }, + "tags": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Tags to apply to uploaded dataset.", + "title": "Tags" + }, + "identifier": { + "title": "Identifier", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + } + }, + "required": ["class", "identifier", "path"], + "title": "PathFileElement", + "type": "object" + }, + "PermissiveBooleanValue": { + "enum": ["0", "1", "true", "false", "True", "False", "yes", "no"], + "title": "PermissiveBooleanValue", + "type": "string" + }, + "Test": { + "additionalProperties": false, + "properties": { + "doc": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Describes the purpose of the test.", + "title": "Doc" + }, + "job": { + "allOf": [ + { + "$ref": "#/$defs/Job" + } + ], + "description": "Defines job to execute. Can be a path to a file or an line dictionary describing the job inputs." + }, + "outputs": { + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/TestOutputElement" + }, + { + "$ref": "#/$defs/TestOutput" + }, + { + "$ref": "#/$defs/TestOutputCollection" + }, + { + "$ref": "#/$defs/TestOutputCollectionDeprecated" + }, + { + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + }, + "description": "Defines assertions about outputs (datasets, collections or parameters). Each key corresponds to a labeled output, values are dictionaries describing the expected output.", + "title": "Outputs", + "type": "object" + } + }, + "required": ["job", "outputs"], + "title": "Test", + "type": "object" + }, + "TestAssertions": { + "additionalProperties": false, + "description": "This tag set defines a sequence of checks or assertions to run against the\ntarget output.\n\nThis tag requires no attributes, but child tags should be used to\ndefine the assertions to make about the output. The functional test framework\nmakes it easy to extend Galaxy with such tags, the following table summarizes\nmany of the default assertion tags that come with Galaxy and examples of each\ncan be found below.\nThe implementation of these tags are simply Python functions defined in the\n[/lib/galaxy/tool_util/verify/asserts](https://github.com/galaxyproject/galaxy/tree/dev/lib/galaxy/tool_util/verify/asserts)\nmodule.", + "properties": { + "has_size": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasSize" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasSize" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Has Size" + }, + "has_text": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasText" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasText" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Has Text" + }, + "not_has_text": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertNotHasText" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertNotHasText" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Not Has Text" + }, + "has_text_matching": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasTextMatching" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasTextMatching" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Has Text Matching" + }, + "has_line": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasLine" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasLine" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Has Line" + }, + "has_line_matching": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasLineMatching" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasLineMatching" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Has Line Matching" + }, + "has_n_lines": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasNlines" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasNlines" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Has N Lines" + }, + "has_n_columns": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasNcolumns" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasNcolumns" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Has N Columns" + }, + "has_archive_member": { + "items": { + "$ref": "#/$defs/AssertHasArchiveMember" + }, + "title": "Has Archive Member", + "type": "array" + }, + "is_valid_xml": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertIsValidXml" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertIsValidXml" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Is Valid Xml" + }, + "xml_element": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertXmlelement" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertXmlelement" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Xml Element" + }, + "has_element_with_path": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasElementWithPath" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasElementWithPath" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Has Element With Path" + }, + "has_n_elements_with_path": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasNelementsWithPath" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasNelementsWithPath" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Has N Elements With Path" + }, + "element_text_matches": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertElementTextMatches" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertElementTextMatches" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Element Text Matches" + }, + "element_text_is": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertElementTextIs" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertElementTextIs" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Element Text Is" + }, + "attribute_matches": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertAttributeMatches" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertAttributeMatches" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Attribute Matches" + }, + "attribute_is": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertAttributeIs" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertAttributeIs" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Attribute Is" + }, + "element_text": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertElementText" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertElementText" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Element Text" + }, + "has_json_property_with_value": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasJsonPropertyWithValue" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasJsonPropertyWithValue" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Has Json Property With Value" + }, + "has_json_property_with_text": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasJsonPropertyWithText" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasJsonPropertyWithText" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Has Json Property With Text" + }, + "has_h5_keys": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasH5Keys" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasH5Keys" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Has H5 Keys" + }, + "has_h5_attribute": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/AssertHasH5Attribute" + }, + "type": "array" + }, + { + "$ref": "#/$defs/AssertHasH5Attribute" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Has H5 Attribute" + } + }, + "title": "TestAssertions", + "type": "object" + }, + "TestDiscoveredDataset": { + "additionalProperties": false, + "description": "This directive specifies a test for an output's discovered dataset.\n\nIt acts as an\n``output`` test tag in many ways and can define any tests of that tag (e.g.\n``assert_contents``, ``value``, ``compare``, ``md5``, ``checksum``, ``metadata``, etc...).\n### Example\nThe functional test tool\n[multi_output_assign_primary.xml](https://github.com/galaxyproject/galaxy/blob/dev/test/functional/tools/multi_output_assign_primary.xml)\nprovides a demonstration of using this tag.\n```xml\n<outputs>\n<data format=\"tabular\" name=\"sample\">\n<discover_datasets pattern=\"(?P&lt;designation&gt;.+)\\.report\\.tsv\" ext=\"tabular\" visible=\"true\" assign_primary_output=\"true\" />\n</data>\n</outputs>\n<test>\n<param name=\"num_param\" value=\"7\" />\n<param name=\"input\" ftype=\"txt\" value=\"simple_line.txt\"/>\n<output name=\"sample\">\n<assert_contents>\n<has_line line=\"1\" />\n</assert_contents>\n<!-- no sample1 it was consumed by named output \"sample\" -->\n<discovered_dataset designation=\"sample2\" ftype=\"tabular\">\n<assert_contents><has_line line=\"2\" /></assert_contents>\n</discovered_dataset>\n<discovered_dataset designation=\"sample3\" ftype=\"tabular\">\n<assert_contents><has_line line=\"3\" /></assert_contents>\n</discovered_dataset>\n</output>\n</test>\n```\nNote that this tool uses ``assign_primary_output=\"true\"`` for ``<discover_datasets>``. Hence, the content of the first discovered dataset (which is the first in the alphabetically sorted list of discovered designations) is checked directly in the ``<output>`` tag of the test.", + "properties": { + "class": { + "anyOf": [ + { + "const": "File" + }, + { + "type": "null" + } + ], + "default": "File", + "title": "Class" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path" + }, + "discovered_dataset": { + "items": { + "$ref": "#/$defs/TestDiscoveredDataset" + }, + "title": "Discovered Dataset", + "type": "array" + }, + "asserts": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/TestAssertions" + }, + "type": "array" + }, + { + "$ref": "#/$defs/TestAssertions" + }, + { + "type": "null" + } + ], + "default": null, + "description": "$assertions\n### Examples\nThe following demonstrates a wide variety of text-based and tabular\nassertion statements.\n```xml\n<output name=\"out_file1\">\n<assert_contents>\n<has_text text=\"chr7\" />\n<not_has_text text=\"chr8\" />\n<has_text_matching expression=\"1274\\d+53\" />\n<has_line_matching expression=\".*\\s+127489808\\s+127494553\" />\n<!-- &#009; is XML escape code for tab -->\n<has_line line=\"chr7&#009;127471195&#009;127489808\" />\n<has_n_columns n=\"3\" />\n</assert_contents>\n</output>\n```\nThe following demonstrates a wide variety of XML assertion statements.\n```xml\n<output name=\"out_file1\">\n<assert_contents>\n<is_valid_xml />\n<has_element_with_path path=\"BlastOutput_param/Parameters/Parameters_matrix\" />\n<has_n_elements_with_path n=\"9\" path=\"BlastOutput_iterations/Iteration/Iteration_hits/Hit/Hit_num\" />\n<element_text_matches path=\"BlastOutput_version\" expression=\"BLASTP\\s+2\\.2.*\" />\n<element_text_is path=\"BlastOutput_program\" text=\"blastp\" />\n<element_text path=\"BlastOutput_iterations/Iteration/Iteration_hits/Hit/Hit_def\">\n<not_has_text text=\"EDK72998.1\" />\n<has_text_matching expression=\"ABK[\\d\\.]+\" />\n</element_text>\n</assert_contents>\n</output>\n```\nThe following demonstrates verifying XML content with XPath-like expressions.\n```xml\n<output name=\"out_file1\">\n<assert_contents>\n<attribute_is path=\"outerElement/innerElement1\" attribute=\"foo\" text=\"bar\" />\n<attribute_matches path=\"outerElement/innerElement2\" attribute=\"foo2\" expression=\"bar\\d+\" />\n</assert_contents>\n</output>\n```", + "title": "Asserts" + }, + "extra_files": { + "items": { + "$ref": "#/$defs/TestExtraFile" + }, + "title": "Extra Files", + "type": "array" + }, + "metadata": { + "items": { + "$ref": "#/$defs/TestOutputMetadata" + }, + "title": "Metadata", + "type": "array" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "This value is the same as the value of the ``name`` attribute of the ``<data>``\ntag set contained within the tool's ``<outputs>`` tag set.", + "title": "Name" + }, + "file": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, this value is the name of the output file stored in the target\n``test-data`` directory which will be used to compare the results of executing\nthe tool via the functional test framework.", + "title": "File" + }, + "value_json": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, this value will be loaded as JSON and compared against the output\ngenerated as JSON. This can be useful for testing tool outputs that are not files.", + "title": "Value Json" + }, + "ftype": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, this value will be checked against the corresponding output's\ndata type. If these do not match, the test will fail.", + "title": "Ftype" + }, + "sort": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/PermissiveBooleanValue" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Applies only if ``compare`` is ``diff``, ``re_match`` or ``re_match_multiline``. This flag causes the lines of the history data set to be sorted before the comparison. In case of ``diff`` and ``re_match`` also the local file is sorted. This could be\nuseful for non-deterministic output.", + "title": "Sort" + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "An alias for ``file``.", + "title": "Value" + }, + "md5": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, the target output's MD5 hash should match the value specified\nhere. For large static files it may be inconvenient to upload the entiry file\nand this can be used instead.", + "title": "Md5" + }, + "checksum": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If specified, the target output's checksum should match the value specified\nhere. This value should have the form ``hash_type$hash_value``\n(e.g. ``sha1$8156d7ca0f46ed7abac98f82e36cfaddb2aca041``). For large static files\nit may be inconvenient to upload the entiry file and this can be used instead.", + "title": "Checksum" + }, + "compare": { + "anyOf": [ + { + "$ref": "#/$defs/TestOutputCompareType" + }, + { + "type": "null" + } + ], + "default": null + }, + "lines_diff": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Applies only if ``compare`` is set to ``diff``, ``re_match``, and ``contains``. If ``compare`` is set to ``diff``, the number of lines of difference to allow (each line with a modification is a line added and a line removed so this counts as two lines).", + "title": "Lines Diff" + }, + "decompress": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/$defs/PermissiveBooleanValue" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If this attribute is true then try to decompress files if needed. This applies to\ntest assertions expressed with ``assert_contents`` or ``compare`` set to anything\nbut ``sim_size``.\nThis flag is useful for testing compressed outputs that are non-deterministic\ndespite having deterministic decompressed contents. By default, only files compressed\nwith bz2, gzip and zip will be automatically decompressed.\nNote, for specifying assertions for compressed as well as decompressed output\nthe corresponding output tag can be specified multiple times.\nThis is available in Galaxy since release 17.05 and was introduced in [pull request #3550](https://github.com/galaxyproject/galaxy/pull/3550).", + "title": "Decompress" + }, + "delta": { + "default": 10000, + "description": "If ``compare`` is set to ``sim_size``, this is the maximum allowed absolute size difference (in bytes) between the data set that is generated in the test and the file in ``test-data/`` that is referenced by the ``file`` attribute. Default value is 10000 bytes. Can be combined with ``delta_frac``.", + "title": "Delta", + "type": "integer" + }, + "delta_frac": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If ``compare`` is set to ``sim_size``, this is the maximum allowed relative size difference between the data set that is generated in the test and the file in ``test-data/`` that is referenced by the ``file`` attribute. A value of 0.1 means that the file that is generated in the test can differ by at most 10% of the file in ``test-data``. The default is not to check for relative size difference. Can be combined with ``delta``.", + "title": "Delta Frac" + }, + "count": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Number or datasets for this output. Should be used for outputs with ``discover_datasets``", + "title": "Count" + }, + "location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "URL that points to a remote output file that will downloaded and used for output comparison.\nPlease use this option only when is not possible to include the files in the `test-data` folder, since\nthis is more error prone due to external factors like remote availability.\nYou can use it in two ways:\n- In combination with `file` it will look for the output file in the `test-data` folder, if it's not available on disk it will\ndownload the file pointed by `location` using the same name as in `file` (or `value`).\n- Specifiying the `location` without a `file` (or `value`), it will download the file and use it as an alias of `file`. The name of the file\nwill be infered from the last component of the location URL. For example, `location=\"https://my_url/my_file.txt\"` will be equivalent to `file=\"my_file.txt\"`.\nIf you specify a `checksum`, it will be also used to check the integrity of the download.", + "title": "Location" + }, + "designation": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The designation of the discovered dataset.", + "title": "Designation" + } + }, + "title": "TestDiscoveredDataset", + "type": "object" + }, + "TestExtraFile": { + "additionalProperties": false, + "description": "Define test for extra files on corresponding output.", + "properties": { + "class": { + "anyOf": [ + { + "const": "File" + }, + { + "type": "null" + } + ], + "default": "File", + "title": "Class" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path" }, "discovered_dataset": { "items": { @@ -1710,8 +3248,12 @@ }, { "$ref": "#/$defs/TestAssertions" + }, + { + "type": "null" } ], + "default": null, "description": "$assertions\n### Examples\nThe following demonstrates a wide variety of text-based and tabular\nassertion statements.\n```xml\n<output name=\"out_file1\">\n<assert_contents>\n<has_text text=\"chr7\" />\n<not_has_text text=\"chr8\" />\n<has_text_matching expression=\"1274\\d+53\" />\n<has_line_matching expression=\".*\\s+127489808\\s+127494553\" />\n<!-- &#009; is XML escape code for tab -->\n<has_line line=\"chr7&#009;127471195&#009;127489808\" />\n<has_n_columns n=\"3\" />\n</assert_contents>\n</output>\n```\nThe following demonstrates a wide variety of XML assertion statements.\n```xml\n<output name=\"out_file1\">\n<assert_contents>\n<is_valid_xml />\n<has_element_with_path path=\"BlastOutput_param/Parameters/Parameters_matrix\" />\n<has_n_elements_with_path n=\"9\" path=\"BlastOutput_iterations/Iteration/Iteration_hits/Hit/Hit_num\" />\n<element_text_matches path=\"BlastOutput_version\" expression=\"BLASTP\\s+2\\.2.*\" />\n<element_text_is path=\"BlastOutput_program\" text=\"blastp\" />\n<element_text path=\"BlastOutput_iterations/Iteration/Iteration_hits/Hit/Hit_def\">\n<not_has_text text=\"EDK72998.1\" />\n<has_text_matching expression=\"ABK[\\d\\.]+\" />\n</element_text>\n</assert_contents>\n</output>\n```\nThe following demonstrates verifying XML content with XPath-like expressions.\n```xml\n<output name=\"out_file1\">\n<assert_contents>\n<attribute_is path=\"outerElement/innerElement1\" attribute=\"foo\" text=\"bar\" />\n<attribute_matches path=\"outerElement/innerElement2\" attribute=\"foo2\" expression=\"bar\\d+\" />\n</assert_contents>\n</output>\n```", "title": "Asserts" }, @@ -1784,14 +3326,18 @@ "sort": { "anyOf": [ { - "$ref": "#/$defs/PermissiveBoolean" + "type": "boolean" + }, + { + "$ref": "#/$defs/PermissiveBooleanValue" }, { "type": "null" } ], "default": null, - "description": "Applies only if ``compare`` is ``diff``, ``re_match`` or ``re_match_multiline``. This flag causes the lines of the history data set to be sorted before the comparison. In case of ``diff`` and ``re_match`` also the local file is sorted. This could be\nuseful for non-deterministic output." + "description": "Applies only if ``compare`` is ``diff``, ``re_match`` or ``re_match_multiline``. This flag causes the lines of the history data set to be sorted before the comparison. In case of ``diff`` and ``re_match`` also the local file is sorted. This could be\nuseful for non-deterministic output.", + "title": "Sort" }, "value": { "anyOf": [ @@ -1859,14 +3405,18 @@ "decompress": { "anyOf": [ { - "$ref": "#/$defs/PermissiveBoolean" + "type": "boolean" + }, + { + "$ref": "#/$defs/PermissiveBooleanValue" }, { "type": "null" } ], "default": null, - "description": "If this attribute is true then try to decompress files if needed. This applies to\ntest assertions expressed with ``assert_contents`` or ``compare`` set to anything\nbut ``sim_size``.\nThis flag is useful for testing compressed outputs that are non-deterministic\ndespite having deterministic decompressed contents. By default, only files compressed\nwith bz2, gzip and zip will be automatically decompressed.\nNote, for specifying assertions for compressed as well as decompressed output\nthe corresponding output tag can be specified multiple times.\nThis is available in Galaxy since release 17.05 and was introduced in [pull request #3550](https://github.com/galaxyproject/galaxy/pull/3550)." + "description": "If this attribute is true then try to decompress files if needed. This applies to\ntest assertions expressed with ``assert_contents`` or ``compare`` set to anything\nbut ``sim_size``.\nThis flag is useful for testing compressed outputs that are non-deterministic\ndespite having deterministic decompressed contents. By default, only files compressed\nwith bz2, gzip and zip will be automatically decompressed.\nNote, for specifying assertions for compressed as well as decompressed output\nthe corresponding output tag can be specified multiple times.\nThis is available in Galaxy since release 17.05 and was introduced in [pull request #3550](https://github.com/galaxyproject/galaxy/pull/3550).", + "title": "Decompress" }, "delta": { "default": 10000, @@ -1913,7 +3463,7 @@ "description": "URL that points to a remote output file that will downloaded and used for output comparison.\nPlease use this option only when is not possible to include the files in the `test-data` folder, since\nthis is more error prone due to external factors like remote availability.\nYou can use it in two ways:\n- In combination with `file` it will look for the output file in the `test-data` folder, if it's not available on disk it will\ndownload the file pointed by `location` using the same name as in `file` (or `value`).\n- Specifiying the `location` without a `file` (or `value`), it will download the file and use it as an alias of `file`. The name of the file\nwill be infered from the last component of the location URL. For example, `location=\"https://my_url/my_file.txt\"` will be equivalent to `file=\"my_file.txt\"`.\nIf you specify a `checksum`, it will be also used to check the integrity of the download.", "title": "Location" }, - "designation": { + "type_value": { "anyOf": [ { "type": "string" @@ -1923,22 +3473,40 @@ } ], "default": null, - "description": "The designation of the discovered dataset.", - "title": "Designation" + "description": "Extra file type (either ``file`` or ``directory``).", + "title": "Type Value" } }, - "title": "TestDiscoveredDataset", + "title": "TestExtraFile", "type": "object" }, - "TestExtraFile": { + "TestOutput": { "additionalProperties": false, + "description": "This tag set defines the variable that names the output dataset for the\nfunctional test framework. The functional test framework will execute the tool\nusing the parameters defined in the ``<param>`` tag sets and generate a\ntemporary file, which will either be compared with the file named in the\n``file`` attribute value or checked against assertions made by a child\n``assert_contents`` tag to verify that the tool is functionally correct.\nDifferent methods can be chosen for the comparison with the local file\nspecified.\n\nby ``file`` using the ``compare`` attribute:\n- ``diff``: uses diff to compare the history data set and the file provided by\n``file``. Compressed files are decompressed before the compariopm if\n``decompress`` is set to ``true``. BAM files are converted to SAM before the\ncomparision and for pdf some special rules are implemented. The number of\nallowed differences can be set with ``lines_diff``. If ``sort=\"true\"`` history\nand local data is sorted before the comparison.\n- ``re_match``: each line of the history data set is compared to the regular\nexpression specified in the corresponding line of the ``file``. The allowed\nnumber of non matching lines can be set with ``lines_diff`` and the history\ndataset is sorted if ``sort`` is set to ``true``.\n- ``re_match_multiline``: it is checked if the history data sets matches the\nmulti line regular expression given in ``file``. The history dataset is sorted\nbefore the comparison if the ``sort`` atrribute is set to ``true``.\n- ``contains``: check if each line in ``file`` is contained in the history data set.\nThe allowed number of lines that are not contained in the history dataset\ncan be set with ``lines_diff``.\n- ``sim_size``: compares the size of the history dataset and the ``file`` subject to\nthe values of the ``delta`` and ``delta_frac`` attributes. Note that a ``has_size``\ncontent assertion should be preferred, because this avoids storing the test file.", "properties": { - "elements": { - "items": { - "$ref": "#/$defs/TestOutput" - }, - "title": "Elements", - "type": "array" + "class": { + "anyOf": [ + { + "const": "File" + }, + { + "type": "null" + } + ], + "default": "File", + "title": "Class" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path" }, "discovered_dataset": { "items": { @@ -1957,8 +3525,12 @@ }, { "$ref": "#/$defs/TestAssertions" + }, + { + "type": "null" } ], + "default": null, "description": "$assertions\n### Examples\nThe following demonstrates a wide variety of text-based and tabular\nassertion statements.\n```xml\n<output name=\"out_file1\">\n<assert_contents>\n<has_text text=\"chr7\" />\n<not_has_text text=\"chr8\" />\n<has_text_matching expression=\"1274\\d+53\" />\n<has_line_matching expression=\".*\\s+127489808\\s+127494553\" />\n<!-- &#009; is XML escape code for tab -->\n<has_line line=\"chr7&#009;127471195&#009;127489808\" />\n<has_n_columns n=\"3\" />\n</assert_contents>\n</output>\n```\nThe following demonstrates a wide variety of XML assertion statements.\n```xml\n<output name=\"out_file1\">\n<assert_contents>\n<is_valid_xml />\n<has_element_with_path path=\"BlastOutput_param/Parameters/Parameters_matrix\" />\n<has_n_elements_with_path n=\"9\" path=\"BlastOutput_iterations/Iteration/Iteration_hits/Hit/Hit_num\" />\n<element_text_matches path=\"BlastOutput_version\" expression=\"BLASTP\\s+2\\.2.*\" />\n<element_text_is path=\"BlastOutput_program\" text=\"blastp\" />\n<element_text path=\"BlastOutput_iterations/Iteration/Iteration_hits/Hit/Hit_def\">\n<not_has_text text=\"EDK72998.1\" />\n<has_text_matching expression=\"ABK[\\d\\.]+\" />\n</element_text>\n</assert_contents>\n</output>\n```\nThe following demonstrates verifying XML content with XPath-like expressions.\n```xml\n<output name=\"out_file1\">\n<assert_contents>\n<attribute_is path=\"outerElement/innerElement1\" attribute=\"foo\" text=\"bar\" />\n<attribute_matches path=\"outerElement/innerElement2\" attribute=\"foo2\" expression=\"bar\\d+\" />\n</assert_contents>\n</output>\n```", "title": "Asserts" }, @@ -2031,14 +3603,18 @@ "sort": { "anyOf": [ { - "$ref": "#/$defs/PermissiveBoolean" + "type": "boolean" + }, + { + "$ref": "#/$defs/PermissiveBooleanValue" }, { "type": "null" } ], "default": null, - "description": "Applies only if ``compare`` is ``diff``, ``re_match`` or ``re_match_multiline``. This flag causes the lines of the history data set to be sorted before the comparison. In case of ``diff`` and ``re_match`` also the local file is sorted. This could be\nuseful for non-deterministic output." + "description": "Applies only if ``compare`` is ``diff``, ``re_match`` or ``re_match_multiline``. This flag causes the lines of the history data set to be sorted before the comparison. In case of ``diff`` and ``re_match`` also the local file is sorted. This could be\nuseful for non-deterministic output.", + "title": "Sort" }, "value": { "anyOf": [ @@ -2106,14 +3682,18 @@ "decompress": { "anyOf": [ { - "$ref": "#/$defs/PermissiveBoolean" + "type": "boolean" + }, + { + "$ref": "#/$defs/PermissiveBooleanValue" }, { "type": "null" } ], "default": null, - "description": "If this attribute is true then try to decompress files if needed. This applies to\ntest assertions expressed with ``assert_contents`` or ``compare`` set to anything\nbut ``sim_size``.\nThis flag is useful for testing compressed outputs that are non-deterministic\ndespite having deterministic decompressed contents. By default, only files compressed\nwith bz2, gzip and zip will be automatically decompressed.\nNote, for specifying assertions for compressed as well as decompressed output\nthe corresponding output tag can be specified multiple times.\nThis is available in Galaxy since release 17.05 and was introduced in [pull request #3550](https://github.com/galaxyproject/galaxy/pull/3550)." + "description": "If this attribute is true then try to decompress files if needed. This applies to\ntest assertions expressed with ``assert_contents`` or ``compare`` set to anything\nbut ``sim_size``.\nThis flag is useful for testing compressed outputs that are non-deterministic\ndespite having deterministic decompressed contents. By default, only files compressed\nwith bz2, gzip and zip will be automatically decompressed.\nNote, for specifying assertions for compressed as well as decompressed output\nthe corresponding output tag can be specified multiple times.\nThis is available in Galaxy since release 17.05 and was introduced in [pull request #3550](https://github.com/galaxyproject/galaxy/pull/3550).", + "title": "Decompress" }, "delta": { "default": 10000, @@ -2159,8 +3739,83 @@ "default": null, "description": "URL that points to a remote output file that will downloaded and used for output comparison.\nPlease use this option only when is not possible to include the files in the `test-data` folder, since\nthis is more error prone due to external factors like remote availability.\nYou can use it in two ways:\n- In combination with `file` it will look for the output file in the `test-data` folder, if it's not available on disk it will\ndownload the file pointed by `location` using the same name as in `file` (or `value`).\n- Specifiying the `location` without a `file` (or `value`), it will download the file and use it as an alias of `file`. The name of the file\nwill be infered from the last component of the location URL. For example, `location=\"https://my_url/my_file.txt\"` will be equivalent to `file=\"my_file.txt\"`.\nIf you specify a `checksum`, it will be also used to check the integrity of the download.", "title": "Location" + } + }, + "title": "TestOutput", + "type": "object" + }, + "TestOutputCollection": { + "additionalProperties": false, + "properties": { + "class": { + "anyOf": [ + { + "const": "Collection" + }, + { + "type": "null" + } + ], + "default": "Collection", + "title": "Class" }, - "type_value": { + "element": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/TestOutput" + }, + "type": "array" + }, + { + "$ref": "#/$defs/TestOutput" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Element" + }, + "count": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Number of elements in output collection.", + "title": "Count" + } + }, + "title": "TestOutputCollection", + "type": "object" + }, + "TestOutputCollectionDeprecated": { + "additionalProperties": false, + "properties": { + "element_tests": { + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/TestOutputElement" + }, + { + "$ref": "#/$defs/TestOutput" + } + ] + }, + "description": "Deprecated field, please use elements to describe expectations about collection elements.", + "metadata": { + "deprecated": true + }, + "title": "Element Tests", + "type": "object" + }, + "collection_type": { "anyOf": [ { "type": "string" @@ -2170,22 +3825,102 @@ } ], "default": null, - "description": "Extra file type (either ``file`` or ``directory``).", - "title": "Type Value" + "title": "Collection Type" + }, + "class": { + "anyOf": [ + { + "const": "Collection" + }, + { + "type": "null" + } + ], + "default": "Collection", + "title": "Class" + }, + "element": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/TestOutput" + }, + "type": "array" + }, + { + "$ref": "#/$defs/TestOutput" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Element" + }, + "count": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Number of elements in output collection.", + "title": "Count" } }, - "title": "TestExtraFile", + "required": ["element_tests"], + "title": "TestOutputCollectionDeprecated", "type": "object" }, - "TestOutput": { + "TestOutputCompareType": { + "description": "Type of comparison to use when comparing test generated output files to\nexpected output files.\n\nCurrently valid value are\n``diff`` (the default), ``re_match``, ``re_match_multiline``,\nand ``contains``. In addition there is ``sim_size`` which is discouraged in favour of a ``has_size`` assertion.", + "enum": ["diff", "re_match", "sim_size", "re_match_multiline", "contains"], + "title": "TestOutputCompareType", + "type": "string" + }, + "TestOutputElement": { "additionalProperties": false, "properties": { "elements": { - "items": { - "$ref": "#/$defs/TestOutput" + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/TestOutputElement" + }, + { + "$ref": "#/$defs/TestOutput" + } + ] }, "title": "Elements", - "type": "array" + "type": "object" + }, + "class": { + "anyOf": [ + { + "const": "File" + }, + { + "type": "null" + } + ], + "default": "File", + "title": "Class" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path" }, "discovered_dataset": { "items": { @@ -2204,8 +3939,12 @@ }, { "$ref": "#/$defs/TestAssertions" + }, + { + "type": "null" } ], + "default": null, "description": "$assertions\n### Examples\nThe following demonstrates a wide variety of text-based and tabular\nassertion statements.\n```xml\n<output name=\"out_file1\">\n<assert_contents>\n<has_text text=\"chr7\" />\n<not_has_text text=\"chr8\" />\n<has_text_matching expression=\"1274\\d+53\" />\n<has_line_matching expression=\".*\\s+127489808\\s+127494553\" />\n<!-- &#009; is XML escape code for tab -->\n<has_line line=\"chr7&#009;127471195&#009;127489808\" />\n<has_n_columns n=\"3\" />\n</assert_contents>\n</output>\n```\nThe following demonstrates a wide variety of XML assertion statements.\n```xml\n<output name=\"out_file1\">\n<assert_contents>\n<is_valid_xml />\n<has_element_with_path path=\"BlastOutput_param/Parameters/Parameters_matrix\" />\n<has_n_elements_with_path n=\"9\" path=\"BlastOutput_iterations/Iteration/Iteration_hits/Hit/Hit_num\" />\n<element_text_matches path=\"BlastOutput_version\" expression=\"BLASTP\\s+2\\.2.*\" />\n<element_text_is path=\"BlastOutput_program\" text=\"blastp\" />\n<element_text path=\"BlastOutput_iterations/Iteration/Iteration_hits/Hit/Hit_def\">\n<not_has_text text=\"EDK72998.1\" />\n<has_text_matching expression=\"ABK[\\d\\.]+\" />\n</element_text>\n</assert_contents>\n</output>\n```\nThe following demonstrates verifying XML content with XPath-like expressions.\n```xml\n<output name=\"out_file1\">\n<assert_contents>\n<attribute_is path=\"outerElement/innerElement1\" attribute=\"foo\" text=\"bar\" />\n<attribute_matches path=\"outerElement/innerElement2\" attribute=\"foo2\" expression=\"bar\\d+\" />\n</assert_contents>\n</output>\n```", "title": "Asserts" }, @@ -2278,14 +4017,18 @@ "sort": { "anyOf": [ { - "$ref": "#/$defs/PermissiveBoolean" + "type": "boolean" + }, + { + "$ref": "#/$defs/PermissiveBooleanValue" }, { "type": "null" } ], "default": null, - "description": "Applies only if ``compare`` is ``diff``, ``re_match`` or ``re_match_multiline``. This flag causes the lines of the history data set to be sorted before the comparison. In case of ``diff`` and ``re_match`` also the local file is sorted. This could be\nuseful for non-deterministic output." + "description": "Applies only if ``compare`` is ``diff``, ``re_match`` or ``re_match_multiline``. This flag causes the lines of the history data set to be sorted before the comparison. In case of ``diff`` and ``re_match`` also the local file is sorted. This could be\nuseful for non-deterministic output.", + "title": "Sort" }, "value": { "anyOf": [ @@ -2353,14 +4096,18 @@ "decompress": { "anyOf": [ { - "$ref": "#/$defs/PermissiveBoolean" + "type": "boolean" + }, + { + "$ref": "#/$defs/PermissiveBooleanValue" }, { "type": "null" } ], "default": null, - "description": "If this attribute is true then try to decompress files if needed. This applies to\ntest assertions expressed with ``assert_contents`` or ``compare`` set to anything\nbut ``sim_size``.\nThis flag is useful for testing compressed outputs that are non-deterministic\ndespite having deterministic decompressed contents. By default, only files compressed\nwith bz2, gzip and zip will be automatically decompressed.\nNote, for specifying assertions for compressed as well as decompressed output\nthe corresponding output tag can be specified multiple times.\nThis is available in Galaxy since release 17.05 and was introduced in [pull request #3550](https://github.com/galaxyproject/galaxy/pull/3550)." + "description": "If this attribute is true then try to decompress files if needed. This applies to\ntest assertions expressed with ``assert_contents`` or ``compare`` set to anything\nbut ``sim_size``.\nThis flag is useful for testing compressed outputs that are non-deterministic\ndespite having deterministic decompressed contents. By default, only files compressed\nwith bz2, gzip and zip will be automatically decompressed.\nNote, for specifying assertions for compressed as well as decompressed output\nthe corresponding output tag can be specified multiple times.\nThis is available in Galaxy since release 17.05 and was introduced in [pull request #3550](https://github.com/galaxyproject/galaxy/pull/3550).", + "title": "Decompress" }, "delta": { "default": 10000, @@ -2408,63 +4155,13 @@ "title": "Location" } }, - "title": "TestOutput", - "type": "object" - }, - "TestOutputCollection": { - "additionalProperties": false, - "properties": { - "elements": { - "items": { - "$ref": "#/$defs/TestOutput" - }, - "title": "Elements", - "type": "array" - }, - "name": { - "description": "This value is the same as the value of the ``name`` attribute of the\n``<collection>`` tag set contained within the tool's ``<outputs>`` tag set.", - "title": "Name", - "type": "string" - }, - "type_value": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Expected collection type (``list`` or ``paired``), nested collections are specified as colon separated list (the most common types are ``list``, ``paired``, ``list:paired``, or ``list:list``).", - "title": "Type Value" - }, - "count": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Number of elements in output collection.", - "title": "Count" - } - }, - "required": ["name"], - "title": "TestOutputCollection", + "required": ["elements"], + "title": "TestOutputElement", "type": "object" }, - "TestOutputCompareType": { - "description": "Type of comparison to use when comparing test generated output files to\nexpected output files.\n\nCurrently valid value are\n``diff`` (the default), ``re_match``, ``re_match_multiline``,\nand ``contains``. In addition there is ``sim_size`` which is discouraged in favour of a ``has_size`` assertion.", - "enum": ["diff", "re_match", "sim_size", "re_match_multiline", "contains"], - "title": "TestOutputCompareType", - "type": "string" - }, "TestOutputMetadata": { "additionalProperties": false, + "description": "This directive specifies a test for an output's metadata as an expected key-\nvalue pair.\n\n### Example\nThe functional test tool\n[tool_provided_metadata_1.xml](https://github.com/galaxyproject/galaxy/blob/dev/test/functional/tools/tool_provided_metadata_1.xml)\nprovides a demonstration of using this tag.\n```xml\n<test>\n<param name=\"input1\" value=\"simple_line.txt\" />\n<output name=\"out1\" file=\"simple_line.txt\" ftype=\"txt\">\n<metadata name=\"name\" value=\"my dynamic name\" />\n<metadata name=\"info\" value=\"my dynamic info\" />\n<metadata name=\"dbkey\" value=\"cust1\" />\n</output>\n</test>\n```", "properties": { "name": { "description": "Name of the metadata element to check.", From 2260d81f82db0013b43df138bd0b34f2da32df58 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Mon, 25 Dec 2023 20:49:40 +0100 Subject: [PATCH 25/98] Vendorize JSON schema handling Unfortunately, the existing yaml-language-server from RedHat has some dependencies that conflict with the browser version of the extension. The code related to schema handling, with some simplifications and adaptations for our use case, has been included here. --- .../src/schema/adapter.ts | 1473 +++++++++++++++++ .../src/schema/jsonSchema.ts | 91 + .../src/schema/provider.ts | 129 +- 3 files changed, 1689 insertions(+), 4 deletions(-) create mode 100644 server/packages/workflow-tests-language-service/src/schema/adapter.ts create mode 100644 server/packages/workflow-tests-language-service/src/schema/jsonSchema.ts diff --git a/server/packages/workflow-tests-language-service/src/schema/adapter.ts b/server/packages/workflow-tests-language-service/src/schema/adapter.ts new file mode 100644 index 0000000..4ebc423 --- /dev/null +++ b/server/packages/workflow-tests-language-service/src/schema/adapter.ts @@ -0,0 +1,1473 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* + * Part of this code is based on https://github.com/redhat-developer/yaml-language-server/ with some + * modifications to fit our needs. + */ + +import { + ASTNode, + ArrayASTNode, + NumberASTNode, + ObjectASTNode, + PropertyASTNode, + StringASTNode, +} from "@gxwf/server-common/src/ast/types"; +import { Diagnostic, DiagnosticSeverity, Range, TextDocument } from "@gxwf/server-common/src/languageTypes"; +import { URI } from "vscode-uri"; +import { JSONSchema, JSONSchemaRef } from "./jsonSchema"; + +const YAML_SCHEMA_PREFIX = "yaml-schema: "; +export const YAML_SOURCE = "YAML"; + +/** + * Error codes used by diagnostics + */ +export enum ErrorCode { + Undefined = 0, + EnumValueMismatch = 1, + Deprecated = 2, + UnexpectedEndOfComment = 257, + UnexpectedEndOfString = 258, + UnexpectedEndOfNumber = 259, + InvalidUnicode = 260, + InvalidEscapeCharacter = 261, + InvalidCharacter = 262, + PropertyExpected = 513, + CommaExpected = 514, + ColonExpected = 515, + ValueExpected = 516, + CommaOrCloseBacketExpected = 517, + CommaOrCloseBraceExpected = 518, + TrailingComma = 519, + DuplicateKey = 520, + CommentNotPermitted = 521, + PropertyKeysMustBeDoublequoted = 528, + SchemaResolveError = 768, + SchemaUnsupportedFeature = 769, +} + +const propertyNotAllowedMessage = (property: string): string => `Property ${property} is not allowed.`; + +export const formats = { + "color-hex": { + errorMessage: "Invalid color format. Use #RGB, #RGBA, #RRGGBB or #RRGGBBAA.", + pattern: /^#([0-9A-Fa-f]{3,4}|([0-9A-Fa-f]{2}){3,4})$/, + }, + "date-time": { + errorMessage: "String is not a RFC3339 date-time.", + pattern: + /^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(Z|(\+|-)([01][0-9]|2[0-3]):([0-5][0-9]))$/i, + }, + date: { + errorMessage: "String is not a RFC3339 date.", + pattern: /^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/i, + }, + time: { + errorMessage: "String is not a RFC3339 time.", + pattern: /^([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(Z|(\+|-)([01][0-9]|2[0-3]):([0-5][0-9]))$/i, + }, + email: { + errorMessage: "String is not an e-mail address.", + pattern: + /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, + }, + ipv4: { + errorMessage: "String does not match IPv4 format.", + pattern: /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/, + }, + ipv6: { + errorMessage: "String does not match IPv6 format.", + pattern: /^([0-9a-f]|:){1,4}(:([0-9a-f]{0,4})*){1,7}$/i, + }, +}; + +export interface IApplicableSchema { + node: ASTNode; + inverted?: boolean; + schema: JSONSchema; +} + +export interface ISchemaCollector { + schemas: IApplicableSchema[]; + add(schema: IApplicableSchema): void; + merge(other: ISchemaCollector): void; + include(node: ASTNode): boolean; + newSub(): ISchemaCollector; +} + +class SchemaCollector implements ISchemaCollector { + schemas: IApplicableSchema[] = []; + constructor( + private focusOffset = -1, + private exclude: ASTNode | null = null + ) {} + add(schema: IApplicableSchema): void { + this.schemas.push(schema); + } + merge(other: ISchemaCollector): void { + this.schemas.push(...other.schemas); + } + include(node: ASTNode): boolean { + return (this.focusOffset === -1 || contains(node, this.focusOffset)) && node !== this.exclude; + } + newSub(): ISchemaCollector { + return new SchemaCollector(-1, this.exclude); + } +} + +class NoOpSchemaCollector implements ISchemaCollector { + private constructor() { + // ignore + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + get schemas(): any[] { + return []; + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + add(schema: IApplicableSchema): void { + // ignore + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + merge(other: ISchemaCollector): void { + // ignore + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + include(node: ASTNode): boolean { + return true; + } + newSub(): ISchemaCollector { + return this; + } + + static instance = new NoOpSchemaCollector(); +} + +export function contains(node: ASTNode, offset: number, includeRightBound = false): boolean { + return ( + (offset >= node.offset && offset <= node.offset + node.length) || + (includeRightBound && offset === node.offset + node.length) + ); +} + +export class SchemaAdapter { + constructor( + private textDocument: TextDocument, + private root: ASTNode, + private disableAdditionalProperties = false + ) {} + + public validate( + schema: JSONSchema | undefined, + severity: DiagnosticSeverity = DiagnosticSeverity.Warning + ): Diagnostic[] | undefined { + if (this.root && schema) { + const validationResult = new ValidationResult(); + validate(this.root, schema, schema, validationResult, NoOpSchemaCollector.instance, { + disableAdditionalProperties: this.disableAdditionalProperties, + uri: this.textDocument.uri, + callFromAutoComplete: false, + }); + return validationResult.problems.map((p) => { + const range = Range.create( + this.textDocument.positionAt(p.location.offset), + this.textDocument.positionAt(p.location.offset + p.location.length) + ); + return Diagnostic.create(range, p.message, p.severity ?? severity, p.code); + }); + } + return undefined; + } + + public getMatchingSchemas( + schema: JSONSchema, + focusOffset = -1, + exclude: ASTNode | null = null, + didCallFromAutoComplete?: boolean + ): IApplicableSchema[] { + const matchingSchemas = new SchemaCollector(focusOffset, exclude); + if (this.root && schema) { + validate(this.root, schema, schema, new ValidationResult(), matchingSchemas, { + disableAdditionalProperties: this.disableAdditionalProperties, + uri: this.textDocument.uri, + callFromAutoComplete: didCallFromAutoComplete, + }); + } + return matchingSchemas.schemas; + } +} + +export enum ProblemType { + missingRequiredPropWarning = "missingRequiredPropWarning", + typeMismatchWarning = "typeMismatchWarning", + constWarning = "constWarning", +} + +export const ProblemTypeMessages: Record = { + [ProblemType.missingRequiredPropWarning]: 'Missing property "{0}".', + [ProblemType.typeMismatchWarning]: 'Incorrect type. Expected "{0}".', + [ProblemType.constWarning]: "Value must be {0}.", +}; + +function getWarningMessage(problemType?: ProblemType, args?: string[]): string { + if (!problemType) { + throw new Error("Unknown problem type while getting warning message"); + } + if (!args) { + throw new Error("No arguments while getting warning message"); + } + return ProblemTypeMessages[problemType].replace("{0}", args.join(" | ")); +} + +export interface IRange { + offset: number; + length: number; +} + +export interface IProblem { + location: IRange; + severity: DiagnosticSeverity; + code?: ErrorCode; + message: string; + source: string; + problemType?: ProblemType; + problemArgs?: string[]; + schemaUri: string[]; + data?: Record; +} + +export function isArrayEqual(fst?: Array, snd?: Array): boolean { + if (!snd || !fst) { + return false; + } + if (snd.length !== fst.length) { + return false; + } + for (let index = fst.length - 1; index >= 0; index--) { + if (fst[index] !== snd[index]) { + return false; + } + } + return true; +} + +export class ValidationResult { + public problems: IProblem[]; + + public propertiesMatches: number; + public propertiesValueMatches: number; + public primaryValueMatches: number; + public enumValueMatch: boolean; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public enumValues: any[]; + + constructor() { + this.problems = []; + this.propertiesMatches = 0; + this.propertiesValueMatches = 0; + this.primaryValueMatches = 0; + this.enumValueMatch = false; + this.enumValues = []; + } + + public hasProblems(): boolean { + return !!this.problems.length; + } + + public mergeAll(validationResults: ValidationResult[]): void { + for (const validationResult of validationResults) { + this.merge(validationResult); + } + } + + public merge(validationResult: ValidationResult): void { + this.problems = this.problems.concat(validationResult.problems); + } + + public mergeEnumValues(validationResult: ValidationResult): void { + if (!this.enumValueMatch && !validationResult.enumValueMatch && this.enumValues && validationResult.enumValues) { + this.enumValues = this.enumValues.concat(validationResult.enumValues); + for (const error of this.problems) { + if (error.code === ErrorCode.EnumValueMismatch) { + error.message = `Value is not accepted. Valid values: ${[...new Set(this.enumValues)] + .map((v) => { + return JSON.stringify(v); + }) + .join(", ")}.`; + } + } + } + } + + /** + * Merge multiple warnings with same problemType together + * @param subValidationResult another possible result + */ + public mergeWarningGeneric(subValidationResult: ValidationResult, problemTypesToMerge: ProblemType[]): void { + if (this.problems?.length) { + for (const problemType of problemTypesToMerge) { + const bestResults = this.problems.filter((p) => p.problemType === problemType); + for (const bestResult of bestResults) { + const mergingResult = subValidationResult.problems?.find( + (p) => + p.problemType === problemType && + bestResult.location.offset === p.location.offset && + (problemType !== ProblemType.missingRequiredPropWarning || + isArrayEqual(p.problemArgs, bestResult.problemArgs)) // missingProp is merged only with same problemArg + ); + if (mergingResult) { + if (mergingResult.problemArgs?.length) { + mergingResult.problemArgs + .filter((p) => !bestResult.problemArgs?.includes(p)) + .forEach((p) => bestResult.problemArgs?.push(p)); + bestResult.message = getWarningMessage(bestResult.problemType, bestResult.problemArgs); + } + this.mergeSources(mergingResult, bestResult); + } + } + } + } + } + + public mergePropertyMatch(propertyValidationResult: ValidationResult): void { + this.merge(propertyValidationResult); + this.propertiesMatches++; + if ( + propertyValidationResult.enumValueMatch || + (!propertyValidationResult.hasProblems() && propertyValidationResult.propertiesMatches) + ) { + this.propertiesValueMatches++; + } + if (propertyValidationResult.enumValueMatch && propertyValidationResult.enumValues) { + this.primaryValueMatches++; + } + } + + private mergeSources(mergingResult: IProblem, bestResult: IProblem): void { + const mergingSource = mergingResult.source.replace(YAML_SCHEMA_PREFIX, ""); + if (!bestResult.source.includes(mergingSource)) { + bestResult.source = bestResult.source + " | " + mergingSource; + } + if (!bestResult.schemaUri.includes(mergingResult.schemaUri[0])) { + bestResult.schemaUri = bestResult.schemaUri.concat(mergingResult.schemaUri); + } + } + + public compareGeneric(other: ValidationResult): number { + const hasProblems = this.hasProblems(); + if (hasProblems !== other.hasProblems()) { + return hasProblems ? -1 : 1; + } + if (this.enumValueMatch !== other.enumValueMatch) { + return other.enumValueMatch ? -1 : 1; + } + if (this.propertiesValueMatches !== other.propertiesValueMatches) { + return this.propertiesValueMatches - other.propertiesValueMatches; + } + if (this.primaryValueMatches !== other.primaryValueMatches) { + return this.primaryValueMatches - other.primaryValueMatches; + } + return this.propertiesMatches - other.propertiesMatches; + } + + public compareKubernetes(other: ValidationResult): number { + const hasProblems = this.hasProblems(); + if (this.propertiesMatches !== other.propertiesMatches) { + return this.propertiesMatches - other.propertiesMatches; + } + if (this.enumValueMatch !== other.enumValueMatch) { + return other.enumValueMatch ? -1 : 1; + } + if (this.primaryValueMatches !== other.primaryValueMatches) { + return this.primaryValueMatches - other.primaryValueMatches; + } + if (this.propertiesValueMatches !== other.propertiesValueMatches) { + return this.propertiesValueMatches - other.propertiesValueMatches; + } + if (hasProblems !== other.hasProblems()) { + return hasProblems ? -1 : 1; + } + return this.propertiesMatches - other.propertiesMatches; + } +} + +interface Options { + disableAdditionalProperties: boolean; + uri: string; + callFromAutoComplete?: boolean; +} + +export function asSchema(schema?: JSONSchemaRef): JSONSchema | undefined { + if (schema === undefined) { + return undefined; + } + + if (isBoolean(schema)) { + return schema ? {} : { not: {} }; + } + + if (typeof schema !== "object") { + // we need to report this case as JSONSchemaRef MUST be an Object or Boolean + console.warn(`Wrong schema: ${JSON.stringify(schema)}, it MUST be an Object or Boolean`); + schema = { + type: schema, + }; + } + if (schema.$ref) { + console.log(`DEF ${schema.$ref}`); + } + return schema; +} + +function getSchemaSource(schema: JSONSchema, originalSchema: JSONSchema): string { + let label: string | undefined = undefined; + if (schema) { + if (schema.title) { + label = schema.title; + return `${YAML_SCHEMA_PREFIX}${label}`; + } + } + + return YAML_SOURCE; +} + +function getSchemaUri(schema: JSONSchema, originalSchema: JSONSchema): string[] { + const uriString = schema.url ?? originalSchema.url; + return uriString ? [uriString] : []; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function getNodeValue(node: ASTNode): any { + switch (node.type) { + case "array": + return node.children.map(getNodeValue); + case "object": { + const obj = Object.create(null); + for (let _i = 0, _a = node.children; _i < _a.length; _i++) { + const prop = _a[_i]; + const valueNode = prop.children && prop.children[1]; + if (valueNode) { + obj[prop.children[0].value as string] = getNodeValue(valueNode); + } + } + return obj; + } + case "null": + case "string": + case "number": + case "boolean": + return node.value; + default: + return undefined; + } +} + +function validate( + node: ASTNode, + schema: JSONSchema | undefined, + originalSchema: JSONSchema, + validationResult: ValidationResult, + matchingSchemas: ISchemaCollector, + options: Options + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): any { + const { callFromAutoComplete } = options; + if (!node) { + return; + } + + // schema should be an Object + if (typeof schema !== "object") { + return; + } + + if (!schema.url) { + schema.url = originalSchema.url; + } + + if (schema.$ref) { + const refs = originalSchema.definitions; + if (refs) { + const key = schema.$ref.replace("#/$defs/", ""); + const definition = refs[key]; + schema = definition ?? schema; + schema.definitions = refs; + } + } + + // schema.closestTitle = schema.title || originalSchema.closestTitle; + + switch (node.type) { + case "object": + _validateObjectNode(node, schema, validationResult, matchingSchemas); + break; + case "array": + _validateArrayNode(node, schema, validationResult, matchingSchemas); + break; + case "string": + _validateStringNode(node, schema, validationResult); + break; + case "number": + _validateNumberNode(node, schema, validationResult); + break; + case "property": + return validate(node.valueNode!, schema, schema, validationResult, matchingSchemas, options); + } + _validateNode(); + + matchingSchemas.add({ node: node, schema: schema }); + + function _validateNode(): void { + if (schema === undefined) { + return; + } + + function matchesType(type: string): boolean { + return node.type === type || (type === "integer" && node.type === "number" && node.isInteger); + } + + if (Array.isArray(schema.type)) { + if (!schema.type.some(matchesType)) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: schema.errorMessage || `Incorrect type. Expected one of ${(schema.type).join(", ")}.`, + source: getSchemaSource(schema, originalSchema), + schemaUri: getSchemaUri(schema, originalSchema), + problemType: ProblemType.typeMismatchWarning, + problemArgs: [(schema.type).join(", ")], + }); + } + } else if (schema.type) { + if (!matchesType(schema.type)) { + //get more specific name than just object + const schemaType = schema.type === "object" ? schema.title ?? "" : schema.type; + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: schema.errorMessage || getWarningMessage(ProblemType.typeMismatchWarning, [schemaType]), + source: getSchemaSource(schema, originalSchema), + schemaUri: getSchemaUri(schema, originalSchema), + problemType: ProblemType.typeMismatchWarning, + problemArgs: [schemaType], + }); + } + } + if (Array.isArray(schema.allOf)) { + for (const subSchemaRef of schema.allOf) { + validate(node, asSchema(subSchemaRef), schema, validationResult, matchingSchemas, options); + } + } + // const notSchema = asSchema({ not: {} }); + // if (notSchema) { + // const subValidationResult = new ValidationResult(); + // const subMatchingSchemas = matchingSchemas.newSub(); + // validate(node, notSchema, schema, subValidationResult, subMatchingSchemas, options); + // if (!subValidationResult.hasProblems()) { + // validationResult.problems.push({ + // location: { offset: node.offset, length: node.length }, + // severity: DiagnosticSeverity.Warning, + // message: localize("notSchemaWarning", "Matches a schema that is not allowed."), + // source: getSchemaSource(schema, originalSchema), + // schemaUri: getSchemaUri(schema, originalSchema), + // }); + // } + // for (const ms of subMatchingSchemas.schemas) { + // ms.inverted = !ms.inverted; + // matchingSchemas.add(ms); + // } + } + + const testAlternatives = (alternatives: JSONSchemaRef[], maxOneMatch: boolean): number => { + const matches = []; + const subMatches = []; + const noPropertyMatches = []; + // remember the best match that is used for error messages + let bestMatch: { + schema: JSONSchema; + validationResult: ValidationResult; + matchingSchemas: ISchemaCollector; + } | null = null; + for (const subSchemaRef of alternatives) { + const subSchema = { ...asSchema(subSchemaRef) }; + const subValidationResult = new ValidationResult(); + const subMatchingSchemas = matchingSchemas.newSub(); + validate(node, subSchema, schema!, subValidationResult, subMatchingSchemas, options); + if (!subValidationResult.hasProblems() || callFromAutoComplete) { + matches.push(subSchema); + subMatches.push(subSchema); + if (subValidationResult.propertiesMatches === 0) { + noPropertyMatches.push(subSchema); + } + if (subSchema.format) { + subMatches.pop(); + } + } + if (!bestMatch) { + bestMatch = { + schema: subSchema, + validationResult: subValidationResult, + matchingSchemas: subMatchingSchemas, + }; + } else { + bestMatch = genericComparison(node, maxOneMatch, subValidationResult, bestMatch, subSchema, subMatchingSchemas); + } + } + + if (subMatches.length > 1 && (subMatches.length > 1 || noPropertyMatches.length === 0) && maxOneMatch) { + validationResult.problems.push({ + location: { offset: node.offset, length: 1 }, + severity: DiagnosticSeverity.Warning, + message: "Matches multiple schemas when only one must validate.", + source: getSchemaSource(schema!, originalSchema), + schemaUri: getSchemaUri(schema!, originalSchema), + problemArgs: [subMatches.map((s) => getSchemaSource(s, originalSchema)).join(", ")], + problemType: ProblemType.typeMismatchWarning, + }); + } + if (bestMatch !== null) { + validationResult.merge(bestMatch.validationResult); + validationResult.propertiesMatches += bestMatch.validationResult.propertiesMatches; + validationResult.propertiesValueMatches += bestMatch.validationResult.propertiesValueMatches; + validationResult.enumValueMatch = validationResult.enumValueMatch || bestMatch.validationResult.enumValueMatch; + if (bestMatch.validationResult.enumValues?.length) { + validationResult.enumValues = (validationResult.enumValues || []).concat(bestMatch.validationResult.enumValues); + } + matchingSchemas.merge(bestMatch.matchingSchemas); + } + return matches.length; + }; + if (Array.isArray(schema.anyOf)) { + testAlternatives(schema.anyOf, false); + } + if (Array.isArray(schema.oneOf)) { + testAlternatives(schema.oneOf, true); + } + + const testBranch = (schema: JSONSchemaRef, originalSchema: JSONSchema): void => { + const subValidationResult = new ValidationResult(); + const subMatchingSchemas = matchingSchemas.newSub(); + + validate(node, asSchema(schema), originalSchema, subValidationResult, subMatchingSchemas, options); + + validationResult.merge(subValidationResult); + validationResult.propertiesMatches += subValidationResult.propertiesMatches; + validationResult.propertiesValueMatches += subValidationResult.propertiesValueMatches; + matchingSchemas.merge(subMatchingSchemas); + }; + + const testCondition = ( + ifSchema: JSONSchemaRef, + originalSchema: JSONSchema, + thenSchema?: JSONSchemaRef, + elseSchema?: JSONSchemaRef + ): void => { + const subSchema = asSchema(ifSchema); + if (subSchema === undefined) { + return; + } + const subValidationResult = new ValidationResult(); + const subMatchingSchemas = matchingSchemas.newSub(); + + validate(node, subSchema, originalSchema, subValidationResult, subMatchingSchemas, options); + matchingSchemas.merge(subMatchingSchemas); + + // const { filePatternAssociation } = subSchema; + // if (filePatternAssociation) { + // const association = new FilePatternAssociation(filePatternAssociation); + // if (!association.matchesPattern(options.uri)) { + // subValidationResult.problems.push({ + // location: { offset: node.offset, length: node.length }, + // severity: DiagnosticSeverity.Warning, + // message: `filePatternAssociation '${filePatternAssociation}' does not match with doc uri '${options.uri}'.`, + // source: getSchemaSource(schema!, originalSchema), + // schemaUri: getSchemaUri(schema!, originalSchema), + // }); + // // don't want to expose the error up to code-completion results + // // validationResult.merge(subValidationResult); + // } + // } + + if (!subValidationResult.hasProblems()) { + if (thenSchema) { + testBranch(thenSchema, originalSchema); + } + } else if (elseSchema) { + testBranch(elseSchema, originalSchema); + } + }; + + // const ifSchema = asSchema(schema.if); + // if (ifSchema) { + // testCondition(ifSchema, schema, asSchema(schema.then), asSchema(schema.else)); + // } + + if (Array.isArray(schema.enum)) { + const val = getNodeValue(node); + let enumValueMatch = false; + for (const e of schema.enum) { + if (equals(val, e) || (callFromAutoComplete && isString(val) && isString(e) && val && e.startsWith(val))) { + enumValueMatch = true; + break; + } + } + validationResult.enumValues = schema.enum; + validationResult.enumValueMatch = enumValueMatch; + if (!enumValueMatch) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + code: ErrorCode.EnumValueMismatch, + message: + schema.errorMessage || + `Value is not accepted. Valid values: ${schema.enum + .map((v) => { + return JSON.stringify(v); + }) + .join(", ")}.`, + source: getSchemaSource(schema, originalSchema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } + } + + if (isDefined(schema.const)) { + const val = getNodeValue(node); + if ( + !equals(val, schema.const) && + !(callFromAutoComplete && isString(val) && isString(schema.const) && schema.const.startsWith(val)) + ) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + code: ErrorCode.EnumValueMismatch, + problemType: ProblemType.constWarning, + message: schema.errorMessage || getWarningMessage(ProblemType.constWarning, [JSON.stringify(schema.const)]), + source: getSchemaSource(schema, originalSchema), + schemaUri: getSchemaUri(schema, originalSchema), + problemArgs: [JSON.stringify(schema.const)], + }); + validationResult.enumValueMatch = false; + } else { + validationResult.enumValueMatch = true; + } + validationResult.enumValues = [schema.const]; + } + + if (schema.deprecationMessage && node.parent) { + validationResult.problems.push({ + location: { offset: node.parent.offset, length: node.parent.length }, + severity: DiagnosticSeverity.Warning, + message: schema.deprecationMessage, + source: getSchemaSource(schema, originalSchema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } + + function _validateNumberNode(node: NumberASTNode, schema: JSONSchema, validationResult: ValidationResult): void { + const val = node.value; + + if (isNumber(schema.multipleOf)) { + if (val % schema.multipleOf !== 0) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: `Value is not divisible by ${schema.multipleOf}.`, + source: getSchemaSource(schema, originalSchema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } + } + function getExclusiveLimit(limit: number | undefined, exclusive: boolean | number | undefined): number | undefined { + if (isNumber(exclusive)) { + return exclusive; + } + if (isBoolean(exclusive) && exclusive) { + return limit; + } + return undefined; + } + function getLimit(limit: number | undefined, exclusive: boolean | number | undefined): number | undefined { + if (!isBoolean(exclusive) || !exclusive) { + return limit; + } + return undefined; + } + const exclusiveMinimum = getExclusiveLimit(schema.minimum, schema.exclusiveMinimum); + if (isNumber(exclusiveMinimum) && val <= exclusiveMinimum) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: `Value is below the exclusive minimum of ${exclusiveMinimum}.`, + source: getSchemaSource(schema, originalSchema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } + const exclusiveMaximum = getExclusiveLimit(schema.maximum, schema.exclusiveMaximum); + if (isNumber(exclusiveMaximum) && val >= exclusiveMaximum) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: `Value is above the exclusive maximum of ${exclusiveMaximum}.`, + source: getSchemaSource(schema, originalSchema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } + const minimum = getLimit(schema.minimum, schema.exclusiveMinimum); + if (isNumber(minimum) && val < minimum) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: `Value is below the minimum of ${minimum}.`, + source: getSchemaSource(schema, originalSchema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } + const maximum = getLimit(schema.maximum, schema.exclusiveMaximum); + if (isNumber(maximum) && val > maximum) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: `Value is below the maximum of ${maximum}.`, + source: getSchemaSource(schema, originalSchema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } + } + + function _validateStringNode(node: StringASTNode, schema: JSONSchema, validationResult: ValidationResult): void { + if (isNumber(schema.minLength) && node.value.length < schema.minLength) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: `String is shorter than the minimum length of ${schema.minLength}.`, + source: getSchemaSource(schema, originalSchema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } + + if (isNumber(schema.maxLength) && node.value.length > schema.maxLength) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: `String is longer than the maximum length of ${schema.maxLength}.`, + source: getSchemaSource(schema, originalSchema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } + + if (isString(schema.pattern)) { + const regex = safeCreateUnicodeRegExp(schema.pattern); + if (!regex.test(node.value)) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: + schema.patternErrorMessage || + schema.errorMessage || + `String does not match the pattern of "${schema.pattern}".`, + source: getSchemaSource(schema, originalSchema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } + } + + if (schema.format) { + switch (schema.format) { + case "uri": + case "uri-reference": + { + let errorMessage; + if (!node.value) { + errorMessage = "URI expected."; + } else { + try { + const uri = URI.parse(node.value); + if (!uri.scheme && schema.format === "uri") { + errorMessage = "URI with a scheme is expected."; + } + } catch (e) { + errorMessage = (e as Error).message; + } + } + if (errorMessage) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: schema.patternErrorMessage || schema.errorMessage || `String is not a URI: ${errorMessage}`, + source: getSchemaSource(schema, originalSchema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } + } + break; + case "color-hex": + case "date-time": + case "date": + case "time": + case "email": + case "ipv4": + case "ipv6": + { + const format = formats[schema.format]; + if (!node.value || !format.pattern.test(node.value)) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: schema.patternErrorMessage || schema.errorMessage || format.errorMessage, + source: getSchemaSource(schema, originalSchema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } + } + break; + default: + } + } + } + function _validateArrayNode( + node: ArrayASTNode, + schema: JSONSchema, + validationResult: ValidationResult, + matchingSchemas: ISchemaCollector + ): void { + if (Array.isArray(schema.items)) { + const subSchemas = schema.items; + for (let index = 0; index < subSchemas.length; index++) { + const subSchemaRef = subSchemas[index]; + const subSchema = asSchema(subSchemaRef); + const itemValidationResult = new ValidationResult(); + const item = node.items[index]; + if (item) { + validate(item, subSchema, schema, itemValidationResult, matchingSchemas, options); + validationResult.mergePropertyMatch(itemValidationResult); + validationResult.mergeEnumValues(itemValidationResult); + } else if (node.items.length >= subSchemas.length) { + validationResult.propertiesValueMatches++; + } + } + if (node.items.length > subSchemas.length) { + if (typeof schema.additionalItems === "object") { + for (let i = subSchemas.length; i < node.items.length; i++) { + const itemValidationResult = new ValidationResult(); + validate( + node.items[i], + // eslint-disable-next-line @typescript-eslint/no-explicit-any + schema.additionalItems, + schema, + itemValidationResult, + matchingSchemas, + options + ); + validationResult.mergePropertyMatch(itemValidationResult); + validationResult.mergeEnumValues(itemValidationResult); + } + } else if (schema.additionalItems === false) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: `Array has too many items according to schema. Expected ${subSchemas.length} or fewer.`, + source: getSchemaSource(schema, originalSchema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } + } + } else { + const itemSchema = asSchema(schema.items); + if (itemSchema) { + const itemValidationResult = new ValidationResult(); + node.items.forEach((item) => { + if (itemSchema.oneOf && itemSchema.oneOf.length === 1) { + const subSchemaRef = itemSchema.oneOf[0]; + const subSchema = { ...asSchema(subSchemaRef) }; + subSchema.title = schema.title; + // subSchema.closestTitle = schema.closestTitle; + validate(item, subSchema, schema, itemValidationResult, matchingSchemas, options); + validationResult.mergePropertyMatch(itemValidationResult); + validationResult.mergeEnumValues(itemValidationResult); + } else { + validate(item, itemSchema, schema, itemValidationResult, matchingSchemas, options); + validationResult.mergePropertyMatch(itemValidationResult); + validationResult.mergeEnumValues(itemValidationResult); + } + }); + } + } + + const containsSchema = asSchema(schema.contains); + if (containsSchema) { + const doesContain = node.items.some((item) => { + const itemValidationResult = new ValidationResult(); + validate(item, containsSchema, schema, itemValidationResult, NoOpSchemaCollector.instance, options); + return !itemValidationResult.hasProblems(); + }); + + if (!doesContain) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: schema.errorMessage || "Array does not contain required item.", + source: getSchemaSource(schema, originalSchema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } + } + + if (isNumber(schema.minItems) && node.items.length < schema.minItems) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: `Array has too few items. Expected ${schema.minItems} or more.`, + source: getSchemaSource(schema, originalSchema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } + + if (isNumber(schema.maxItems) && node.items.length > schema.maxItems) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: `Array has too many items. Expected ${schema.maxItems} or fewer.`, + source: getSchemaSource(schema, originalSchema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } + + if (schema.uniqueItems === true) { + const values: unknown[] = getNodeValue(node); + const duplicates = values.some((value, index) => { + return index !== values.lastIndexOf(value); + }); + if (duplicates) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: "Array has duplicate items.", + source: getSchemaSource(schema, originalSchema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } + } + } + + function _validateObjectNode( + node: ObjectASTNode, + schema: JSONSchema, + validationResult: ValidationResult, + matchingSchemas: ISchemaCollector + ): void { + const seenKeys: { [key: string]: ASTNode } = Object.create(null); + const unprocessedProperties: string[] = []; + const unprocessedNodes: PropertyASTNode[] = [...node.properties]; + + while (unprocessedNodes.length > 0) { + const propertyNode = unprocessedNodes.pop(); + if (!propertyNode) { + continue; + } + const key = propertyNode.keyNode.value; + + //Replace the merge key with the actual values of what the node value points to in seen keys + if (key === "<<" && propertyNode.valueNode) { + switch (propertyNode.valueNode.type) { + case "object": { + unprocessedNodes.push(...propertyNode.valueNode["properties"]); + break; + } + case "array": { + propertyNode.valueNode["items"].forEach((sequenceNode) => { + if (sequenceNode && isIterable((sequenceNode as ObjectASTNode)["properties"])) { + unprocessedNodes.push(...(sequenceNode as ObjectASTNode)["properties"]); + } + }); + break; + } + default: { + break; + } + } + } else { + seenKeys[key] = propertyNode.valueNode as ASTNode; + unprocessedProperties.push(key); + } + } + + if (Array.isArray(schema.required)) { + for (const propertyName of schema.required) { + if (seenKeys[propertyName] === undefined) { + const keyNode = node.parent && node.parent.type === "property" && node.parent.keyNode; + const location = keyNode + ? { offset: keyNode.offset, length: keyNode.length } + : { offset: node.offset, length: 1 }; + validationResult.problems.push({ + location: location, + severity: DiagnosticSeverity.Warning, + message: getWarningMessage(ProblemType.missingRequiredPropWarning, [propertyName]), + source: getSchemaSource(schema, originalSchema), + schemaUri: getSchemaUri(schema, originalSchema), + problemArgs: [propertyName], + problemType: ProblemType.missingRequiredPropWarning, + }); + } + } + } + + const propertyProcessed = (prop: string): void => { + let index = unprocessedProperties.indexOf(prop); + while (index >= 0) { + unprocessedProperties.splice(index, 1); + index = unprocessedProperties.indexOf(prop); + } + }; + + if (schema.properties) { + for (const propertyName of Object.keys(schema.properties)) { + propertyProcessed(propertyName); + const propertySchema = schema.properties[propertyName]; + const child = seenKeys[propertyName]; + if (child) { + if (isBoolean(propertySchema)) { + if (!propertySchema) { + const propertyNode = child.parent; + validationResult.problems.push({ + location: { + offset: propertyNode.keyNode.offset, + length: propertyNode.keyNode.length, + }, + severity: DiagnosticSeverity.Warning, + message: schema.errorMessage || propertyNotAllowedMessage(propertyName), + source: getSchemaSource(schema, originalSchema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } else { + validationResult.propertiesMatches++; + validationResult.propertiesValueMatches++; + } + } else { + propertySchema.url = schema.url ?? originalSchema.url; + const propertyValidationResult = new ValidationResult(); + validate(child, propertySchema, schema, propertyValidationResult, matchingSchemas, options); + validationResult.mergePropertyMatch(propertyValidationResult); + validationResult.mergeEnumValues(propertyValidationResult); + } + } + } + } + + if (schema.patternProperties) { + for (const propertyPattern of Object.keys(schema.patternProperties)) { + const regex = safeCreateUnicodeRegExp(propertyPattern); + for (const propertyName of unprocessedProperties.slice(0)) { + if (regex.test(propertyName)) { + propertyProcessed(propertyName); + const child = seenKeys[propertyName]; + if (child) { + const propertySchema = schema.patternProperties[propertyPattern]; + if (isBoolean(propertySchema)) { + if (!propertySchema) { + const propertyNode = child.parent; + validationResult.problems.push({ + location: { + offset: propertyNode.keyNode.offset, + length: propertyNode.keyNode.length, + }, + severity: DiagnosticSeverity.Warning, + message: schema.errorMessage || propertyNotAllowedMessage(propertyName), + source: getSchemaSource(schema, originalSchema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } else { + validationResult.propertiesMatches++; + validationResult.propertiesValueMatches++; + } + } else { + const propertyValidationResult = new ValidationResult(); + validate(child, propertySchema, schema, propertyValidationResult, matchingSchemas, options); + validationResult.mergePropertyMatch(propertyValidationResult); + validationResult.mergeEnumValues(propertyValidationResult); + } + } + } + } + } + } + if (typeof schema.additionalProperties === "object") { + for (const propertyName of unprocessedProperties) { + const child = seenKeys[propertyName]; + if (child) { + const propertyValidationResult = new ValidationResult(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + validate(child, schema.additionalProperties, schema, propertyValidationResult, matchingSchemas, options); + validationResult.mergePropertyMatch(propertyValidationResult); + validationResult.mergeEnumValues(propertyValidationResult); + } + } + } else if ( + schema.additionalProperties === false || + (schema.type === "object" && + schema.additionalProperties === undefined && + options.disableAdditionalProperties === true) + ) { + if (unprocessedProperties.length > 0) { + const possibleProperties = + schema.properties && Object.keys(schema.properties).filter((prop) => !seenKeys[prop]); + + for (const propertyName of unprocessedProperties) { + const child = seenKeys[propertyName]; + if (child) { + let propertyNode = null; + if (child.type !== "property") { + propertyNode = child.parent as ASTNode; + if (propertyNode.type === "object") { + propertyNode = propertyNode.properties[0]; + } + } else { + propertyNode = child; + } + const problem: IProblem = { + location: { + offset: (propertyNode as PropertyASTNode).keyNode.offset, + length: (propertyNode as PropertyASTNode).keyNode.length, + }, + severity: DiagnosticSeverity.Warning, + message: schema.errorMessage || propertyNotAllowedMessage(propertyName), + source: getSchemaSource(schema, originalSchema), + schemaUri: getSchemaUri(schema, originalSchema), + }; + if (possibleProperties?.length) { + problem.data = { properties: possibleProperties }; + } + validationResult.problems.push(problem); + } + } + } + } + + if (isNumber(schema.maxProperties)) { + if (node.properties.length > schema.maxProperties) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: `Object has more properties than limit of ${schema.maxProperties}.`, + source: getSchemaSource(schema, originalSchema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } + } + + if (isNumber(schema.minProperties)) { + if (node.properties.length < schema.minProperties) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: `Object has fewer properties than the required number of ${schema.minProperties}`, + source: getSchemaSource(schema, originalSchema), + schemaUri: getSchemaUri(schema, originalSchema), + }); + } + } + } + + //genericComparison tries to find the best matching schema using a generic comparison + function genericComparison( + node: ASTNode, + maxOneMatch: boolean, + subValidationResult: ValidationResult, + bestMatch: { + schema: JSONSchema; + validationResult: ValidationResult; + matchingSchemas: ISchemaCollector; + }, + subSchema: JSONSchema, + subMatchingSchemas: ISchemaCollector + ): { + schema: JSONSchema; + validationResult: ValidationResult; + matchingSchemas: ISchemaCollector; + } { + if ( + !maxOneMatch && + !subValidationResult.hasProblems() && + (!bestMatch.validationResult.hasProblems() || callFromAutoComplete) + ) { + // no errors, both are equally good matches + bestMatch.matchingSchemas.merge(subMatchingSchemas); + bestMatch.validationResult.propertiesMatches += subValidationResult.propertiesMatches; + bestMatch.validationResult.propertiesValueMatches += subValidationResult.propertiesValueMatches; + } else { + const compareResult = subValidationResult.compareGeneric(bestMatch.validationResult); + if ( + compareResult > 0 || + (compareResult === 0 && + maxOneMatch && + bestMatch.schema.type === "object" && + node.type !== "null" && + node.type !== bestMatch.schema.type) + ) { + // our node is the best matching so far + bestMatch = { + schema: subSchema, + validationResult: subValidationResult, + matchingSchemas: subMatchingSchemas, + }; + } else if (compareResult === 0) { + // there's already a best matching but we are as good + bestMatch.matchingSchemas.merge(subMatchingSchemas); + bestMatch.validationResult.mergeEnumValues(subValidationResult); + bestMatch.validationResult.mergeWarningGeneric(subValidationResult, [ + ProblemType.missingRequiredPropWarning, + ProblemType.typeMismatchWarning, + ProblemType.constWarning, + ]); + } + } + return bestMatch; + } +} + +class FilePatternAssociation { + private schemas: string[]; + private patternRegExp: RegExp | null; + + constructor(pattern: string) { + try { + this.patternRegExp = new RegExp(convertSimple2RegExpPattern(pattern) + "$"); + } catch (e) { + // invalid pattern + this.patternRegExp = null; + } + this.schemas = []; + } + + public addSchema(id: string): void { + this.schemas.push(id); + } + + public matchesPattern(fileName: string): boolean { + return (this.patternRegExp && this.patternRegExp.test(fileName)) ?? false; + } + + public getSchemas(): string[] { + return this.schemas; + } +} + +export function equals(one: unknown, other: unknown): boolean { + if (one === other) { + return true; + } + if (one === null || one === undefined || other === null || other === undefined) { + return false; + } + if (typeof one !== typeof other) { + return false; + } + if (typeof one !== "object") { + return false; + } + if (Array.isArray(one) !== Array.isArray(other)) { + return false; + } + + let i: number, key: string; + + if (Array.isArray(one) && Array.isArray(other)) { + if (one.length !== other.length) { + return false; + } + for (i = 0; i < one.length; i++) { + if (!equals(one[i], other[i])) { + return false; + } + } + } else { + const oneKeys: string[] = []; + + for (key in one) { + oneKeys.push(key); + } + oneKeys.sort(); + const otherKeys: string[] = []; + for (const key in other) { + otherKeys.push(key); + } + otherKeys.sort(); + if (!equals(oneKeys, otherKeys)) { + return false; + } + for (let i = 0; i < oneKeys.length; i++) { + if (!equals(one[oneKeys[i] as keyof typeof one], other[oneKeys[i] as keyof typeof other])) { + return false; + } + } + } + return true; +} + +export function isNumber(val: unknown): val is number { + return typeof val === "number"; +} + +// eslint-disable-next-line @typescript-eslint/ban-types +export function isDefined(val: unknown): val is object | string | number | boolean { + return typeof val !== "undefined"; +} + +export function isBoolean(val: unknown): val is boolean { + return typeof val === "boolean"; +} + +export function isString(val: unknown): val is string { + return typeof val === "string"; +} + +/** + * Check that provided value is Iterable + * @param val the value to check + * @returns true if val is iterable, false otherwise + */ +export function isIterable(val: unknown): boolean { + return Symbol.iterator in Object(val); +} + +export function safeCreateUnicodeRegExp(pattern: string): RegExp { + // fall back to regular regexp if we cannot create Unicode one + try { + return new RegExp(pattern, "u"); + } catch (ignore) { + return new RegExp(pattern); + } +} + +export function convertSimple2RegExpPattern(pattern: string): string { + return pattern.replace(/[-\\{}+?|^$.,[\]()#\s]/g, "\\$&").replace(/[*]/g, ".*"); +} + +/** + * check all the schemas which is inside anyOf presented or not in matching schema. + * @param node node + * @param matchingSchemas all matching schema + * @param schema scheam which is having anyOf + * @returns true if all the schemas which inside anyOf presents in matching schema + */ +export function isAllSchemasMatched(node: ASTNode, matchingSchemas: IApplicableSchema[], schema: JSONSchema): boolean { + let count = 0; + for (const matchSchema of matchingSchemas) { + if (node === matchSchema.node && matchSchema.schema !== schema) { + schema.anyOf?.forEach((childSchema: JSONSchema) => { + if ( + matchSchema.schema.title === childSchema.title && + matchSchema.schema.description === childSchema.description && + matchSchema.schema.properties === childSchema.properties + ) { + count++; + } + }); + } + } + return count === schema.anyOf?.length; +} diff --git a/server/packages/workflow-tests-language-service/src/schema/jsonSchema.ts b/server/packages/workflow-tests-language-service/src/schema/jsonSchema.ts new file mode 100644 index 0000000..ca08873 --- /dev/null +++ b/server/packages/workflow-tests-language-service/src/schema/jsonSchema.ts @@ -0,0 +1,91 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* + * Part of this code is based on https://github.com/redhat-developer/yaml-language-server/ with some + * modifications to fit our needs. + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export interface JSONSchema { + id?: string; + $schema?: string; + type?: string | string[]; + title?: string; + default?: any; + definitions?: JSONSchemaMap; + description?: string; + properties?: JSONSchemaMap; + patternProperties?: JSONSchemaMap; + additionalProperties?: any; + minProperties?: number; + maxProperties?: number; + dependencies?: JSONSchemaMap | string[]; + items?: any; + minItems?: number; + maxItems?: number; + uniqueItems?: boolean; + additionalItems?: boolean; + pattern?: string; + minLength?: number; + maxLength?: number; + minimum?: number; + maximum?: number; + exclusiveMinimum?: boolean; + exclusiveMaximum?: boolean; + multipleOf?: number; + required?: string[]; + firstProperty?: string[]; + $ref?: string; + anyOf?: JSONSchema[]; + allOf?: JSONSchema[]; + oneOf?: JSONSchema[]; + not?: JSONSchema; + enum?: any[]; + format?: string; + errorMessage?: string; + patternErrorMessage?: string; + deprecationMessage?: string; + doNotSuggest?: boolean; + enumDescriptions?: string[]; + ignoreCase?: string; + aliases?: string[]; + document?: { [key: string]: string }; + $id?: string; + insertText?: string; + triggerSuggest?: boolean; + // Extra + url?: string; + const?: any; + contains?: JSONSchema; +} + +export interface JSONSchemaMap { + [name: string]: JSONSchema; +} + +export type JSONSchemaRef = JSONSchema | boolean; + +export class UnresolvedSchema { + schema: JSONSchema; + errors: string[]; + + constructor(schema: JSONSchema, errors: string[] = []) { + this.schema = schema; + this.errors = errors; + } +} + +export class ResolvedSchema { + schema: JSONSchema; + errors: string[]; + + constructor(schema: JSONSchema, errors: string[] = []) { + this.schema = schema; + this.errors = errors; + } +} diff --git a/server/packages/workflow-tests-language-service/src/schema/provider.ts b/server/packages/workflow-tests-language-service/src/schema/provider.ts index 74e0c85..7309480 100644 --- a/server/packages/workflow-tests-language-service/src/schema/provider.ts +++ b/server/packages/workflow-tests-language-service/src/schema/provider.ts @@ -1,14 +1,135 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* + * Part of this code is based on https://github.com/redhat-developer/yaml-language-server/ with some + * modifications to fit our needs. + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + import { injectable } from "inversify"; +import { JSONSchema, ResolvedSchema, UnresolvedSchema } from "./jsonSchema"; import WorkflowTestsSchema from "../../../../../workflow-languages/schemas/tests.schema.json"; -import { JSONSchema } from "vscode-json-languageservice"; export interface WorkflowTestsSchemaProvider { - getSchema(): JSONSchema; + getResolvedSchema(): ResolvedSchema; } @injectable() export class WorkflowTestsSchemaProviderImpl implements WorkflowTestsSchemaProvider { - public getSchema(): JSONSchema { - return WorkflowTestsSchema; + private readonly resolvedSchema: ResolvedSchema; + + constructor() { + this.resolvedSchema = this.resolveSchemaContent(new UnresolvedSchema(WorkflowTestsSchema, [])); + } + + public getResolvedSchema(): ResolvedSchema { + return this.resolvedSchema; + } + + private resolveSchemaContent(schemaToResolve: UnresolvedSchema): ResolvedSchema { + const resolveErrors: string[] = schemaToResolve.errors.slice(0); + const schema = schemaToResolve.schema; + + const findSection = (schema: JSONSchema, path: string): any => { + if (!path) { + return schema; + } + let current: any = schema; + if (path[0] === "/") { + path = path.substring(1); + } + path.split("/").some((part) => { + current = current[part]; + return !current; + }); + return current; + }; + + const resolveLink = (node: any, linkedSchema: JSONSchema, linkPath: string): void => { + const section = findSection(linkedSchema, linkPath); + if (section) { + for (const key in section) { + if (Object.prototype.hasOwnProperty.call(section, key) && !Object.prototype.hasOwnProperty.call(node, key)) { + node[key] = section[key]; + } + } + } else { + resolveErrors.push(`json.schema.invalidref: $ref '${linkPath}' in ${linkedSchema.id} can not be resolved.`); + } + delete node.$ref; + }; + + const resolveRefs = (node: JSONSchema, parentSchema: JSONSchema): void => { + if (!node) { + return; + } + + const toWalk: JSONSchema[] = [node]; + const seen: JSONSchema[] = []; + + const collectEntries = (...entries: JSONSchema[]): void => { + for (const entry of entries) { + if (typeof entry === "object") { + toWalk.push(entry); + } + } + }; + + const collectMapEntries = (...maps: JSONSchema[]): void => { + for (const map of maps) { + if (typeof map === "object") { + for (const key in map) { + if (Object.prototype.hasOwnProperty.call(map, key)) { + const entry = (map as any)[key]; + toWalk.push(entry); + } + } + } + } + }; + + const collectArrayEntries = (...arrays: JSONSchema[][]): void => { + for (const array of arrays) { + if (Array.isArray(array)) { + toWalk.push(...array); + } + } + }; + + while (toWalk.length) { + const next = toWalk.pop(); + if (!next) { + break; + } + if (seen.indexOf(next) >= 0) { + continue; + } + seen.push(next); + if (next.$ref) { + const segments = next.$ref.split("#", 2); + resolveLink(next, parentSchema, segments[1]); + } + collectEntries(next.items, next.additionalProperties, next.not as any); + collectMapEntries( + next.definitions as JSONSchema, + next.properties as JSONSchema, + next.patternProperties as JSONSchema, + next.dependencies as JSONSchema + ); + collectArrayEntries( + next.anyOf as JSONSchema[], + next.allOf as JSONSchema[], + next.oneOf as JSONSchema[], + next.items as JSONSchema[] + ); + } + }; + resolveRefs(schema, schema); + return new ResolvedSchema(schema, resolveErrors); } } From ab99441a7f0f3f8cf3806c915498d6fb48b72ab7 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Thu, 28 Dec 2023 17:34:16 +0100 Subject: [PATCH 26/98] Refactor schema adapter --- .../src/schema/adapter.ts | 242 ++++++------------ 1 file changed, 72 insertions(+), 170 deletions(-) diff --git a/server/packages/workflow-tests-language-service/src/schema/adapter.ts b/server/packages/workflow-tests-language-service/src/schema/adapter.ts index 4ebc423..42be7ec 100644 --- a/server/packages/workflow-tests-language-service/src/schema/adapter.ts +++ b/server/packages/workflow-tests-language-service/src/schema/adapter.ts @@ -17,7 +17,7 @@ import { PropertyASTNode, StringASTNode, } from "@gxwf/server-common/src/ast/types"; -import { Diagnostic, DiagnosticSeverity, Range, TextDocument } from "@gxwf/server-common/src/languageTypes"; +import { Diagnostic, DiagnosticSeverity, DocumentContext, Range } from "@gxwf/server-common/src/languageTypes"; import { URI } from "vscode-uri"; import { JSONSchema, JSONSchemaRef } from "./jsonSchema"; @@ -53,7 +53,7 @@ export enum ErrorCode { const propertyNotAllowedMessage = (property: string): string => `Property ${property} is not allowed.`; -export const formats = { +const formats = { "color-hex": { errorMessage: "Invalid color format. Use #RGB, #RGBA, #RRGGBB or #RRGGBBAA.", pattern: /^#([0-9A-Fa-f]{3,4}|([0-9A-Fa-f]{2}){3,4})$/, @@ -88,11 +88,10 @@ export const formats = { export interface IApplicableSchema { node: ASTNode; - inverted?: boolean; schema: JSONSchema; } -export interface ISchemaCollector { +interface ISchemaCollector { schemas: IApplicableSchema[]; add(schema: IApplicableSchema): void; merge(other: ISchemaCollector): void; @@ -154,28 +153,43 @@ export function contains(node: ASTNode, offset: number, includeRightBound = fals ); } -export class SchemaAdapter { - constructor( - private textDocument: TextDocument, - private root: ASTNode, - private disableAdditionalProperties = false - ) {} +export interface SchemaService { + validate( + documentContext: DocumentContext, + schema: JSONSchema | undefined, + severity?: DiagnosticSeverity, + disableAdditionalProperties?: boolean + ): Diagnostic[] | undefined; + getMatchingSchemas( + documentContext: DocumentContext, + schema: JSONSchema, + focusOffset?: number, + exclude?: ASTNode | null, + didCallFromAutoComplete?: boolean, + disableAdditionalProperties?: boolean + ): IApplicableSchema[]; +} + +export class SchemaServiceImpl implements SchemaService { public validate( + documentContext: DocumentContext, schema: JSONSchema | undefined, - severity: DiagnosticSeverity = DiagnosticSeverity.Warning + severity: DiagnosticSeverity = DiagnosticSeverity.Error, + disableAdditionalProperties = false ): Diagnostic[] | undefined { - if (this.root && schema) { + const root = documentContext.nodeManager.root!; + if (root && schema) { const validationResult = new ValidationResult(); - validate(this.root, schema, schema, validationResult, NoOpSchemaCollector.instance, { - disableAdditionalProperties: this.disableAdditionalProperties, - uri: this.textDocument.uri, + validate(root, schema, schema, validationResult, NoOpSchemaCollector.instance, { + disableAdditionalProperties, + uri: documentContext.textDocument.uri, callFromAutoComplete: false, }); return validationResult.problems.map((p) => { const range = Range.create( - this.textDocument.positionAt(p.location.offset), - this.textDocument.positionAt(p.location.offset + p.location.length) + documentContext.textDocument.positionAt(p.location.offset), + documentContext.textDocument.positionAt(p.location.offset + p.location.length) ); return Diagnostic.create(range, p.message, p.severity ?? severity, p.code); }); @@ -184,16 +198,19 @@ export class SchemaAdapter { } public getMatchingSchemas( + documentContext: DocumentContext, schema: JSONSchema, focusOffset = -1, exclude: ASTNode | null = null, - didCallFromAutoComplete?: boolean + didCallFromAutoComplete?: boolean, + disableAdditionalProperties = false ): IApplicableSchema[] { + const root = documentContext.nodeManager.root!; const matchingSchemas = new SchemaCollector(focusOffset, exclude); - if (this.root && schema) { - validate(this.root, schema, schema, new ValidationResult(), matchingSchemas, { - disableAdditionalProperties: this.disableAdditionalProperties, - uri: this.textDocument.uri, + if (root && schema) { + validate(root, schema, schema, new ValidationResult(), matchingSchemas, { + disableAdditionalProperties, + uri: documentContext.textDocument.uri, callFromAutoComplete: didCallFromAutoComplete, }); } @@ -201,13 +218,13 @@ export class SchemaAdapter { } } -export enum ProblemType { +enum ProblemType { missingRequiredPropWarning = "missingRequiredPropWarning", typeMismatchWarning = "typeMismatchWarning", constWarning = "constWarning", } -export const ProblemTypeMessages: Record = { +const ProblemTypeMessages: Record = { [ProblemType.missingRequiredPropWarning]: 'Missing property "{0}".', [ProblemType.typeMismatchWarning]: 'Incorrect type. Expected "{0}".', [ProblemType.constWarning]: "Value must be {0}.", @@ -223,12 +240,12 @@ function getWarningMessage(problemType?: ProblemType, args?: string[]): string { return ProblemTypeMessages[problemType].replace("{0}", args.join(" | ")); } -export interface IRange { +interface IRange { offset: number; length: number; } -export interface IProblem { +interface IProblem { location: IRange; severity: DiagnosticSeverity; code?: ErrorCode; @@ -423,7 +440,7 @@ export function asSchema(schema?: JSONSchemaRef): JSONSchema | undefined { return schema; } -function getSchemaSource(schema: JSONSchema, originalSchema: JSONSchema): string { +function getSchemaSource(schema: JSONSchema): string { let label: string | undefined = undefined; if (schema) { if (schema.title) { @@ -489,18 +506,6 @@ function validate( schema.url = originalSchema.url; } - if (schema.$ref) { - const refs = originalSchema.definitions; - if (refs) { - const key = schema.$ref.replace("#/$defs/", ""); - const definition = refs[key]; - schema = definition ?? schema; - schema.definitions = refs; - } - } - - // schema.closestTitle = schema.title || originalSchema.closestTitle; - switch (node.type) { case "object": _validateObjectNode(node, schema, validationResult, matchingSchemas); @@ -536,7 +541,7 @@ function validate( location: { offset: node.offset, length: node.length }, severity: DiagnosticSeverity.Warning, message: schema.errorMessage || `Incorrect type. Expected one of ${(schema.type).join(", ")}.`, - source: getSchemaSource(schema, originalSchema), + source: getSchemaSource(schema), schemaUri: getSchemaUri(schema, originalSchema), problemType: ProblemType.typeMismatchWarning, problemArgs: [(schema.type).join(", ")], @@ -550,7 +555,7 @@ function validate( location: { offset: node.offset, length: node.length }, severity: DiagnosticSeverity.Warning, message: schema.errorMessage || getWarningMessage(ProblemType.typeMismatchWarning, [schemaType]), - source: getSchemaSource(schema, originalSchema), + source: getSchemaSource(schema), schemaUri: getSchemaUri(schema, originalSchema), problemType: ProblemType.typeMismatchWarning, problemArgs: [schemaType], @@ -562,24 +567,6 @@ function validate( validate(node, asSchema(subSchemaRef), schema, validationResult, matchingSchemas, options); } } - // const notSchema = asSchema({ not: {} }); - // if (notSchema) { - // const subValidationResult = new ValidationResult(); - // const subMatchingSchemas = matchingSchemas.newSub(); - // validate(node, notSchema, schema, subValidationResult, subMatchingSchemas, options); - // if (!subValidationResult.hasProblems()) { - // validationResult.problems.push({ - // location: { offset: node.offset, length: node.length }, - // severity: DiagnosticSeverity.Warning, - // message: localize("notSchemaWarning", "Matches a schema that is not allowed."), - // source: getSchemaSource(schema, originalSchema), - // schemaUri: getSchemaUri(schema, originalSchema), - // }); - // } - // for (const ms of subMatchingSchemas.schemas) { - // ms.inverted = !ms.inverted; - // matchingSchemas.add(ms); - // } } const testAlternatives = (alternatives: JSONSchemaRef[], maxOneMatch: boolean): number => { @@ -623,9 +610,9 @@ function validate( location: { offset: node.offset, length: 1 }, severity: DiagnosticSeverity.Warning, message: "Matches multiple schemas when only one must validate.", - source: getSchemaSource(schema!, originalSchema), + source: getSchemaSource(schema!), schemaUri: getSchemaUri(schema!, originalSchema), - problemArgs: [subMatches.map((s) => getSchemaSource(s, originalSchema)).join(", ")], + problemArgs: [subMatches.map((s) => getSchemaSource(s)).join(", ")], problemType: ProblemType.typeMismatchWarning, }); } @@ -648,64 +635,6 @@ function validate( testAlternatives(schema.oneOf, true); } - const testBranch = (schema: JSONSchemaRef, originalSchema: JSONSchema): void => { - const subValidationResult = new ValidationResult(); - const subMatchingSchemas = matchingSchemas.newSub(); - - validate(node, asSchema(schema), originalSchema, subValidationResult, subMatchingSchemas, options); - - validationResult.merge(subValidationResult); - validationResult.propertiesMatches += subValidationResult.propertiesMatches; - validationResult.propertiesValueMatches += subValidationResult.propertiesValueMatches; - matchingSchemas.merge(subMatchingSchemas); - }; - - const testCondition = ( - ifSchema: JSONSchemaRef, - originalSchema: JSONSchema, - thenSchema?: JSONSchemaRef, - elseSchema?: JSONSchemaRef - ): void => { - const subSchema = asSchema(ifSchema); - if (subSchema === undefined) { - return; - } - const subValidationResult = new ValidationResult(); - const subMatchingSchemas = matchingSchemas.newSub(); - - validate(node, subSchema, originalSchema, subValidationResult, subMatchingSchemas, options); - matchingSchemas.merge(subMatchingSchemas); - - // const { filePatternAssociation } = subSchema; - // if (filePatternAssociation) { - // const association = new FilePatternAssociation(filePatternAssociation); - // if (!association.matchesPattern(options.uri)) { - // subValidationResult.problems.push({ - // location: { offset: node.offset, length: node.length }, - // severity: DiagnosticSeverity.Warning, - // message: `filePatternAssociation '${filePatternAssociation}' does not match with doc uri '${options.uri}'.`, - // source: getSchemaSource(schema!, originalSchema), - // schemaUri: getSchemaUri(schema!, originalSchema), - // }); - // // don't want to expose the error up to code-completion results - // // validationResult.merge(subValidationResult); - // } - // } - - if (!subValidationResult.hasProblems()) { - if (thenSchema) { - testBranch(thenSchema, originalSchema); - } - } else if (elseSchema) { - testBranch(elseSchema, originalSchema); - } - }; - - // const ifSchema = asSchema(schema.if); - // if (ifSchema) { - // testCondition(ifSchema, schema, asSchema(schema.then), asSchema(schema.else)); - // } - if (Array.isArray(schema.enum)) { const val = getNodeValue(node); let enumValueMatch = false; @@ -729,7 +658,7 @@ function validate( return JSON.stringify(v); }) .join(", ")}.`, - source: getSchemaSource(schema, originalSchema), + source: getSchemaSource(schema), schemaUri: getSchemaUri(schema, originalSchema), }); } @@ -747,7 +676,7 @@ function validate( code: ErrorCode.EnumValueMismatch, problemType: ProblemType.constWarning, message: schema.errorMessage || getWarningMessage(ProblemType.constWarning, [JSON.stringify(schema.const)]), - source: getSchemaSource(schema, originalSchema), + source: getSchemaSource(schema), schemaUri: getSchemaUri(schema, originalSchema), problemArgs: [JSON.stringify(schema.const)], }); @@ -763,7 +692,7 @@ function validate( location: { offset: node.parent.offset, length: node.parent.length }, severity: DiagnosticSeverity.Warning, message: schema.deprecationMessage, - source: getSchemaSource(schema, originalSchema), + source: getSchemaSource(schema), schemaUri: getSchemaUri(schema, originalSchema), }); } @@ -777,7 +706,7 @@ function validate( location: { offset: node.offset, length: node.length }, severity: DiagnosticSeverity.Warning, message: `Value is not divisible by ${schema.multipleOf}.`, - source: getSchemaSource(schema, originalSchema), + source: getSchemaSource(schema), schemaUri: getSchemaUri(schema, originalSchema), }); } @@ -803,7 +732,7 @@ function validate( location: { offset: node.offset, length: node.length }, severity: DiagnosticSeverity.Warning, message: `Value is below the exclusive minimum of ${exclusiveMinimum}.`, - source: getSchemaSource(schema, originalSchema), + source: getSchemaSource(schema), schemaUri: getSchemaUri(schema, originalSchema), }); } @@ -813,7 +742,7 @@ function validate( location: { offset: node.offset, length: node.length }, severity: DiagnosticSeverity.Warning, message: `Value is above the exclusive maximum of ${exclusiveMaximum}.`, - source: getSchemaSource(schema, originalSchema), + source: getSchemaSource(schema), schemaUri: getSchemaUri(schema, originalSchema), }); } @@ -823,7 +752,7 @@ function validate( location: { offset: node.offset, length: node.length }, severity: DiagnosticSeverity.Warning, message: `Value is below the minimum of ${minimum}.`, - source: getSchemaSource(schema, originalSchema), + source: getSchemaSource(schema), schemaUri: getSchemaUri(schema, originalSchema), }); } @@ -833,7 +762,7 @@ function validate( location: { offset: node.offset, length: node.length }, severity: DiagnosticSeverity.Warning, message: `Value is below the maximum of ${maximum}.`, - source: getSchemaSource(schema, originalSchema), + source: getSchemaSource(schema), schemaUri: getSchemaUri(schema, originalSchema), }); } @@ -845,7 +774,7 @@ function validate( location: { offset: node.offset, length: node.length }, severity: DiagnosticSeverity.Warning, message: `String is shorter than the minimum length of ${schema.minLength}.`, - source: getSchemaSource(schema, originalSchema), + source: getSchemaSource(schema), schemaUri: getSchemaUri(schema, originalSchema), }); } @@ -855,7 +784,7 @@ function validate( location: { offset: node.offset, length: node.length }, severity: DiagnosticSeverity.Warning, message: `String is longer than the maximum length of ${schema.maxLength}.`, - source: getSchemaSource(schema, originalSchema), + source: getSchemaSource(schema), schemaUri: getSchemaUri(schema, originalSchema), }); } @@ -870,7 +799,7 @@ function validate( schema.patternErrorMessage || schema.errorMessage || `String does not match the pattern of "${schema.pattern}".`, - source: getSchemaSource(schema, originalSchema), + source: getSchemaSource(schema), schemaUri: getSchemaUri(schema, originalSchema), }); } @@ -899,7 +828,7 @@ function validate( location: { offset: node.offset, length: node.length }, severity: DiagnosticSeverity.Warning, message: schema.patternErrorMessage || schema.errorMessage || `String is not a URI: ${errorMessage}`, - source: getSchemaSource(schema, originalSchema), + source: getSchemaSource(schema), schemaUri: getSchemaUri(schema, originalSchema), }); } @@ -919,7 +848,7 @@ function validate( location: { offset: node.offset, length: node.length }, severity: DiagnosticSeverity.Warning, message: schema.patternErrorMessage || schema.errorMessage || format.errorMessage, - source: getSchemaSource(schema, originalSchema), + source: getSchemaSource(schema), schemaUri: getSchemaUri(schema, originalSchema), }); } @@ -971,7 +900,7 @@ function validate( location: { offset: node.offset, length: node.length }, severity: DiagnosticSeverity.Warning, message: `Array has too many items according to schema. Expected ${subSchemas.length} or fewer.`, - source: getSchemaSource(schema, originalSchema), + source: getSchemaSource(schema), schemaUri: getSchemaUri(schema, originalSchema), }); } @@ -1011,7 +940,7 @@ function validate( location: { offset: node.offset, length: node.length }, severity: DiagnosticSeverity.Warning, message: schema.errorMessage || "Array does not contain required item.", - source: getSchemaSource(schema, originalSchema), + source: getSchemaSource(schema), schemaUri: getSchemaUri(schema, originalSchema), }); } @@ -1022,7 +951,7 @@ function validate( location: { offset: node.offset, length: node.length }, severity: DiagnosticSeverity.Warning, message: `Array has too few items. Expected ${schema.minItems} or more.`, - source: getSchemaSource(schema, originalSchema), + source: getSchemaSource(schema), schemaUri: getSchemaUri(schema, originalSchema), }); } @@ -1032,7 +961,7 @@ function validate( location: { offset: node.offset, length: node.length }, severity: DiagnosticSeverity.Warning, message: `Array has too many items. Expected ${schema.maxItems} or fewer.`, - source: getSchemaSource(schema, originalSchema), + source: getSchemaSource(schema), schemaUri: getSchemaUri(schema, originalSchema), }); } @@ -1047,7 +976,7 @@ function validate( location: { offset: node.offset, length: node.length }, severity: DiagnosticSeverity.Warning, message: "Array has duplicate items.", - source: getSchemaSource(schema, originalSchema), + source: getSchemaSource(schema), schemaUri: getSchemaUri(schema, originalSchema), }); } @@ -1107,7 +1036,7 @@ function validate( location: location, severity: DiagnosticSeverity.Warning, message: getWarningMessage(ProblemType.missingRequiredPropWarning, [propertyName]), - source: getSchemaSource(schema, originalSchema), + source: getSchemaSource(schema), schemaUri: getSchemaUri(schema, originalSchema), problemArgs: [propertyName], problemType: ProblemType.missingRequiredPropWarning, @@ -1140,7 +1069,7 @@ function validate( }, severity: DiagnosticSeverity.Warning, message: schema.errorMessage || propertyNotAllowedMessage(propertyName), - source: getSchemaSource(schema, originalSchema), + source: getSchemaSource(schema), schemaUri: getSchemaUri(schema, originalSchema), }); } else { @@ -1177,7 +1106,7 @@ function validate( }, severity: DiagnosticSeverity.Warning, message: schema.errorMessage || propertyNotAllowedMessage(propertyName), - source: getSchemaSource(schema, originalSchema), + source: getSchemaSource(schema), schemaUri: getSchemaUri(schema, originalSchema), }); } else { @@ -1235,7 +1164,7 @@ function validate( }, severity: DiagnosticSeverity.Warning, message: schema.errorMessage || propertyNotAllowedMessage(propertyName), - source: getSchemaSource(schema, originalSchema), + source: getSchemaSource(schema), schemaUri: getSchemaUri(schema, originalSchema), }; if (possibleProperties?.length) { @@ -1253,7 +1182,7 @@ function validate( location: { offset: node.offset, length: node.length }, severity: DiagnosticSeverity.Warning, message: `Object has more properties than limit of ${schema.maxProperties}.`, - source: getSchemaSource(schema, originalSchema), + source: getSchemaSource(schema), schemaUri: getSchemaUri(schema, originalSchema), }); } @@ -1265,7 +1194,7 @@ function validate( location: { offset: node.offset, length: node.length }, severity: DiagnosticSeverity.Warning, message: `Object has fewer properties than the required number of ${schema.minProperties}`, - source: getSchemaSource(schema, originalSchema), + source: getSchemaSource(schema), schemaUri: getSchemaUri(schema, originalSchema), }); } @@ -1329,33 +1258,6 @@ function validate( } } -class FilePatternAssociation { - private schemas: string[]; - private patternRegExp: RegExp | null; - - constructor(pattern: string) { - try { - this.patternRegExp = new RegExp(convertSimple2RegExpPattern(pattern) + "$"); - } catch (e) { - // invalid pattern - this.patternRegExp = null; - } - this.schemas = []; - } - - public addSchema(id: string): void { - this.schemas.push(id); - } - - public matchesPattern(fileName: string): boolean { - return (this.patternRegExp && this.patternRegExp.test(fileName)) ?? false; - } - - public getSchemas(): string[] { - return this.schemas; - } -} - export function equals(one: unknown, other: unknown): boolean { if (one === other) { return true; From d890901518b9f195d866101e2b09a0d7bf3ff613 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Thu, 28 Dec 2023 18:02:53 +0100 Subject: [PATCH 27/98] Add WorkflowTestsSchemaService + refactor hover --- .../src/inversify.config.ts | 6 + .../src/languageService.ts | 3 +- .../src/schema/adapter.ts | 8 +- .../src/schema/service.ts | 27 ++++ .../src/services/hover.ts | 115 +++++++++++++++++- .../src/types.ts | 2 + 6 files changed, 150 insertions(+), 11 deletions(-) create mode 100644 server/packages/workflow-tests-language-service/src/schema/service.ts diff --git a/server/packages/workflow-tests-language-service/src/inversify.config.ts b/server/packages/workflow-tests-language-service/src/inversify.config.ts index e58578a..e65b518 100644 --- a/server/packages/workflow-tests-language-service/src/inversify.config.ts +++ b/server/packages/workflow-tests-language-service/src/inversify.config.ts @@ -4,11 +4,17 @@ import { TYPES as COMMON_TYPES, WorkflowTestsLanguageService } from "@gxwf/serve import { WorkflowTestsHoverService, WorkflowTestsHoverServiceImpl } from "./services/hover"; import { TYPES } from "./types"; import { WorkflowTestsSchemaProvider, WorkflowTestsSchemaProviderImpl } from "./schema/provider"; +import { JSONSchemaService, JSONSchemaServiceImpl } from "./schema/adapter"; +import { WorkflowTestsSchemaService, WorkflowTestsSchemaServiceImpl } from "./schema/service"; export const WorkflowTestsLanguageServiceContainerModule = new ContainerModule((bind) => { bind(TYPES.WorkflowTestsSchemaProvider) .to(WorkflowTestsSchemaProviderImpl) .inSingletonScope(); + bind(TYPES.JSONSchemaService).to(JSONSchemaServiceImpl).inSingletonScope(); + bind(TYPES.WorkflowTestsSchemaService) + .to(WorkflowTestsSchemaServiceImpl) + .inSingletonScope(); bind(TYPES.WorkflowTestsHoverService).to(WorkflowTestsHoverServiceImpl).inSingletonScope(); bind(COMMON_TYPES.WorkflowTestsLanguageService) .to(GxWorkflowTestsLanguageServiceImpl) diff --git a/server/packages/workflow-tests-language-service/src/languageService.ts b/server/packages/workflow-tests-language-service/src/languageService.ts index 84d54d8..ba69e91 100644 --- a/server/packages/workflow-tests-language-service/src/languageService.ts +++ b/server/packages/workflow-tests-language-service/src/languageService.ts @@ -36,8 +36,7 @@ export class GxWorkflowTestsLanguageServiceImpl extends LanguageServiceBase { - const hover = this.hoverService.doHover(documentContext.textDocument, position, documentContext.nodeManager); - return hover; + return this.hoverService.doHover(documentContext, position); } public override doComplete( diff --git a/server/packages/workflow-tests-language-service/src/schema/adapter.ts b/server/packages/workflow-tests-language-service/src/schema/adapter.ts index 42be7ec..d357efb 100644 --- a/server/packages/workflow-tests-language-service/src/schema/adapter.ts +++ b/server/packages/workflow-tests-language-service/src/schema/adapter.ts @@ -20,6 +20,7 @@ import { import { Diagnostic, DiagnosticSeverity, DocumentContext, Range } from "@gxwf/server-common/src/languageTypes"; import { URI } from "vscode-uri"; import { JSONSchema, JSONSchemaRef } from "./jsonSchema"; +import { injectable } from "inversify"; const YAML_SCHEMA_PREFIX = "yaml-schema: "; export const YAML_SOURCE = "YAML"; @@ -153,7 +154,7 @@ export function contains(node: ASTNode, offset: number, includeRightBound = fals ); } -export interface SchemaService { +export interface JSONSchemaService { validate( documentContext: DocumentContext, schema: JSONSchema | undefined, @@ -171,11 +172,12 @@ export interface SchemaService { ): IApplicableSchema[]; } -export class SchemaServiceImpl implements SchemaService { +@injectable() +export class JSONSchemaServiceImpl implements JSONSchemaService { public validate( documentContext: DocumentContext, schema: JSONSchema | undefined, - severity: DiagnosticSeverity = DiagnosticSeverity.Error, + severity: DiagnosticSeverity = DiagnosticSeverity.Warning, disableAdditionalProperties = false ): Diagnostic[] | undefined { const root = documentContext.nodeManager.root!; diff --git a/server/packages/workflow-tests-language-service/src/schema/service.ts b/server/packages/workflow-tests-language-service/src/schema/service.ts new file mode 100644 index 0000000..e3ccc94 --- /dev/null +++ b/server/packages/workflow-tests-language-service/src/schema/service.ts @@ -0,0 +1,27 @@ +import { inject, injectable } from "inversify"; +import { IApplicableSchema, JSONSchemaService } from "./adapter"; +import { WorkflowTestsSchemaProvider } from "./provider"; +import { TYPES } from "../types"; +import { DocumentContext } from "@gxwf/server-common/src/languageTypes"; +import { DiagnosticSeverity, Diagnostic } from "vscode-languageserver-types"; + +export interface WorkflowTestsSchemaService { + validate(documentContext: DocumentContext, severity?: DiagnosticSeverity): Diagnostic[] | undefined; + getMatchingSchemas(documentContext: DocumentContext, nodeOffset?: number | undefined): IApplicableSchema[]; +} + +@injectable() +export class WorkflowTestsSchemaServiceImpl implements WorkflowTestsSchemaService { + constructor( + @inject(TYPES.WorkflowTestsSchemaProvider) protected schemaProvider: WorkflowTestsSchemaProvider, + @inject(TYPES.JSONSchemaService) protected jsonSchemaService: JSONSchemaService + ) {} + validate(documentContext: DocumentContext, severity?: DiagnosticSeverity): Diagnostic[] | undefined { + const resolvedSchema = this.schemaProvider.getResolvedSchema(); + return this.jsonSchemaService.validate(documentContext, resolvedSchema.schema, severity); + } + getMatchingSchemas(documentContext: DocumentContext, nodeOffset?: number | undefined): IApplicableSchema[] { + const resolvedSchema = this.schemaProvider.getResolvedSchema(); + return this.jsonSchemaService.getMatchingSchemas(documentContext, resolvedSchema.schema, nodeOffset); + } +} diff --git a/server/packages/workflow-tests-language-service/src/services/hover.ts b/server/packages/workflow-tests-language-service/src/services/hover.ts index 7729b98..845cb04 100644 --- a/server/packages/workflow-tests-language-service/src/services/hover.ts +++ b/server/packages/workflow-tests-language-service/src/services/hover.ts @@ -1,14 +1,117 @@ -import { ASTNodeManager } from "@gxwf/server-common/src/ast/nodeManager"; -import { Hover, Position, TextDocument } from "@gxwf/server-common/src/languageTypes"; -import { injectable } from "inversify"; +import { + DocumentContext, + Hover, + MarkupContent, + MarkupKind, + Position, + Range, +} from "@gxwf/server-common/src/languageTypes"; +import { inject, injectable } from "inversify"; +import { TYPES } from "../types"; +import { isAllSchemasMatched, isBoolean } from "../schema/adapter"; +import { JSONSchemaRef } from "../schema/jsonSchema"; +import { WorkflowTestsSchemaService } from "../schema/service"; export interface WorkflowTestsHoverService { - doHover(document: TextDocument, position: Position, nodeManager: ASTNodeManager): Promise; + doHover(documentContext: DocumentContext, position: Position): Promise; } @injectable() export class WorkflowTestsHoverServiceImpl implements WorkflowTestsHoverService { - public async doHover(document: TextDocument, position: Position, nodeManager: ASTNodeManager): Promise { - return null; + constructor(@inject(TYPES.WorkflowTestsSchemaService) protected schemaService: WorkflowTestsSchemaService) {} + + public async doHover(documentContext: DocumentContext, position: Position): Promise { + const offset = documentContext.textDocument.offsetAt(position); + let node = documentContext.nodeManager.getNodeFromOffset(offset); + if ( + !node || + ((node.type === "object" || node.type === "array") && + offset > node.offset + 1 && + offset < node.offset + node.length - 1) + ) { + return Promise.resolve(null); + } + const hoverRangeNode = node; + + // use the property description when hovering over an object key + if (node.type === "string") { + const parent = node.parent; + if (parent && parent.type === "property" && parent.keyNode === node) { + node = parent.valueNode; + } + } + + if (!node) { + return Promise.resolve(null); + } + + const hoverRange = Range.create( + documentContext.textDocument.positionAt(hoverRangeNode.offset), + documentContext.textDocument.positionAt(hoverRangeNode.offset + hoverRangeNode.length) + ); + + const matchingSchemas = this.schemaService.getMatchingSchemas(documentContext, node.offset); + + const removePipe = (value: string): string => { + return value.replace(/\|\|\s*$/, ""); + }; + + let title: string | undefined = undefined; + let markdownDescription: string | undefined = undefined; + + matchingSchemas.every((matchingSchema) => { + if ( + (matchingSchema.node === node || (node?.type === "property" && node.valueNode === matchingSchema.node)) && + matchingSchema.schema + ) { + title = title || matchingSchema.schema.title; + markdownDescription = markdownDescription || matchingSchema.schema.description; + if (matchingSchema.schema.anyOf && isAllSchemasMatched(node, matchingSchemas, matchingSchema.schema)) { + title = ""; + markdownDescription = ""; + matchingSchema.schema.anyOf.forEach((childSchema: JSONSchemaRef, index: number) => { + if (isBoolean(childSchema)) { + return; + } + title += childSchema.title || ""; + markdownDescription += childSchema.description || ""; + const numOptions = matchingSchema.schema.anyOf ? matchingSchema.schema.anyOf.length - 1 : 0; + if (index !== numOptions) { + title += " || "; + markdownDescription += " || "; + } + }); + title = removePipe(title); + markdownDescription = removePipe(markdownDescription); + } + } + return true; + }); + let result = ""; + if (title) { + result = `#### ${title}`; + } + if (markdownDescription) { + if (result.length > 0) { + result += "\n\n"; + } + result += markdownDescription; + } + + const contents = [result == "" ? "Nothing found" : result]; + const hover = this.createHover(contents.join("\n\n"), hoverRange); + return Promise.resolve(hover); + } + + private createHover(contents: string, hoverRange: Range): Hover { + const markupContent: MarkupContent = { + kind: MarkupKind.Markdown, + value: contents, + }; + const result: Hover = { + contents: markupContent, + range: hoverRange, + }; + return result; } } diff --git a/server/packages/workflow-tests-language-service/src/types.ts b/server/packages/workflow-tests-language-service/src/types.ts index 58abc1d..930380e 100644 --- a/server/packages/workflow-tests-language-service/src/types.ts +++ b/server/packages/workflow-tests-language-service/src/types.ts @@ -1,4 +1,6 @@ export const TYPES = { + JSONSchemaService: Symbol.for("JSONSchemaService"), WorkflowTestsSchemaProvider: Symbol.for("WorkflowTestsSchemaProvider"), + WorkflowTestsSchemaService: Symbol.for("WorkflowTestsSchemaService"), WorkflowTestsHoverService: Symbol.for("WorkflowTestsHoverService"), }; From 640818ac56a1d2be34b83de4432e5abdab26f389 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Thu, 28 Dec 2023 18:03:24 +0100 Subject: [PATCH 28/98] Add WorkflowTestsValidationService --- .../src/inversify.config.ts | 4 ++ .../src/languageService.ts | 7 +- .../src/services/validation.ts | 71 +++++++++++++++++++ .../src/types.ts | 1 + 4 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 server/packages/workflow-tests-language-service/src/services/validation.ts diff --git a/server/packages/workflow-tests-language-service/src/inversify.config.ts b/server/packages/workflow-tests-language-service/src/inversify.config.ts index e65b518..3267f23 100644 --- a/server/packages/workflow-tests-language-service/src/inversify.config.ts +++ b/server/packages/workflow-tests-language-service/src/inversify.config.ts @@ -6,6 +6,7 @@ import { TYPES } from "./types"; import { WorkflowTestsSchemaProvider, WorkflowTestsSchemaProviderImpl } from "./schema/provider"; import { JSONSchemaService, JSONSchemaServiceImpl } from "./schema/adapter"; import { WorkflowTestsSchemaService, WorkflowTestsSchemaServiceImpl } from "./schema/service"; +import { WorkflowTestsValidationService, WorkflowTestsValidationServiceImpl } from "./services/validation"; export const WorkflowTestsLanguageServiceContainerModule = new ContainerModule((bind) => { bind(TYPES.WorkflowTestsSchemaProvider) @@ -16,6 +17,9 @@ export const WorkflowTestsLanguageServiceContainerModule = new ContainerModule(( .to(WorkflowTestsSchemaServiceImpl) .inSingletonScope(); bind(TYPES.WorkflowTestsHoverService).to(WorkflowTestsHoverServiceImpl).inSingletonScope(); + bind(TYPES.WorkflowTestsValidationService) + .to(WorkflowTestsValidationServiceImpl) + .inSingletonScope(); bind(COMMON_TYPES.WorkflowTestsLanguageService) .to(GxWorkflowTestsLanguageServiceImpl) .inSingletonScope(); diff --git a/server/packages/workflow-tests-language-service/src/languageService.ts b/server/packages/workflow-tests-language-service/src/languageService.ts index ba69e91..598f841 100644 --- a/server/packages/workflow-tests-language-service/src/languageService.ts +++ b/server/packages/workflow-tests-language-service/src/languageService.ts @@ -16,12 +16,14 @@ import { inject, injectable } from "inversify"; import { TYPES as YAML_TYPES } from "@gxwf/yaml-language-service/src/inversify.config"; import { WorkflowTestsHoverService } from "./services/hover"; import { TYPES } from "./types"; +import { WorkflowTestsValidationService } from "./services/validation"; @injectable() export class GxWorkflowTestsLanguageServiceImpl extends LanguageServiceBase { constructor( @inject(YAML_TYPES.YAMLLanguageService) protected yamlLanguageService: YAMLLanguageService, - @inject(TYPES.WorkflowTestsHoverService) protected hoverService: WorkflowTestsHoverService + @inject(TYPES.WorkflowTestsHoverService) protected hoverService: WorkflowTestsHoverService, + @inject(TYPES.WorkflowTestsValidationService) protected validationService: WorkflowTestsValidationService ) { super("gxwftests"); } @@ -48,7 +50,6 @@ export class GxWorkflowTestsLanguageServiceImpl extends LanguageServiceBase { - // TODO: Implement validation - return Promise.resolve([]); + return this.validationService.doValidation(documentContext); } } diff --git a/server/packages/workflow-tests-language-service/src/services/validation.ts b/server/packages/workflow-tests-language-service/src/services/validation.ts new file mode 100644 index 0000000..a839127 --- /dev/null +++ b/server/packages/workflow-tests-language-service/src/services/validation.ts @@ -0,0 +1,71 @@ +import { Diagnostic, DiagnosticSeverity, DocumentContext, Range } from "@gxwf/server-common/src/languageTypes"; +import { inject, injectable } from "inversify"; +import { WorkflowTestsSchemaProvider } from "../schema/provider"; +import { TYPES } from "../types"; +import { ResolvedSchema } from "../schema/jsonSchema"; +import { WorkflowTestsSchemaService } from "../schema/service"; + +export interface WorkflowTestsValidationService { + doValidation(documentContext: DocumentContext): Promise; +} + +@injectable() +export class WorkflowTestsValidationServiceImpl implements WorkflowTestsValidationService { + constructor( + @inject(TYPES.WorkflowTestsSchemaProvider) protected schemaProvider: WorkflowTestsSchemaProvider, + @inject(TYPES.WorkflowTestsSchemaService) protected schemaService: WorkflowTestsSchemaService + ) {} + + async doValidation(documentContext: DocumentContext): Promise { + const diagnostics: Diagnostic[] = []; + const added: { [signature: string]: boolean } = {}; + + const addProblem = (problem: Diagnostic): void => { + const signature = `${problem.range.start.line} ${problem.range.start.character} ${problem.message}`; + if (!added[signature]) { + added[signature] = true; + diagnostics.push(problem); + } + }; + const getDiagnostics = (schema: ResolvedSchema | undefined): Diagnostic[] => { + const severity = DiagnosticSeverity.Error; + + if (schema) { + const addSchemaProblem = (errorMessage: string): void => { + if (documentContext.nodeManager.root) { + const astRoot = documentContext.nodeManager.root; + const property = astRoot.type === "object" ? astRoot.properties[0] : undefined; + if (property && property.keyNode.value === "$schema") { + const node = property.valueNode || property; + const range = Range.create( + documentContext.textDocument.positionAt(node.offset), + documentContext.textDocument.positionAt(node.offset + node.length) + ); + addProblem(Diagnostic.create(range, errorMessage, severity)); + } else { + const range = Range.create( + documentContext.textDocument.positionAt(astRoot.offset), + documentContext.textDocument.positionAt(astRoot.offset + 1) + ); + addProblem(Diagnostic.create(range, errorMessage, severity)); + } + } + }; + + if (schema.errors.length) { + addSchemaProblem(schema.errors[0]); + } else if (severity) { + const semanticErrors = this.schemaService.validate(documentContext, severity); + if (semanticErrors) { + semanticErrors.forEach(addProblem); + } + } + } + + return diagnostics; + }; + + const schema = this.schemaProvider.getResolvedSchema(); + return getDiagnostics(schema); + } +} diff --git a/server/packages/workflow-tests-language-service/src/types.ts b/server/packages/workflow-tests-language-service/src/types.ts index 930380e..b5536cf 100644 --- a/server/packages/workflow-tests-language-service/src/types.ts +++ b/server/packages/workflow-tests-language-service/src/types.ts @@ -3,4 +3,5 @@ export const TYPES = { WorkflowTestsSchemaProvider: Symbol.for("WorkflowTestsSchemaProvider"), WorkflowTestsSchemaService: Symbol.for("WorkflowTestsSchemaService"), WorkflowTestsHoverService: Symbol.for("WorkflowTestsHoverService"), + WorkflowTestsValidationService: Symbol.for("WorkflowTestsValidationService"), }; From 475b87fac1df8a12daa65c2b8dce8030fd2d6125 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Fri, 26 Jan 2024 10:26:35 +0100 Subject: [PATCH 29/98] Use constants for language IDs --- server/gx-workflow-ls-format2/src/languageService.ts | 4 +++- server/gx-workflow-ls-native/src/languageService.ts | 4 +++- .../src/languageService.ts | 8 +++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/server/gx-workflow-ls-format2/src/languageService.ts b/server/gx-workflow-ls-format2/src/languageService.ts index 41350f5..d75d993 100644 --- a/server/gx-workflow-ls-format2/src/languageService.ts +++ b/server/gx-workflow-ls-format2/src/languageService.ts @@ -20,6 +20,8 @@ import { GxFormat2SchemaValidationService, WorkflowValidationService } from "./s import { inject, injectable } from "inversify"; import { TYPES as YAML_TYPES } from "@gxwf/yaml-language-service/src/inversify.config"; +const LANGUAGE_ID = "gxformat2"; + export interface GxFormat2WorkflowLanguageService extends LanguageService {} /** @@ -38,7 +40,7 @@ export class GxFormat2WorkflowLanguageServiceImpl private _validationServices: WorkflowValidator[]; constructor(@inject(YAML_TYPES.YAMLLanguageService) yamlLanguageService: YAMLLanguageService) { - super("gxformat2"); + super(LANGUAGE_ID); this._schemaLoader = new GalaxyWorkflowFormat2SchemaLoader(); this._yamlLanguageService = yamlLanguageService; this._hoverService = new GxFormat2HoverService(this._schemaLoader.nodeResolver); diff --git a/server/gx-workflow-ls-native/src/languageService.ts b/server/gx-workflow-ls-native/src/languageService.ts index 745d28c..c8c9284 100644 --- a/server/gx-workflow-ls-native/src/languageService.ts +++ b/server/gx-workflow-ls-native/src/languageService.ts @@ -23,6 +23,8 @@ import NativeWorkflowSchema from "../../../workflow-languages/schemas/native.sch import { NativeWorkflowDocument } from "./nativeWorkflowDocument"; import { injectable } from "inversify"; +const LANGUAGE_ID = "galaxyworkflow"; + export interface NativeWorkflowLanguageService extends LanguageService {} /** @@ -38,7 +40,7 @@ export class NativeWorkflowLanguageServiceImpl private _documentSettings: DocumentLanguageSettings = { schemaValidation: "error" }; constructor() { - super("galaxyworkflow"); + super(LANGUAGE_ID); const params: LanguageServiceParams = {}; const settings = this.getLanguageSettings(); this._jsonLanguageService = getLanguageService(params); diff --git a/server/packages/workflow-tests-language-service/src/languageService.ts b/server/packages/workflow-tests-language-service/src/languageService.ts index 598f841..751aa8f 100644 --- a/server/packages/workflow-tests-language-service/src/languageService.ts +++ b/server/packages/workflow-tests-language-service/src/languageService.ts @@ -18,6 +18,12 @@ import { WorkflowTestsHoverService } from "./services/hover"; import { TYPES } from "./types"; import { WorkflowTestsValidationService } from "./services/validation"; +const LANGUAGE_ID = "gxwftests"; + +/** + * A custom implementation of the YAML Language Service to support language features + * for Galaxy workflow test files. + */ @injectable() export class GxWorkflowTestsLanguageServiceImpl extends LanguageServiceBase { constructor( @@ -25,7 +31,7 @@ export class GxWorkflowTestsLanguageServiceImpl extends LanguageServiceBase Date: Sat, 27 Jan 2024 12:05:24 +0100 Subject: [PATCH 30/98] Provide access to resolved schema from service --- .../src/schema/service.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/server/packages/workflow-tests-language-service/src/schema/service.ts b/server/packages/workflow-tests-language-service/src/schema/service.ts index e3ccc94..2491c85 100644 --- a/server/packages/workflow-tests-language-service/src/schema/service.ts +++ b/server/packages/workflow-tests-language-service/src/schema/service.ts @@ -1,11 +1,13 @@ +import { DocumentContext } from "@gxwf/server-common/src/languageTypes"; import { inject, injectable } from "inversify"; +import { Diagnostic, DiagnosticSeverity } from "vscode-languageserver-types"; +import { TYPES } from "../types"; import { IApplicableSchema, JSONSchemaService } from "./adapter"; +import { ResolvedSchema } from "./jsonSchema"; import { WorkflowTestsSchemaProvider } from "./provider"; -import { TYPES } from "../types"; -import { DocumentContext } from "@gxwf/server-common/src/languageTypes"; -import { DiagnosticSeverity, Diagnostic } from "vscode-languageserver-types"; export interface WorkflowTestsSchemaService { + schema: ResolvedSchema; validate(documentContext: DocumentContext, severity?: DiagnosticSeverity): Diagnostic[] | undefined; getMatchingSchemas(documentContext: DocumentContext, nodeOffset?: number | undefined): IApplicableSchema[]; } @@ -16,10 +18,16 @@ export class WorkflowTestsSchemaServiceImpl implements WorkflowTestsSchemaServic @inject(TYPES.WorkflowTestsSchemaProvider) protected schemaProvider: WorkflowTestsSchemaProvider, @inject(TYPES.JSONSchemaService) protected jsonSchemaService: JSONSchemaService ) {} + + get schema(): ResolvedSchema { + return this.schemaProvider.getResolvedSchema(); + } + validate(documentContext: DocumentContext, severity?: DiagnosticSeverity): Diagnostic[] | undefined { const resolvedSchema = this.schemaProvider.getResolvedSchema(); return this.jsonSchemaService.validate(documentContext, resolvedSchema.schema, severity); } + getMatchingSchemas(documentContext: DocumentContext, nodeOffset?: number | undefined): IApplicableSchema[] { const resolvedSchema = this.schemaProvider.getResolvedSchema(); return this.jsonSchemaService.getMatchingSchemas(documentContext, resolvedSchema.schema, nodeOffset); From a803ef9b019ed9342eca516ffce21fd623f58281 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sun, 28 Jan 2024 12:22:30 +0100 Subject: [PATCH 31/98] Add wf tests completion service (no-op for now) --- .../src/inversify.config.ts | 18 ++++++++++---- .../src/languageService.ts | 24 ++++++++++--------- .../src/services/completion/completion.ts | 22 +++++++++++++++++ .../src/services/completion/index.ts | 3 +++ .../src/types.ts | 1 + 5 files changed, 53 insertions(+), 15 deletions(-) create mode 100644 server/packages/workflow-tests-language-service/src/services/completion/completion.ts create mode 100644 server/packages/workflow-tests-language-service/src/services/completion/index.ts diff --git a/server/packages/workflow-tests-language-service/src/inversify.config.ts b/server/packages/workflow-tests-language-service/src/inversify.config.ts index 3267f23..b7e58eb 100644 --- a/server/packages/workflow-tests-language-service/src/inversify.config.ts +++ b/server/packages/workflow-tests-language-service/src/inversify.config.ts @@ -1,25 +1,35 @@ +import { TYPES as COMMON_TYPES, WorkflowTestsLanguageService } from "@gxwf/server-common/src/languageTypes"; import { ContainerModule } from "inversify"; import { GxWorkflowTestsLanguageServiceImpl } from "./languageService"; -import { TYPES as COMMON_TYPES, WorkflowTestsLanguageService } from "@gxwf/server-common/src/languageTypes"; -import { WorkflowTestsHoverService, WorkflowTestsHoverServiceImpl } from "./services/hover"; -import { TYPES } from "./types"; -import { WorkflowTestsSchemaProvider, WorkflowTestsSchemaProviderImpl } from "./schema/provider"; import { JSONSchemaService, JSONSchemaServiceImpl } from "./schema/adapter"; +import { WorkflowTestsSchemaProvider, WorkflowTestsSchemaProviderImpl } from "./schema/provider"; import { WorkflowTestsSchemaService, WorkflowTestsSchemaServiceImpl } from "./schema/service"; +import { WorkflowTestsCompletionService, WorkflowTestsCompletionServiceImpl } from "./services/completion"; +import { WorkflowTestsHoverService, WorkflowTestsHoverServiceImpl } from "./services/hover"; import { WorkflowTestsValidationService, WorkflowTestsValidationServiceImpl } from "./services/validation"; +import { TYPES } from "./types"; export const WorkflowTestsLanguageServiceContainerModule = new ContainerModule((bind) => { bind(TYPES.WorkflowTestsSchemaProvider) .to(WorkflowTestsSchemaProviderImpl) .inSingletonScope(); + bind(TYPES.JSONSchemaService).to(JSONSchemaServiceImpl).inSingletonScope(); + bind(TYPES.WorkflowTestsSchemaService) .to(WorkflowTestsSchemaServiceImpl) .inSingletonScope(); + bind(TYPES.WorkflowTestsHoverService).to(WorkflowTestsHoverServiceImpl).inSingletonScope(); + + bind(TYPES.WorkflowTestsCompletionService) + .to(WorkflowTestsCompletionServiceImpl) + .inSingletonScope(); + bind(TYPES.WorkflowTestsValidationService) .to(WorkflowTestsValidationServiceImpl) .inSingletonScope(); + bind(COMMON_TYPES.WorkflowTestsLanguageService) .to(GxWorkflowTestsLanguageServiceImpl) .inSingletonScope(); diff --git a/server/packages/workflow-tests-language-service/src/languageService.ts b/server/packages/workflow-tests-language-service/src/languageService.ts index 751aa8f..c3a2133 100644 --- a/server/packages/workflow-tests-language-service/src/languageService.ts +++ b/server/packages/workflow-tests-language-service/src/languageService.ts @@ -1,34 +1,37 @@ import { - TextDocument, - Range, + CompletionList, + Diagnostic, FormattingOptions, - TextEdit, + Hover, LanguageServiceBase, Position, - Hover, - CompletionList, - Diagnostic, + Range, + TextDocument, + TextEdit, WorkflowTestsDocument, } from "@gxwf/server-common/src/languageTypes"; +import { TYPES as YAML_TYPES } from "@gxwf/yaml-language-service/src/inversify.config"; import { YAMLLanguageService } from "@gxwf/yaml-language-service/src/yamlLanguageService"; -import { GxWorkflowTestsDocument } from "./document"; import { inject, injectable } from "inversify"; -import { TYPES as YAML_TYPES } from "@gxwf/yaml-language-service/src/inversify.config"; +import { GxWorkflowTestsDocument } from "./document"; +import { WorkflowTestsCompletionService } from "./services/completion"; import { WorkflowTestsHoverService } from "./services/hover"; -import { TYPES } from "./types"; import { WorkflowTestsValidationService } from "./services/validation"; +import { TYPES } from "./types"; const LANGUAGE_ID = "gxwftests"; /** * A custom implementation of the YAML Language Service to support language features * for Galaxy workflow test files. + * It combines specific services to implement the language features. */ @injectable() export class GxWorkflowTestsLanguageServiceImpl extends LanguageServiceBase { constructor( @inject(YAML_TYPES.YAMLLanguageService) protected yamlLanguageService: YAMLLanguageService, @inject(TYPES.WorkflowTestsHoverService) protected hoverService: WorkflowTestsHoverService, + @inject(TYPES.WorkflowTestsCompletionService) protected completionService: WorkflowTestsCompletionService, @inject(TYPES.WorkflowTestsValidationService) protected validationService: WorkflowTestsValidationService ) { super(LANGUAGE_ID); @@ -51,8 +54,7 @@ export class GxWorkflowTestsLanguageServiceImpl extends LanguageServiceBase { - // TODO: Implement completion - return Promise.resolve(null); + return this.completionService.doComplete(documentContext, position); } protected override async doValidation(documentContext: WorkflowTestsDocument): Promise { diff --git a/server/packages/workflow-tests-language-service/src/services/completion/completion.ts b/server/packages/workflow-tests-language-service/src/services/completion/completion.ts new file mode 100644 index 0000000..63fff19 --- /dev/null +++ b/server/packages/workflow-tests-language-service/src/services/completion/completion.ts @@ -0,0 +1,22 @@ +import { CompletionList, DocumentContext, Position } from "@gxwf/server-common/src/languageTypes"; +import { inject, injectable } from "inversify"; +import { WorkflowTestsSchemaService } from "../../schema/service"; +import { TYPES } from "../../types"; + +export interface WorkflowTestsCompletionService { + doComplete(documentContext: DocumentContext, position: Position): Promise; +} + +/** + * Simple wrapper around the YAMLCompletionHelper to combine it with custom completion logic. + */ +@injectable() +export class WorkflowTestsCompletionServiceImpl implements WorkflowTestsCompletionService { + constructor(@inject(TYPES.WorkflowTestsSchemaService) protected schemaService: WorkflowTestsSchemaService) {} + + public async doComplete(documentContext: DocumentContext, position: Position): Promise { + // TODO: Add custom completion logic specific to workflow test files here + const result = null; + return result; + } +} diff --git a/server/packages/workflow-tests-language-service/src/services/completion/index.ts b/server/packages/workflow-tests-language-service/src/services/completion/index.ts new file mode 100644 index 0000000..f12a8a1 --- /dev/null +++ b/server/packages/workflow-tests-language-service/src/services/completion/index.ts @@ -0,0 +1,3 @@ +import { WorkflowTestsCompletionService, WorkflowTestsCompletionServiceImpl } from "./completion"; + +export { WorkflowTestsCompletionService, WorkflowTestsCompletionServiceImpl }; diff --git a/server/packages/workflow-tests-language-service/src/types.ts b/server/packages/workflow-tests-language-service/src/types.ts index b5536cf..53efaf6 100644 --- a/server/packages/workflow-tests-language-service/src/types.ts +++ b/server/packages/workflow-tests-language-service/src/types.ts @@ -3,5 +3,6 @@ export const TYPES = { WorkflowTestsSchemaProvider: Symbol.for("WorkflowTestsSchemaProvider"), WorkflowTestsSchemaService: Symbol.for("WorkflowTestsSchemaService"), WorkflowTestsHoverService: Symbol.for("WorkflowTestsHoverService"), + WorkflowTestsCompletionService: Symbol.for("WorkflowTestsCompletionService"), WorkflowTestsValidationService: Symbol.for("WorkflowTestsValidationService"), }; From ecdea4fa134c550944fd9db0c4dc89620b0010b0 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sun, 18 Feb 2024 00:03:26 +0100 Subject: [PATCH 32/98] Expose internal document for compatibility Attempt to make the document compatible with Red Hat's yaml language server code. --- .../src/nativeWorkflowDocument.ts | 4 ++-- server/packages/server-common/src/ast/types.ts | 2 ++ server/packages/server-common/src/languageTypes.ts | 1 + .../packages/server-common/src/models/document.ts | 8 ++++++-- .../src/parser/yamlDocument.ts | 14 +++++++++++--- 5 files changed, 22 insertions(+), 7 deletions(-) diff --git a/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts b/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts index c49ba8f..bd9db44 100644 --- a/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts +++ b/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts @@ -1,5 +1,5 @@ -import { JSONDocument } from "vscode-json-languageservice"; import { TextDocument, WorkflowDocument } from "@gxwf/server-common/src/languageTypes"; +import { JSONDocument } from "vscode-json-languageservice"; /** * This class provides information about a Native workflow document structure. @@ -8,7 +8,7 @@ export class NativeWorkflowDocument extends WorkflowDocument { private _jsonDocument: JSONDocument; constructor(textDocument: TextDocument, jsonDocument: JSONDocument) { - super(textDocument, jsonDocument); + super(textDocument, { ...jsonDocument, internalDocument: jsonDocument }); this._jsonDocument = jsonDocument; } diff --git a/server/packages/server-common/src/ast/types.ts b/server/packages/server-common/src/ast/types.ts index cec2efd..4813459 100644 --- a/server/packages/server-common/src/ast/types.ts +++ b/server/packages/server-common/src/ast/types.ts @@ -25,6 +25,8 @@ export { export interface ParsedDocument { root?: ASTNode; getNodeFromOffset(offset: number): ASTNode | undefined; + /** Exposed for compatibility with existing external logic. */ + internalDocument: unknown; } /** diff --git a/server/packages/server-common/src/languageTypes.ts b/server/packages/server-common/src/languageTypes.ts index d270083..2941df5 100644 --- a/server/packages/server-common/src/languageTypes.ts +++ b/server/packages/server-common/src/languageTypes.ts @@ -153,6 +153,7 @@ export interface DocumentContext { uri: URI; textDocument: TextDocument; nodeManager: ASTNodeManager; + internalDocument: unknown; } export interface LanguageService { diff --git a/server/packages/server-common/src/models/document.ts b/server/packages/server-common/src/models/document.ts index 9905ebd..e9e2862 100644 --- a/server/packages/server-common/src/models/document.ts +++ b/server/packages/server-common/src/models/document.ts @@ -1,7 +1,7 @@ -import { DocumentContext, TextDocument } from "../languageTypes"; import { URI } from "vscode-uri"; -import { ParsedDocument } from "../ast/types"; import { ASTNodeManager } from "../ast/nodeManager"; +import { ParsedDocument } from "../ast/types"; +import { DocumentContext, TextDocument } from "../languageTypes"; /** * This class contains basic common handling logic for any kind of known document. @@ -21,4 +21,8 @@ export abstract class DocumentBase implements DocumentContext { public get languageId(): string { return this.textDocument.languageId; } + + public get internalDocument(): unknown { + return this.parsedDocument.internalDocument; + } } diff --git a/server/packages/yaml-language-service/src/parser/yamlDocument.ts b/server/packages/yaml-language-service/src/parser/yamlDocument.ts index af29a0e..f5a2d73 100644 --- a/server/packages/yaml-language-service/src/parser/yamlDocument.ts +++ b/server/packages/yaml-language-service/src/parser/yamlDocument.ts @@ -42,6 +42,13 @@ export class YAMLDocument implements ParsedDocument { return this.subDocuments.at(0); } + /** Internal parsed document. + * Exposed for compatibility with reused code from RedHat's YAML Language Service. + */ + public get internalDocument(): YAMLSubDocument | undefined { + return this.mainDocument; + } + /** Returns basic YAML syntax errors or warnings. */ public get syntaxDiagnostics(): Diagnostic[] { if (!this._diagnostics) { @@ -137,9 +144,10 @@ export class YAMLSubDocument { private _lineComments: LineComment[] | undefined; constructor( - public readonly root: ASTNode | undefined, - private readonly parsedDocument: Document - ) {} + + get internalDocument(): Document { + return this.parsedDocument; + } get errors(): YAMLError[] { return this.parsedDocument.errors; From f0cd6c9cab4dfa376ca65246b8d2b2ed0b5c6770 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sun, 18 Feb 2024 19:12:21 +0100 Subject: [PATCH 33/98] Add upstream methods to yaml language server --- .../yaml-language-service/src/parser/index.ts | 8 +- .../src/parser/yamlDocument.ts | 174 +++++++++++++++++- .../yaml-language-service/src/utils/index.ts | 58 ++++++ .../src/utils/textBuffer.ts | 19 +- 4 files changed, 235 insertions(+), 24 deletions(-) create mode 100644 server/packages/yaml-language-service/src/utils/index.ts diff --git a/server/packages/yaml-language-service/src/parser/index.ts b/server/packages/yaml-language-service/src/parser/index.ts index 55749af..935df37 100644 --- a/server/packages/yaml-language-service/src/parser/index.ts +++ b/server/packages/yaml-language-service/src/parser/index.ts @@ -1,10 +1,9 @@ "use strict"; -import { Parser, Composer, Document, LineCounter, ParseOptions, DocumentOptions, SchemaOptions, Node } from "yaml"; import { TextDocument } from "vscode-languageserver-textdocument"; -import { YAMLDocument, YAMLSubDocument } from "./yamlDocument"; +import { Composer, Document, DocumentOptions, LineCounter, ParseOptions, Parser, SchemaOptions } from "yaml"; import { TextBuffer } from "../utils/textBuffer"; -import { convertAST } from "./astConverter"; +import { YAMLDocument, YAMLSubDocument } from "./yamlDocument"; export { YAMLDocument }; @@ -42,6 +41,5 @@ export function parse(textDocument: TextDocument, parserOptions: ParserOptions = } function getParsedSubDocument(parsedDocument: Document, lineCounter: LineCounter): YAMLSubDocument { - const root = convertAST(undefined, parsedDocument.contents as Node, parsedDocument, lineCounter); - return new YAMLSubDocument(root, parsedDocument); + return new YAMLSubDocument(parsedDocument, lineCounter); } diff --git a/server/packages/yaml-language-service/src/parser/yamlDocument.ts b/server/packages/yaml-language-service/src/parser/yamlDocument.ts index f5a2d73..14deeac 100644 --- a/server/packages/yaml-language-service/src/parser/yamlDocument.ts +++ b/server/packages/yaml-language-service/src/parser/yamlDocument.ts @@ -1,10 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + import { ParsedDocument } from "@gxwf/server-common/src/ast/types"; import { TextDocument } from "vscode-languageserver-textdocument"; import { Diagnostic, DiagnosticSeverity, Position } from "vscode-languageserver-types"; -import { Document, Node, YAMLError, YAMLWarning, visit } from "yaml"; +import { Document, LineCounter, Node, Pair, YAMLError, YAMLWarning, isNode, isPair, isScalar, visit } from "yaml"; +import { getIndentation, getParent } from "../utils"; import { guessIndentation } from "../utils/indentationGuesser"; import { TextBuffer } from "../utils/textBuffer"; -import { ASTNode, ObjectASTNodeImpl } from "./astTypes"; +import { convertAST } from "./astConverter"; +import { ASTNode, ObjectASTNodeImpl, YamlNode } from "./astTypes"; const FULL_LINE_ERROR = true; const YAML_SOURCE = "YAML"; @@ -143,7 +150,19 @@ export class YAMLDocument implements ParsedDocument { export class YAMLSubDocument { private _lineComments: LineComment[] | undefined; + private _root: ASTNode | undefined; + constructor( + private readonly parsedDocument: Document, + private readonly _lineCounter: LineCounter + ) {} + + get root(): ASTNode | undefined { + if (!this._root) { + this.updateFromInternalDocument(); + } + return this._root; + } get internalDocument(): Document { return this.parsedDocument; @@ -164,6 +183,10 @@ export class YAMLSubDocument { return this._lineComments; } + public updateFromInternalDocument(): void { + this._root = convertAST(undefined, this.parsedDocument.contents as Node, this.parsedDocument, this._lineCounter); + } + private collectLineComments(): LineComment[] { const lineComments = []; if (this.parsedDocument.commentBefore) { @@ -187,4 +210,151 @@ export class YAMLSubDocument { } return lineComments; } + + /** + * Create a deep copy of this document + */ + clone(): YAMLSubDocument { + const parsedDocumentCopy = this.parsedDocument.clone(); + const lineCounterCopy = new LineCounter(); + this._lineCounter.lineStarts.forEach((lineStart) => lineCounterCopy.addNewLine(lineStart)); + const copy = new YAMLSubDocument(parsedDocumentCopy, lineCounterCopy); + return copy; + } + + getNodeFromPosition( + positionOffset: number, + textBuffer: TextBuffer, + configuredIndentation?: number + ): [YamlNode | undefined, boolean] { + const position = textBuffer.getPosition(positionOffset); + const lineContent = textBuffer.getLineContent(position.line); + if (lineContent.trim().length === 0) { + return [this.findClosestNode(positionOffset, textBuffer, configuredIndentation), true]; + } + + const textAfterPosition = lineContent.substring(position.character); + const spacesAfterPositionMatch = textAfterPosition.match(/^([ ]+)\n?$/); + const areOnlySpacesAfterPosition = !!spacesAfterPositionMatch; + const countOfSpacesAfterPosition = spacesAfterPositionMatch?.[1].length ?? 0; + let closestNode: Node | undefined = undefined; + visit(this.parsedDocument, (_, node) => { + if (!node) { + return; + } + const range = (node as Node).range; + if (!range) { + return; + } + + const isNullNodeOnTheLine = (): boolean => + areOnlySpacesAfterPosition && + positionOffset + countOfSpacesAfterPosition === range[2] && + isScalar(node) && + node.value === null; + + if ((range[0] <= positionOffset && range[1] >= positionOffset) || isNullNodeOnTheLine()) { + closestNode = node as Node; + } else { + return visit.SKIP; + } + }); + + return [closestNode, false]; + } + + findClosestNode(offset: number, textBuffer: TextBuffer, configuredIndentation?: number): YamlNode | undefined { + let offsetDiff = this.parsedDocument.range?.[2] ?? 0; + let maxOffset = this.parsedDocument.range?.[0] ?? 0; + let closestNode: YamlNode | undefined = undefined; + visit(this.parsedDocument, (key, node) => { + if (!node) { + return; + } + const range = (node as Node).range; + if (!range) { + return; + } + const diff = range[1] - offset; + if (maxOffset <= range[0] && diff <= 0 && Math.abs(diff) <= offsetDiff) { + offsetDiff = Math.abs(diff); + maxOffset = range[0]; + closestNode = node as Node; + } + }); + + const position = textBuffer.getPosition(offset); + const lineContent = textBuffer.getLineContent(position.line); + const indentation = getIndentation(lineContent, position.character); + + if (isScalar(closestNode) && (closestNode as Pair).value === null) { + return closestNode; + } + + if (indentation === position.character) { + closestNode = this.getProperParentByIndentation(indentation, closestNode, textBuffer, "", configuredIndentation); + } + + return closestNode; + } + + private getProperParentByIndentation( + indentation: number, + node: YamlNode | undefined, + textBuffer: TextBuffer, + currentLine: string, + configuredIndentation?: number, + rootParent?: YamlNode + ): YamlNode { + if (!node) { + return this.parsedDocument.contents as Node; + } + configuredIndentation = !configuredIndentation ? 2 : configuredIndentation; + if (isNode(node) && node.range) { + const position = textBuffer.getPosition(node.range[0]); + const lineContent = textBuffer.getLineContent(position.line); + currentLine = currentLine === "" ? lineContent.trim() : currentLine; + if (currentLine.startsWith("-") && indentation === configuredIndentation && currentLine === lineContent.trim()) { + position.character += indentation; + } + if (position.character > indentation && position.character > 0) { + const parent = this.getParent(node); + if (parent) { + return this.getProperParentByIndentation( + indentation, + parent, + textBuffer, + currentLine, + configuredIndentation, + rootParent + ); + } + } else if (position.character < indentation) { + const parent = this.getParent(node); + if (isPair(parent) && isNode(parent.value)) { + return parent.value; + } else if (isPair(rootParent) && isNode(rootParent.value)) { + return rootParent.value; + } + } else { + return node; + } + } else if (isPair(node)) { + rootParent = node; + const parent = this.getParent(node); + return this.getProperParentByIndentation( + indentation, + parent, + textBuffer, + currentLine, + configuredIndentation, + rootParent + ); + } + return node; + } + + getParent(node: YamlNode): YamlNode | undefined { + return getParent(this.parsedDocument, node); + } } diff --git a/server/packages/yaml-language-service/src/utils/index.ts b/server/packages/yaml-language-service/src/utils/index.ts new file mode 100644 index 0000000..cb6c134 --- /dev/null +++ b/server/packages/yaml-language-service/src/utils/index.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Document, Node, YAMLMap, YAMLSeq, isDocument, isScalar, visit } from "yaml"; +import { YamlNode } from "../parser/astTypes"; +import { CharCode } from "../parser/charCode"; + +export function getIndentation(lineContent: string, lineOffset: number): number { + if (lineContent.length < lineOffset) { + return 0; + } + + for (let i = 0; i < lineOffset; i++) { + const char = lineContent.charCodeAt(i); + if (char !== CharCode.Space && char !== CharCode.Tab) { + return i; + } + } + + // assuming that current position is indentation + return lineOffset; +} + +export function getParent(doc: Document, nodeToFind: YamlNode): YamlNode | undefined { + let parentNode: Node | undefined = undefined; + visit(doc, (_, node, path) => { + if (node === nodeToFind) { + parentNode = path[path.length - 1] as Node; + return visit.BREAK; + } + }); + + if (isDocument(parentNode)) { + return undefined; + } + + return parentNode; +} + +export function indexOf(seq: YAMLSeq, item: YamlNode): number | undefined { + for (const [i, obj] of seq.items.entries()) { + if (item === obj) { + return i; + } + } + return undefined; +} + +export function isMapContainsEmptyPair(map: YAMLMap): boolean { + if (map.items.length > 1) { + return false; + } + + const pair = map.items[0]; + return isScalar(pair.key) && isScalar(pair.value) && pair.key.value === "" && !pair.value.value; +} diff --git a/server/packages/yaml-language-service/src/utils/textBuffer.ts b/server/packages/yaml-language-service/src/utils/textBuffer.ts index 5b0f9d5..1977f0d 100644 --- a/server/packages/yaml-language-service/src/utils/textBuffer.ts +++ b/server/packages/yaml-language-service/src/utils/textBuffer.ts @@ -5,6 +5,7 @@ import { TextDocument } from "vscode-languageserver-textdocument"; import { Position, Range } from "vscode-languageserver-types"; +import { getIndentation } from "."; import { CharCode } from "../parser/charCode"; interface FullTextDocument { @@ -83,7 +84,7 @@ export class TextBuffer { public getLineIndentationAtOffset(offset: number): number { const position = this.getPosition(offset); const lineContent = this.getLineContent(position.line); - const indentation = this.getIndentation(lineContent, position.character); + const indentation = getIndentation(lineContent, position.character); return indentation; } @@ -103,20 +104,4 @@ export class TextBuffer { } return currentLine; } - - private getIndentation(lineContent: string, lineOffset: number): number { - if (lineContent.length < lineOffset) { - return 0; - } - - for (let i = 0; i < lineOffset; i++) { - const char = lineContent.charCodeAt(i); - if (char !== CharCode.Space && char !== CharCode.Tab) { - return i; - } - } - - // assuming that current position is indentation - return lineOffset; - } } From f5d74d4ec3c454594968a0b7d1b88f43c0f2d39e Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Mon, 19 Feb 2024 13:00:11 +0100 Subject: [PATCH 34/98] Refactor imports in common languageTypes.ts --- .../server-common/src/languageTypes.ts | 118 +++++++++--------- 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/server/packages/server-common/src/languageTypes.ts b/server/packages/server-common/src/languageTypes.ts index 2941df5..7331234 100644 --- a/server/packages/server-common/src/languageTypes.ts +++ b/server/packages/server-common/src/languageTypes.ts @@ -1,104 +1,104 @@ import "reflect-metadata"; import { - Range, - Position, - DocumentUri, - MarkupContent, - MarkupKind, + CodeAction, + CodeActionContext, + CodeActionKind, Color, ColorInformation, ColorPresentation, - FoldingRange, - FoldingRangeKind, - SelectionRange, - Diagnostic, - DiagnosticSeverity, + Command, CompletionItem, CompletionItemKind, - CompletionList, CompletionItemTag, - InsertTextFormat, - SymbolInformation, - SymbolKind, - DocumentSymbol, - Location, - Hover, - MarkedString, - FormattingOptions as LSPFormattingOptions, + CompletionList, DefinitionLink, - CodeActionContext, - Command, - CodeAction, + Diagnostic, + DiagnosticSeverity, DocumentHighlight, + DocumentHighlightKind, DocumentLink, - WorkspaceEdit, - TextEdit, - CodeActionKind, + DocumentSymbol, + DocumentUri, + FoldingRange, + FoldingRangeKind, + Hover, + InsertTextFormat, + FormattingOptions as LSPFormattingOptions, + Location, + MarkedString, + MarkupContent, + MarkupKind, + Position, + Range, + SelectionRange, + SymbolInformation, + SymbolKind, TextDocumentEdit, + TextEdit, VersionedTextDocumentIdentifier, - DocumentHighlightKind, + WorkspaceEdit, } from "vscode-languageserver-types"; import { TextDocument } from "vscode-languageserver-textdocument"; +import { injectable, unmanaged } from "inversify"; import { Connection, DocumentFormattingParams, DocumentRangeFormattingParams, - HoverParams, DocumentSymbolParams, + HoverParams, } from "vscode-languageserver/browser"; -import { WorkflowDocument } from "./models/workflowDocument"; -import { ASTNodeManager } from "./ast/nodeManager"; import { URI } from "vscode-uri"; -import { WorkflowTestsDocument } from "./models/workflowTestsDocument"; -import { injectable, unmanaged } from "inversify"; +import { ASTNodeManager } from "./ast/nodeManager"; import { ConfigService } from "./configService"; +import { WorkflowDocument } from "./models/workflowDocument"; +import { WorkflowTestsDocument } from "./models/workflowTestsDocument"; export { - TextDocument, - Range, - Position, - DocumentUri, - MarkupContent, - MarkupKind, + CodeAction, + CodeActionContext, + CodeActionKind, Color, ColorInformation, ColorPresentation, - FoldingRange, - FoldingRangeKind, - SelectionRange, - Diagnostic, - DiagnosticSeverity, + Command, CompletionItem, CompletionItemKind, - CompletionList, CompletionItemTag, - InsertTextFormat, + CompletionList, DefinitionLink, - SymbolInformation, - SymbolKind, + Diagnostic, + DiagnosticSeverity, + DocumentFormattingParams, + DocumentHighlight, + DocumentHighlightKind, + DocumentLink, + DocumentRangeFormattingParams, DocumentSymbol, - Location, + DocumentSymbolParams, + DocumentUri, + FoldingRange, + FoldingRangeKind, Hover, HoverParams, + InsertTextFormat, + Location, MarkedString, - CodeActionContext, - Command, - CodeAction, - DocumentHighlight, - DocumentLink, - WorkspaceEdit, - TextEdit, - CodeActionKind, + MarkupContent, + MarkupKind, + Position, + Range, + SelectionRange, + SymbolInformation, + SymbolKind, + TextDocument, TextDocumentEdit, + TextEdit, VersionedTextDocumentIdentifier, - DocumentHighlightKind, - DocumentFormattingParams, - DocumentRangeFormattingParams, WorkflowDocument, - DocumentSymbolParams, WorkflowTestsDocument, + WorkspaceEdit, }; export interface FormattingOptions extends LSPFormattingOptions { From cb322003297b8182614833c4e973d7ab676e10a5 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Mon, 19 Feb 2024 13:02:12 +0100 Subject: [PATCH 35/98] Add getMatchingSchemas to WorkflowTestsSchemaService --- .../src/schema/service.ts | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/server/packages/workflow-tests-language-service/src/schema/service.ts b/server/packages/workflow-tests-language-service/src/schema/service.ts index 2491c85..709404f 100644 --- a/server/packages/workflow-tests-language-service/src/schema/service.ts +++ b/server/packages/workflow-tests-language-service/src/schema/service.ts @@ -9,7 +9,11 @@ import { WorkflowTestsSchemaProvider } from "./provider"; export interface WorkflowTestsSchemaService { schema: ResolvedSchema; validate(documentContext: DocumentContext, severity?: DiagnosticSeverity): Diagnostic[] | undefined; - getMatchingSchemas(documentContext: DocumentContext, nodeOffset?: number | undefined): IApplicableSchema[]; + getMatchingSchemas( + documentContext: DocumentContext, + nodeOffset?: number | undefined, + didCallFromAutoComplete?: boolean + ): IApplicableSchema[]; } @injectable() @@ -28,8 +32,18 @@ export class WorkflowTestsSchemaServiceImpl implements WorkflowTestsSchemaServic return this.jsonSchemaService.validate(documentContext, resolvedSchema.schema, severity); } - getMatchingSchemas(documentContext: DocumentContext, nodeOffset?: number | undefined): IApplicableSchema[] { + getMatchingSchemas( + documentContext: DocumentContext, + nodeOffset?: number | undefined, + didCallFromAutoComplete?: boolean + ): IApplicableSchema[] { const resolvedSchema = this.schemaProvider.getResolvedSchema(); - return this.jsonSchemaService.getMatchingSchemas(documentContext, resolvedSchema.schema, nodeOffset); + return this.jsonSchemaService.getMatchingSchemas( + documentContext, + resolvedSchema.schema, + nodeOffset, + null, + didCallFromAutoComplete + ); } } From 98a0d4404d362555c414dfec4ce68a0be6bde45e Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Thu, 22 Feb 2024 15:09:04 +0100 Subject: [PATCH 36/98] Add YAML completion helper --- .../src/services/completion/completion.ts | 9 +- .../src/services/completion/helper.ts | 1672 +++++++++++++++++ 2 files changed, 1679 insertions(+), 2 deletions(-) create mode 100644 server/packages/workflow-tests-language-service/src/services/completion/helper.ts diff --git a/server/packages/workflow-tests-language-service/src/services/completion/completion.ts b/server/packages/workflow-tests-language-service/src/services/completion/completion.ts index 63fff19..fa99764 100644 --- a/server/packages/workflow-tests-language-service/src/services/completion/completion.ts +++ b/server/packages/workflow-tests-language-service/src/services/completion/completion.ts @@ -2,6 +2,7 @@ import { CompletionList, DocumentContext, Position } from "@gxwf/server-common/s import { inject, injectable } from "inversify"; import { WorkflowTestsSchemaService } from "../../schema/service"; import { TYPES } from "../../types"; +import { YAMLCompletionHelper } from "./helper"; export interface WorkflowTestsCompletionService { doComplete(documentContext: DocumentContext, position: Position): Promise; @@ -12,11 +13,15 @@ export interface WorkflowTestsCompletionService { */ @injectable() export class WorkflowTestsCompletionServiceImpl implements WorkflowTestsCompletionService { - constructor(@inject(TYPES.WorkflowTestsSchemaService) protected schemaService: WorkflowTestsSchemaService) {} + private yamlCompletionHelper: YAMLCompletionHelper; + + constructor(@inject(TYPES.WorkflowTestsSchemaService) protected schemaService: WorkflowTestsSchemaService) { + this.yamlCompletionHelper = new YAMLCompletionHelper(schemaService); + } public async doComplete(documentContext: DocumentContext, position: Position): Promise { // TODO: Add custom completion logic specific to workflow test files here - const result = null; + const result = await this.yamlCompletionHelper.doComplete(documentContext, position); return result; } } diff --git a/server/packages/workflow-tests-language-service/src/services/completion/helper.ts b/server/packages/workflow-tests-language-service/src/services/completion/helper.ts new file mode 100644 index 0000000..0d0df99 --- /dev/null +++ b/server/packages/workflow-tests-language-service/src/services/completion/helper.ts @@ -0,0 +1,1672 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* + * This is a modified version of the original yamlCompletion logic from the yaml-language-server. + * The original file can be found here: https://github.com/redhat-developer/yaml-language-server/blob/main/src/languageservice/services/yamlCompletion.ts#L1601 + * + * The reason for this is that the original dependency is not compatible with the browser version of the language server. + * In addition, there are some differences in the way we handle the AST and the schemas. + */ + +import { + CompletionItem as CompletionItemBase, + CompletionItemKind, + CompletionList, + DocumentContext, + InsertTextFormat, + MarkupContent, + MarkupKind, + Position, + Range, + TextEdit, +} from "@gxwf/server-common/src/languageTypes"; +import { YamlNode } from "@gxwf/yaml-language-service/src/parser/astTypes"; +import { YAMLSubDocument } from "@gxwf/yaml-language-service/src/parser/yamlDocument"; +import { indexOf, isMapContainsEmptyPair } from "@gxwf/yaml-language-service/src/utils"; +import { guessIndentation } from "@gxwf/yaml-language-service/src/utils/indentationGuesser"; +import { TextBuffer } from "@gxwf/yaml-language-service/src/utils/textBuffer"; +import { Node, Pair, YAMLMap, YAMLSeq, isMap, isNode, isPair, isScalar, isSeq } from "yaml"; +import { isDefined, isString } from "../../schema/adapter"; +import { JSONSchema, JSONSchemaRef } from "../../schema/jsonSchema"; +import { WorkflowTestsSchemaService } from "../../schema/service"; + +const doubleQuotesEscapeRegExp = /[\\]+"/g; +const parentCompletionKind = CompletionItemKind.Class; +const existingProposeItem = "__"; + +interface ParentCompletionItemOptions { + schema: JSONSchema; + indent?: string; + insertTexts?: string[]; +} + +interface CompletionItem extends CompletionItemBase { + parent?: ParentCompletionItemOptions; +} + +interface CompletionsCollector { + add(suggestion: CompletionItem, oneOfSchema?: boolean): void; + error(message: string): void; + log(message: string): void; + getNumberOfProposals(): number; + result: CompletionList; + proposed: { [key: string]: CompletionItem }; +} + +interface InsertText { + insertText: string; + insertIndex: number; +} + +export class YAMLCompletionHelper { + private indentation: string = " "; + private arrayPrefixIndentation: string = ""; + + constructor(protected schemaService: WorkflowTestsSchemaService) {} + + private get newTestSnippet(): string { + return `- doc: \${1:TODO write test description} +${this.indentation}job: +${this.indentation}${this.indentation}$0 +`; + } + + private get newTestSnippetCompletion(): CompletionItem { + const completionItem: CompletionItem = { + label: "- doc:", + labelDetails: { detail: "New Workflow Test" }, + documentation: { + kind: MarkupKind.Markdown, + value: + "Create a new workflow test definition.\n\nYou can provide a `description` for the test and then press `Tab` to continue defining input jobs.", + }, + kind: CompletionItemKind.Property, + insertText: this.newTestSnippet, + insertTextFormat: InsertTextFormat.Snippet, + }; + return completionItem; + } + + public async doComplete(documentContext: DocumentContext, position: Position): Promise { + const result = CompletionList.create([], false); + + const document = documentContext.textDocument; + const textBuffer = new TextBuffer(document); + + const indent = guessIndentation(textBuffer, 2, true); + this.indentation = indent.insertSpaces ? " ".repeat(indent.tabSize) : "\t"; + + const offset = document.offsetAt(position); + const text = document.getText(); + + if (text.charAt(offset - 1) === ":") { + return Promise.resolve(result); + } + + let currentDoc = documentContext.internalDocument as YAMLSubDocument; + if (currentDoc === null) { + return Promise.resolve(result); + } + // as we modify AST for completion, we need to use copy of original document + currentDoc = currentDoc.clone(); + let [node, foundByClosest] = currentDoc.getNodeFromPosition(offset, textBuffer, this.indentation.length); + + if (!node) { + result.items.push(this.newTestSnippetCompletion); + return result; + } + + const currentWord = textBuffer.getCurrentWord(offset); + let lineContent = textBuffer.getLineContent(position.line); + const lineAfterPosition = lineContent.substring(position.character); + const areOnlySpacesAfterPosition = /^[ ]+\n?$/.test(lineAfterPosition); + + // if the line is empty, or only contains a dash without indentation, + // we suggest a new test snippet + if (lineContent.match(/^(\n|-|- {1}|-\n)$/)) { + result.items.push(this.newTestSnippetCompletion); + return result; + } + + let overwriteRange: Range | null = null; + if (areOnlySpacesAfterPosition) { + overwriteRange = Range.create(position, Position.create(position.line, lineContent.length)); + const isOnlyWhitespace = lineContent.trim().length === 0; + const isOnlyDash = lineContent.match(/^\s*(-)\s*$/); + if (node && isScalar(node) && !isOnlyWhitespace && !isOnlyDash) { + const lineToPosition = lineContent.substring(0, position.character); + const matches = + // get indentation of unfinished property (between indent and cursor) + lineToPosition.match(/^[\s-]*([^:]+)?$/) || + // OR get unfinished value (between colon and cursor) + lineToPosition.match(/:[ \t]((?!:[ \t]).*)$/); + + if (matches?.[1]) { + overwriteRange = Range.create( + Position.create(position.line, position.character - matches[1].length), + Position.create(position.line, lineContent.length) + ); + } + } + } else if (node && isScalar(node) && node.value === "null") { + const nodeStartPos = document.positionAt(node.range?.[0] ?? 0); + nodeStartPos.character += 1; + const nodeEndPos = document.positionAt(node.range?.[2] ?? 0); + nodeEndPos.character += 1; + overwriteRange = Range.create(nodeStartPos, nodeEndPos); + } else if (node && isScalar(node) && node.value) { + const start = document.positionAt(node.range?.[0] ?? 0); + overwriteRange = Range.create(start, document.positionAt(node.range?.[1] ?? 0)); + } else if (node && isScalar(node) && node.value === null && currentWord === "-") { + overwriteRange = Range.create(position, position); + this.arrayPrefixIndentation = " "; + } else { + let overwriteStart = offset - currentWord.length; + if (overwriteStart > 0 && text[overwriteStart - 1] === '"') { + overwriteStart--; + } + overwriteRange = Range.create(document.positionAt(overwriteStart), position); + } + + const proposed: { [key: string]: CompletionItem } = {}; + const collector: CompletionsCollector = { + add: (completionItem: CompletionItem, oneOfSchema: boolean) => { + const addSuggestionForParent = function (completionItem: CompletionItem): void { + const existsInYaml = proposed[completionItem.label]?.label === existingProposeItem; + //don't put to parent suggestion if already in yaml + if (existsInYaml) { + return; + } + const schema = completionItem.parent?.schema; + const schemaType = schema?.title ?? "unknown schema type"; + const schemaDescription = schema?.description; + + let parentCompletion: CompletionItem | undefined = result.items.find( + (item: CompletionItem) => item.parent?.schema === schema && item.kind === parentCompletionKind + ); + + if ( + !parentCompletion || + !parentCompletion.parent || + !parentCompletion.parent.insertTexts || + !completionItem.insertText + ) { + return; + } + + if (parentCompletion.parent.insertTexts.includes(completionItem.insertText)) { + // already exists in the parent + return; + } else if (!parentCompletion) { + // create a new parent + parentCompletion = { + ...completionItem, + label: schemaType, + documentation: schemaDescription, + sortText: "_" + schemaType, // this parent completion goes first, + kind: parentCompletionKind, + }; + parentCompletion.label = parentCompletion.label || completionItem.label; + if (parentCompletion.parent) { + parentCompletion.parent.insertTexts = [completionItem.insertText]; + } + result.items.push(parentCompletion); + } else { + // add to the existing parent + parentCompletion.parent.insertTexts.push(completionItem.insertText); + } + }; + + const isForParentCompletion = !!completionItem.parent; + let label = completionItem.label; + if (!label) { + // we receive not valid CompletionItem as `label` is mandatory field, so just ignore it + console.warn(`Ignoring CompletionItem without label: ${JSON.stringify(completionItem)}`); + return; + } + if (!isString(label)) { + label = String(label); + } + + label = label.replace(/[\n]/g, "↵"); + if (label.length > 60) { + const shortendedLabel = label.substr(0, 57).trim() + "..."; + if (!proposed[shortendedLabel]) { + label = shortendedLabel; + } + } + + // trim $1 from end of completion + if (completionItem.insertText) { + if (completionItem.insertText.endsWith("$1") && !isForParentCompletion) { + completionItem.insertText = completionItem.insertText.substr(0, completionItem.insertText.length - 2); + } + if (overwriteRange && overwriteRange.start.line === overwriteRange.end.line) { + completionItem.textEdit = TextEdit.replace(overwriteRange, completionItem.insertText); + } + } + + completionItem.label = label; + + if (isForParentCompletion) { + addSuggestionForParent(completionItem); + return; + } + + if (this.arrayPrefixIndentation) { + this.updateCompletionText(completionItem, this.arrayPrefixIndentation + completionItem.insertText); + } + + const existing = proposed[label]; + const isInsertTextDifferent = + existing?.label !== existingProposeItem && existing?.insertText !== completionItem.insertText; + if (!existing) { + proposed[label] = completionItem; + result.items.push(completionItem); + } else if (existing.insertText && completionItem.insertText && isInsertTextDifferent) { + // try to merge simple insert values + const mergedText = this.mergeSimpleInsertTexts( + label, + existing.insertText, + completionItem.insertText, + oneOfSchema + ); + if (mergedText) { + this.updateCompletionText(existing, mergedText); + } else { + // add to result when it wasn't able to merge (even if the item is already there but with a different value) + proposed[label] = completionItem; + result.items.push(completionItem); + } + } + if (existing && !existing.documentation && completionItem.documentation) { + existing.documentation = completionItem.documentation; + } + }, + error: (message: string) => { + console.error(message); + }, + log: (message: string) => { + console.log(message); + }, + getNumberOfProposals: () => { + return result.items.length; + }, + result, + proposed, + }; + + // Not supported in this version + // if (this.customTags.length > 0) { + // this.getCustomTagValueCompletions(collector); + // } + + if (lineContent.endsWith("\n")) { + lineContent = lineContent.substring(0, lineContent.length - 1); + } + + try { + const schema = this.schemaService.schema; + + // Modeline not supported in this version + // if (!schema || schema.errors.length) { + // if (position.line === 0 && position.character === 0 && !isModeline(lineContent)) { + // const inlineSchemaCompletion = { + // kind: CompletionItemKind.Text, + // label: "Inline schema", + // insertText: "# yaml-language-server: $schema=", + // insertTextFormat: InsertTextFormat.PlainText, + // }; + // result.items.push(inlineSchemaCompletion); + // } + // } + + // if (isModeline(lineContent) || isInComment(doc.tokens, offset)) { + // const schemaIndex = lineContent.indexOf("$schema="); + // if (schemaIndex !== -1 && schemaIndex + "$schema=".length <= position.character) { + // this.schemaService.getAllSchemas().forEach((schema) => { + // const schemaIdCompletion: CompletionItem = { + // kind: CompletionItemKind.Constant, + // label: schema.name ?? schema.uri, + // detail: schema.description, + // insertText: schema.uri, + // insertTextFormat: InsertTextFormat.PlainText, + // insertTextMode: InsertTextMode.asIs, + // }; + // result.items.push(schemaIdCompletion); + // }); + // } + // return result; + // } + + if (!schema || schema.errors.length) { + return result; + } + + let currentProperty: YamlNode | null = null; + + if (!node) { + if (!currentDoc.internalDocument.contents || isScalar(currentDoc.internalDocument.contents)) { + const map = currentDoc.internalDocument.createNode({}); + map.range = [offset, offset + 1, offset + 1]; + currentDoc.internalDocument.contents = map; + currentDoc.updateFromInternalDocument(); + node = map; + } else { + node = currentDoc.findClosestNode(offset, textBuffer); + foundByClosest = true; + } + } + + const originalNode = node!; + if (node) { + if (lineContent.length === 0) { + node = currentDoc.internalDocument.contents as Node; + } else { + const parent = currentDoc.getParent(node); + if (parent) { + if (isScalar(node)) { + if (node.value) { + if (isPair(parent)) { + if (parent.value === node) { + if (lineContent.trim().length > 0 && lineContent.indexOf(":") < 0) { + const map = this.createTempObjNode(currentWord, node, currentDoc); + const parentParent = currentDoc.getParent(parent); + if (isSeq(currentDoc.internalDocument.contents)) { + const index = indexOf(currentDoc.internalDocument.contents, parent); + if (typeof index === "number") { + currentDoc.internalDocument.set(index, map); + currentDoc.updateFromInternalDocument(); + } + } else if (parentParent && (isMap(parentParent) || isSeq(parentParent))) { + parentParent.set(parent.key, map); + currentDoc.updateFromInternalDocument(); + } else { + currentDoc.internalDocument.set(parent.key, map); + currentDoc.updateFromInternalDocument(); + } + + currentProperty = (map as YAMLMap).items[0]; + node = map; + } else if (lineContent.trim().length === 0) { + const parentParent = currentDoc.getParent(parent); + if (parentParent) { + node = parentParent; + } + } + } else if (parent.key === node) { + const parentParent = currentDoc.getParent(parent); + currentProperty = parent; + if (parentParent) { + node = parentParent; + } + } + } else if (isSeq(parent)) { + if (lineContent.trim().length > 0) { + const map = this.createTempObjNode(currentWord, node, currentDoc); + parent.delete(node); + parent.add(map); + currentDoc.updateFromInternalDocument(); + node = map; + } else { + node = parent; + } + } + } else if (node.value === null) { + if (isPair(parent)) { + if (parent.key === node) { + node = parent; + } else { + if (isNode(parent.key) && parent.key.range) { + const parentParent = currentDoc.getParent(parent); + if ( + foundByClosest && + parentParent && + isMap(parentParent) && + isMapContainsEmptyPair(parentParent) + ) { + node = parentParent; + } else { + const parentPosition = document.positionAt(parent.key.range[0]); + //if cursor has bigger indentation that parent key, then we need to complete new empty object + if (position.character > parentPosition.character && position.line !== parentPosition.line) { + const map = this.createTempObjNode(currentWord, node, currentDoc); + + if (parentParent && (isMap(parentParent) || isSeq(parentParent))) { + parentParent.set(parent.key, map); + currentDoc.updateFromInternalDocument(); + } else { + currentDoc.internalDocument.set(parent.key, map); + currentDoc.updateFromInternalDocument(); + } + currentProperty = (map as YAMLMap).items[0]; + node = map; + } else if (parentPosition.character === position.character) { + if (parentParent) { + node = parentParent; + } + } + } + } + } + } else if (isSeq(parent)) { + if (lineContent.charAt(position.character - 1) !== "-") { + const map = this.createTempObjNode(currentWord, node, currentDoc); + parent.delete(node); + parent.add(map); + currentDoc.updateFromInternalDocument(); + node = map; + } else if (lineContent.charAt(position.character - 1) === "-") { + const map = this.createTempObjNode("", node, currentDoc); + parent.delete(node); + parent.add(map); + currentDoc.updateFromInternalDocument(); + node = map; + } else { + node = parent; + } + } + } + } else if (isMap(node)) { + if (!foundByClosest && lineContent.trim().length === 0 && isSeq(parent)) { + const nextLine = textBuffer.getLineContent(position.line + 1); + if (textBuffer.getLineCount() === position.line + 1 || nextLine.trim().length === 0) { + node = parent; + } + } + } + } else if (isScalar(node)) { + const map = this.createTempObjNode(currentWord, node, currentDoc); + currentDoc.internalDocument.contents = map; + currentDoc.updateFromInternalDocument(); + currentProperty = map.items[0]; + node = map; + } else if (isMap(node)) { + for (const pair of node.items) { + if (isNode(pair.value) && pair.value.range && pair.value.range[0] === offset + 1) { + node = pair.value; + } + } + } else if (isSeq(node)) { + if (lineContent.charAt(position.character - 1) !== "-") { + const map = this.createTempObjNode(currentWord, node, currentDoc); + map.items = []; + currentDoc.updateFromInternalDocument(); + for (const pair of node.items) { + if (isMap(pair)) { + pair.items.forEach((value) => { + map.items.push(value); + }); + } + } + node = map; + } + } + } + } + + // completion for object keys + if (node && isMap(node)) { + // don't suggest properties that are already present + const properties = node.items; + for (const p of properties) { + if (!currentProperty || currentProperty !== p) { + if (isScalar(p.key)) { + proposed[p.key.value + ""] = CompletionItemBase.create(existingProposeItem); + } + } + } + + this.addPropertyCompletions( + documentContext, + currentDoc, + node, + originalNode, + "", + collector, + textBuffer, + overwriteRange + ); + + if (!schema && currentWord.length > 0 && text.charAt(offset - currentWord.length - 1) !== '"') { + collector.add({ + kind: CompletionItemKind.Property, + label: currentWord, + insertText: this.getInsertTextForProperty(currentWord, null, ""), + insertTextFormat: InsertTextFormat.Snippet, + }); + } + } + + // proposals for values + const types: { [type: string]: boolean } = {}; + this.getValueCompletions(documentContext, currentDoc, offset, collector, types, node); + } catch (err) { + console.error(err); + } + + this.finalizeParentCompletion(result); + + const uniqueItems = result.items.filter( + (arr, index, self) => + index === + self.findIndex( + (item) => item.label === arr.label && item.insertText === arr.insertText && item.kind === arr.kind + ) + ); + + if (uniqueItems?.length > 0) { + result.items = uniqueItems; + } + + return result; + } + + updateCompletionText(completionItem: CompletionItem, text: string): void { + completionItem.insertText = text; + if (completionItem.textEdit) { + completionItem.textEdit.newText = text; + } + } + + mergeSimpleInsertTexts( + label: string, + existingText: string, + addingText: string, + oneOfSchema: boolean + ): string | undefined { + const containsNewLineAfterColon = (value: string): boolean => { + return value.includes("\n"); + }; + const startWithNewLine = (value: string): boolean => { + return value.startsWith("\n"); + }; + const isNullObject = (value: string): boolean => { + const index = value.indexOf("\n"); + return index > 0 && value.substring(index, value.length).trim().length === 0; + }; + if (containsNewLineAfterColon(existingText) || containsNewLineAfterColon(addingText)) { + //if the exisiting object null one then replace with the non-null object + if (oneOfSchema && isNullObject(existingText) && !isNullObject(addingText) && !startWithNewLine(addingText)) { + return addingText; + } + return undefined; + } + const existingValues = this.getValuesFromInsertText(existingText); + const addingValues = this.getValuesFromInsertText(addingText); + + const newValues = Array.prototype.concat(existingValues, addingValues); + if (!newValues.length) { + return undefined; + } else if (newValues.length === 1) { + return `${label}: \${1:${newValues[0]}}`; + } else { + return `${label}: \${1|${newValues.join(",")}|}`; + } + } + + getValuesFromInsertText(insertText: string): string[] { + const value = insertText.substring(insertText.indexOf(":") + 1).trim(); + if (!value) { + return []; + } + const valueMath = value.match(/^\${1[|:]([^|]*)+\|?}$/); // ${1|one,two,three|} or ${1:one} + if (valueMath) { + return valueMath[1].split(","); + } + return [value]; + } + + private getInsertTextForProperty( + key: string, + propertySchema: JSONSchema | null, + separatorAfter: string, + indent = this.indentation + ): string { + const propertyText = this.getInsertTextForValue(key, "", "string"); + const resultText = propertyText + ":"; + + let value: string = ""; + let nValueProposals = 0; + if (propertySchema) { + let type = Array.isArray(propertySchema.type) ? propertySchema.type[0] : propertySchema.type; + if (!type) { + if (propertySchema.properties) { + type = "object"; + } else if (propertySchema.items) { + type = "array"; + } else if (propertySchema.anyOf) { + type = "anyOf"; + } + } + + if (!type) { + return value; + } + + if (propertySchema.enum) { + if (!value && propertySchema.enum.length === 1) { + value = " " + this.getInsertTextForGuessedValue(propertySchema.enum[0], "", type); + } + nValueProposals += propertySchema.enum.length; + } + + if (propertySchema.const) { + if (!value) { + value = this.getInsertTextForGuessedValue(propertySchema.const, "", type); + value = evaluateTab1Symbol(value); // prevent const being selected after snippet insert + value = " " + value; + } + nValueProposals++; + } + + if (isDefined(propertySchema.default)) { + if (!value) { + value = " " + this.getInsertTextForGuessedValue(propertySchema.default, "", type); + } + nValueProposals++; + } + + if (propertySchema.properties) { + return `${resultText}\n${this.getInsertTextForObject(propertySchema, separatorAfter, indent).insertText}`; + } else if (propertySchema.items) { + return `${resultText}\n${indent}- ${ + this.getInsertTextForArray(propertySchema.items, separatorAfter, 1, indent).insertText + }`; + } + if (nValueProposals === 0) { + switch (type) { + case "boolean": + value = " $1"; + break; + case "string": + value = " $1"; + break; + case "object": + value = `\n${indent}`; + break; + case "array": + value = `\n${indent}- `; + break; + case "number": + case "integer": + value = " ${1:0}"; + break; + case "null": + value = " ${1:null}"; + break; + case "anyOf": + value = " $1"; + break; + default: + return propertyText; + } + } + } + if (!value || nValueProposals > 1) { + value = " $1"; + } + return resultText + value + separatorAfter; + } + + private getInsertTextForObject( + schema: JSONSchema, + separatorAfter: string, + indent = this.indentation, + insertIndex = 1 + ): InsertText { + let insertText = ""; + if (!schema.properties) { + insertText = `${indent}$${insertIndex++}\n`; + return { insertText, insertIndex }; + } + + const properties = schema.properties; + + if (!properties) { + return { insertText, insertIndex }; + } + + Object.keys(properties).forEach((key: string) => { + const propertySchema = properties[key]; + let type = Array.isArray(propertySchema?.type) ? propertySchema.type[0] : propertySchema?.type; + if (!type) { + if (propertySchema.anyOf) { + type = "anyOf"; + } + if (propertySchema.properties) { + type = "object"; + } + if (propertySchema.items) { + type = "array"; + } + } + if (schema.required && schema.required.indexOf(key) > -1) { + switch (type) { + case "boolean": + case "string": + case "number": + case "integer": + case "anyOf": { + let value = propertySchema.default || propertySchema.const; + if (value) { + if (type === "string") { + value = convertToStringValue(value); + } + insertText += `${indent}${key}: \${${insertIndex++}:${value}}\n`; + } else { + insertText += `${indent}${key}: $${insertIndex++}\n`; + } + break; + } + case "array": + { + const arrayInsertResult = this.getInsertTextForArray( + propertySchema.items, + separatorAfter, + insertIndex++, + indent + ); + const arrayInsertLines = arrayInsertResult.insertText.split("\n"); + let arrayTemplate = arrayInsertResult.insertText; + if (arrayInsertLines.length > 1) { + for (let index = 1; index < arrayInsertLines.length; index++) { + const element = arrayInsertLines[index]; + arrayInsertLines[index] = ` ${element}`; + } + arrayTemplate = arrayInsertLines.join("\n"); + } + insertIndex = arrayInsertResult.insertIndex; + insertText += `${indent}${key}:\n${indent}${this.indentation}- ${arrayTemplate}\n`; + } + break; + case "object": + { + const objectInsertResult = this.getInsertTextForObject( + propertySchema, + separatorAfter, + `${indent}${this.indentation}`, + insertIndex++ + ); + insertIndex = objectInsertResult.insertIndex; + insertText += `${indent}${key}:\n${objectInsertResult.insertText}\n`; + } + break; + } + } else if (propertySchema.default !== undefined) { + switch (type) { + case "boolean": + case "number": + case "integer": + insertText += `${indent}${ + //added quote if key is null + key === "null" ? this.getInsertTextForValue(key, "", "string") : key + }: \${${insertIndex++}:${propertySchema.default}}\n`; + break; + case "string": + insertText += `${indent}${key}: \${${insertIndex++}:${convertToStringValue(propertySchema.default)}}\n`; + break; + case "array": + case "object": + // TODO: support default value for array object + break; + } + } + }); + if (insertText.trim().length === 0) { + insertText = `${indent}$${insertIndex++}\n`; + } + insertText = insertText.trimRight() + separatorAfter; + return { insertText, insertIndex }; + } + + private createTempObjNode(currentWord: string, node: Node, currentDoc: YAMLSubDocument): YAMLMap { + const obj: { [key: string]: unknown } = {}; // Add index signature to allow indexing with a string + obj[currentWord] = null; + const map: YAMLMap = currentDoc.internalDocument.createNode(obj) as YAMLMap; + map.range = node.range; + (map.items[0].key as Node).range = node.range; + (map.items[0].value as Node).range = node.range; + return map; + } + + private getInsertTextForArray( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + schema: any, + separatorAfter: string, + insertIndex = 1, + indent = this.indentation + ): InsertText { + let insertText = ""; + if (!schema) { + insertText = `$${insertIndex++}`; + return { insertText, insertIndex }; + } + let type = Array.isArray(schema.type) ? schema.type[0] : schema.type; + if (!type) { + if (schema.properties) { + type = "object"; + } + if (schema.items) { + type = "array"; + } + } + switch (schema.type) { + case "boolean": + insertText = `\${${insertIndex++}:false}`; + break; + case "number": + case "integer": + insertText = `\${${insertIndex++}:0}`; + break; + case "string": + insertText = `\${${insertIndex++}:""}`; + break; + case "object": + { + const objectInsertResult = this.getInsertTextForObject(schema, separatorAfter, `${indent} `, insertIndex++); + insertText = objectInsertResult.insertText.trimLeft(); + insertIndex = objectInsertResult.insertIndex; + } + break; + } + return { insertText, insertIndex }; + } + + private finalizeParentCompletion(result: CompletionList): void { + const reindexText = (insertTexts: string[]): string[] => { + //modify added props to have unique $x + let max$index = 0; + return insertTexts.map((text) => { + const match = text.match(/\$([0-9]+)|\${[0-9]+:/g); + if (!match) { + return text; + } + const max$indexLocal = match + .map((m) => +m.replace(/\${([0-9]+)[:|]/g, "$1").replace("$", "")) // get numbers form $1 or ${1:...} + .reduce((p, n) => (n > p ? n : p), 0); // find the max one + const reindexedStr = text + .replace(/\$([0-9]+)/g, (s, args) => "$" + (+args + max$index)) // increment each by max$index + .replace(/\${([0-9]+)[:|]/g, (s, args) => "${" + (+args + max$index) + ":"); // increment each by max$index + max$index += max$indexLocal; + return reindexedStr; + }); + }; + + result.items.forEach((completionItem) => { + if (isParentCompletionItem(completionItem) && completionItem.parent && completionItem.parent.insertTexts) { + const indent = completionItem.parent.indent || ""; + + const reindexedTexts = reindexText(completionItem.parent.insertTexts); + + // add indent to each object property and join completion item texts + let insertText = reindexedTexts.join(`\n${indent}`); + + // trim $1 from end of completion + if (insertText.endsWith("$1")) { + insertText = insertText.substring(0, insertText.length - 2); + } + + completionItem.insertText = this.arrayPrefixIndentation + insertText; + if (completionItem.textEdit) { + completionItem.textEdit.newText = completionItem.insertText; + } + // remove $x or use {$x:value} in documentation + const mdText = insertText.replace(/\${[0-9]+[:|](.*)}/g, (s, arg) => arg).replace(/\$([0-9]+)/g, ""); + + const originalDocumentation = completionItem.documentation + ? [completionItem.documentation, "", "----", ""] + : []; + completionItem.documentation = { + kind: MarkupKind.Markdown, + value: [...originalDocumentation, "```yaml", indent + mdText, "```"].join("\n"), + }; + delete completionItem.parent; + } + }); + } + + private addPropertyCompletions( + documentContext: DocumentContext, + doc: YAMLSubDocument, + node: YAMLMap, + originalNode: YamlNode, + separatorAfter: string, + collector: CompletionsCollector, + textBuffer: TextBuffer, + overwriteRange: Range + ): void { + const didCallFromAutoComplete = true; + const matchingSchemas = this.schemaService.getMatchingSchemas(documentContext, -1, didCallFromAutoComplete); + const existingKey = textBuffer.getText(overwriteRange); + const lineContent = textBuffer.getLineContent(overwriteRange.start.line); + const hasOnlyWhitespace = lineContent.trim().length === 0; + const hasColon = lineContent.indexOf(":") !== -1; + // const isInArray = lineContent.trimLeft().indexOf("-") === 0; + const nodeParent = doc.getParent(node); + const matchOriginal = matchingSchemas.find(function (it) { + return it.node.internalNode === originalNode && it.schema.properties; + }); + const oneOfSchema = matchingSchemas + .filter((schema) => schema.schema.oneOf) + .map((oneOfSchema) => oneOfSchema.schema.oneOf)[0]; + let didOneOfSchemaMatches = false; + if (oneOfSchema?.length ?? 0 < matchingSchemas.length) { + oneOfSchema?.forEach((property: JSONSchema, index: number) => { + if ( + !matchingSchemas[index]?.schema.oneOf && + matchingSchemas[index]?.schema.properties === property.properties + ) { + didOneOfSchemaMatches = true; + } + }); + } + for (const schema of matchingSchemas) { + if ( + (schema.node.internalNode === node && !matchOriginal) || + (schema.node.internalNode === originalNode && !hasColon) || + (schema.node.parent?.internalNode === originalNode && !hasColon) + ) { + // this.collectDefaultSnippets(schema.schema, separatorAfter, collector, { + // newLineFirst: false, + // indentFirstObject: false, + // shouldIndentWithTab: isInArray, + // }); + + const schemaProperties = schema.schema.properties; + if (schemaProperties) { + const maxProperties = schema.schema.maxProperties; + if ( + maxProperties === undefined || + node.items === undefined || + node.items.length < maxProperties || + (node.items.length === maxProperties && !hasOnlyWhitespace) + ) { + for (const key in schemaProperties) { + if (Object.prototype.hasOwnProperty.call(schemaProperties, key)) { + const propertySchema = schemaProperties[key]; + + if ( + typeof propertySchema === "object" && + !propertySchema.deprecationMessage && + !propertySchema["doNotSuggest"] + ) { + let identCompensation = ""; + if (node.range && nodeParent && isSeq(nodeParent) && node.items.length <= 1 && !hasOnlyWhitespace) { + // because there is a slash '-' to prevent the properties generated to have the correct + // indent + const sourceText = textBuffer.getText(); + const indexOfSlash = sourceText.lastIndexOf("-", node.range[0] - 1); + if (indexOfSlash >= 0) { + // add one space to compensate the '-' + const overwriteChars = overwriteRange.end.character - overwriteRange.start.character; + identCompensation = " " + sourceText.slice(indexOfSlash + 1, node.range[1] - overwriteChars); + } + } + identCompensation += this.arrayPrefixIndentation; + + // if check that current node has last pair with "null" value and key witch match key from schema, + // and if schema has array definition it add completion item for array item creation + let pair: Pair | undefined; + if ( + propertySchema.type === "array" && + (pair = node.items.find( + (it) => + isScalar(it.key) && + it.key.range && + it.key.value === key && + isScalar(it.value) && + !it.value.value && + textBuffer.getPosition(it.key.range[2]).line === overwriteRange.end.line - 1 + )) && + pair + ) { + if (Array.isArray(propertySchema.items)) { + this.addSchemaValueCompletions( + propertySchema.items[0], + separatorAfter, + collector, + {}, + "property" + ); + } else if (typeof propertySchema.items === "object" && propertySchema.items.type === "object") { + this.addArrayItemValueCompletion(propertySchema.items, separatorAfter, collector); + } + } + + let insertText = key; + if (!key.startsWith(existingKey) || !hasColon) { + insertText = this.getInsertTextForProperty( + key, + propertySchema, + separatorAfter, + identCompensation + this.indentation + ); + } + const isNodeNull = + (isScalar(originalNode) && originalNode.value === null) || + (isMap(originalNode) && originalNode.items.length === 0); + const existsParentCompletion = schema.schema.required?.length ?? 0 > 0; + if (!isNodeNull || !existsParentCompletion) { + collector.add( + { + kind: CompletionItemKind.Property, + label: key, + insertText, + insertTextFormat: InsertTextFormat.Snippet, + documentation: propertySchema.description || "", + }, + didOneOfSchemaMatches + ); + } + // if the prop is required add it also to parent suggestion + if (schema.schema.required?.includes(key)) { + collector.add({ + label: key, + insertText: this.getInsertTextForProperty( + key, + propertySchema, + separatorAfter, + identCompensation + this.indentation + ), + insertTextFormat: InsertTextFormat.Snippet, + documentation: propertySchema.description || "", + parent: { + schema: schema.schema, + indent: identCompensation, + }, + }); + } + } + } + } + } + } + // Error fix + // If this is a array of string/boolean/number + // test: + // - item1 + // it will treated as a property key since `:` has been appended + if (nodeParent && isSeq(nodeParent) && isPrimitiveType(schema.schema)) { + this.addSchemaValueCompletions( + schema.schema, + separatorAfter, + collector, + {}, + "property", + Array.isArray(nodeParent.items) + ); + } + + // if (schema.schema.propertyNames && schema.schema.additionalProperties && schema.schema.type === "object") { + // const propertyNameSchema = asSchema(schema.schema.propertyNames); + // const label = propertyNameSchema.title || "property"; + // collector.add({ + // kind: CompletionItemKind.Property, + // label, + // insertText: "$" + `{1:${label}}: `, + // insertTextFormat: InsertTextFormat.Snippet, + // documentation: + // this.fromMarkup(propertyNameSchema.markdownDescription) || propertyNameSchema.description || "", + // }); + // } + } + + // if (nodeParent && schema.node.internalNode === nodeParent && schema.schema.defaultSnippets) { + // // For some reason the first item in the array needs to be treated differently, otherwise + // // the indentation will not be correct + // if (node.items.length === 1) { + // this.collectDefaultSnippets( + // schema.schema, + // separatorAfter, + // collector, + // { + // newLineFirst: false, + // indentFirstObject: false, + // shouldIndentWithTab: true, + // }, + // 1 + // ); + // } else { + // this.collectDefaultSnippets( + // schema.schema, + // separatorAfter, + // collector, + // { + // newLineFirst: false, + // indentFirstObject: true, + // shouldIndentWithTab: false, + // }, + // 1 + // ); + // } + // } + } + } + + private getValueCompletions( + documentContext: DocumentContext, + doc: YAMLSubDocument, + offset: number, + collector: CompletionsCollector, + types: { [type: string]: boolean }, + node?: YamlNode + ): void { + const schema = this.schemaService.schema; + let parentKey: string | null = null; + + if (node && isScalar(node)) { + node = doc.getParent(node); + } + + if (!node) { + this.addSchemaValueCompletions(schema.schema, "", collector, types, "value"); + return; + } + + if (isPair(node)) { + const valueNode: Node = node.value as Node; + if (valueNode && valueNode.range && offset > valueNode.range[0] + valueNode.range[2]) { + return; // we are past the value node + } + parentKey = isScalar(node.key) ? node.key.value + "" : null; + node = doc.getParent(node); + } + + if (node && (parentKey !== null || isSeq(node))) { + const separatorAfter = ""; + const didCallFromAutoComplete = true; + const matchingSchemas = this.schemaService.getMatchingSchemas(documentContext, -1, didCallFromAutoComplete); + for (const s of matchingSchemas) { + if (s.node.internalNode === node && s.schema) { + if (s.schema.items) { + // this.collectDefaultSnippets(s.schema, separatorAfter, collector, { + // newLineFirst: false, + // indentFirstObject: false, + // shouldIndentWithTab: false, + // }); + if (isSeq(node) && node.items) { + if (Array.isArray(s.schema.items)) { + const index = this.findItemAtOffset(node, offset); + if (index < s.schema.items.length) { + this.addSchemaValueCompletions(s.schema.items[index], separatorAfter, collector, types, "value"); + } + } else if ( + typeof s.schema.items === "object" && + (s.schema.items.type === "object" || isAnyOfAllOfOneOfType(s.schema.items)) + ) { + this.addSchemaValueCompletions(s.schema.items, separatorAfter, collector, types, "value", true); + } else { + this.addSchemaValueCompletions(s.schema.items, separatorAfter, collector, types, "value"); + } + } + } + if (s.schema.properties && parentKey !== null) { + const propertySchema = s.schema.properties[parentKey]; + if (propertySchema) { + this.addSchemaValueCompletions(propertySchema, separatorAfter, collector, types, "value"); + } + } else if (s.schema.additionalProperties) { + this.addSchemaValueCompletions(s.schema.additionalProperties, separatorAfter, collector, types, "value"); + } + } + } + + if (types["boolean"]) { + this.addBooleanValueCompletion(true, separatorAfter, collector); + this.addBooleanValueCompletion(false, separatorAfter, collector); + } + if (types["null"]) { + this.addNullValueCompletion(separatorAfter, collector); + } + } + } + + private addArrayItemValueCompletion( + schema: JSONSchema, + separatorAfter: string, + collector: CompletionsCollector, + index?: number + ): void { + const schemaType = getSchemaTypeName(schema); + const insertText = `- ${this.getInsertTextForObject(schema, separatorAfter).insertText.trimStart()}`; + //append insertText to documentation + const schemaTypeTitle = schemaType ? " type `" + schemaType + "`" : ""; + const schemaDescription = schema.description ? " (" + schema.description + ")" : ""; + const documentation = this.getDocumentationWithMarkdownText( + `Create an item of an array${schemaTypeTitle}${schemaDescription}`, + insertText + ); + collector.add({ + kind: this.getSuggestionKind(schema.type), + label: "- (array item) " + (schemaType || index), + documentation: documentation, + insertText: insertText, + insertTextFormat: InsertTextFormat.Snippet, + }); + } + + private getDocumentationWithMarkdownText(documentation: string, insertText: string): string | MarkupContent { + let res: string | MarkupContent = documentation; + if (this.doesSupportMarkdown()) { + insertText = insertText + .replace(/\${[0-9]+[:|](.*)}/g, (s, arg) => { + return arg; + }) + .replace(/\$([0-9]+)/g, ""); + res = this.fromMarkup(`${documentation}\n \`\`\`\n${insertText}\n\`\`\``) as MarkupContent; + } + return res; + } + + private fromMarkup(markupString: string): MarkupContent | undefined { + if (markupString && this.doesSupportMarkdown()) { + return { + kind: MarkupKind.Markdown, + value: markupString, + }; + } + return undefined; + } + + private doesSupportMarkdown(): boolean { + // Forcing markdown for now + return true; + } + + private getInsertTextForPlainText(text: string): string { + return text.replace(/[\\$}]/g, "\\$&"); // escape $, \ and } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private getInsertTextForValue(value: any, separatorAfter: string, type?: string | string[]): string { + if (value === null) { + return "null"; // replace type null with string 'null' + } + switch (typeof value) { + case "object": { + const indent = this.indentation; + return this.getInsertTemplateForValue(value, indent, { index: 1 }, separatorAfter); + } + case "number": + case "boolean": + return this.getInsertTextForPlainText(value + separatorAfter); + } + type = Array.isArray(type) ? type[0] : type; + if (type === "string") { + value = convertToStringValue(value); + } + return this.getInsertTextForPlainText(value + separatorAfter); + } + + private getInsertTemplateForValue( + value: unknown | [], + indent: string, + navOrder: { index: number }, + separatorAfter: string + ): string { + if (Array.isArray(value)) { + let insertText = "\n"; + for (const arrValue of value) { + insertText += `${indent}- \${${navOrder.index++}:${arrValue}}\n`; + } + return insertText; + } else if (typeof value === "object") { + let insertText = "\n"; + for (const key in value) { + if (Object.prototype.hasOwnProperty.call(value, key)) { + const element = value[key as keyof typeof value]; + insertText += `${indent}\${${navOrder.index++}:${key}}:`; + let valueTemplate; + if (typeof element === "object") { + valueTemplate = `${this.getInsertTemplateForValue( + element, + indent + this.indentation, + navOrder, + separatorAfter + )}`; + } else { + valueTemplate = ` \${${navOrder.index++}:${this.getInsertTextForPlainText(element + separatorAfter)}}\n`; + } + insertText += `${valueTemplate}`; + } + } + return insertText; + } + return this.getInsertTextForPlainText(value + separatorAfter); + } + + private addSchemaValueCompletions( + schema: JSONSchemaRef, + separatorAfter: string, + collector: CompletionsCollector, + types: { [key: string]: boolean }, + completionType: "property" | "value", + isArray?: boolean + ): void { + if (typeof schema === "object") { + this.addEnumValueCompletions(schema, separatorAfter, collector, isArray); + this.addDefaultValueCompletions(schema, separatorAfter, collector); + this.collectTypes(schema, types); + + if (isArray && completionType === "value" && !isAnyOfAllOfOneOfType(schema)) { + // add array only for final types (no anyOf, allOf, oneOf) + this.addArrayItemValueCompletion(schema, separatorAfter, collector); + } + + if (Array.isArray(schema.allOf)) { + schema.allOf.forEach((s) => { + return this.addSchemaValueCompletions(s, separatorAfter, collector, types, completionType, isArray); + }); + } + if (Array.isArray(schema.anyOf)) { + schema.anyOf.forEach((s) => { + return this.addSchemaValueCompletions(s, separatorAfter, collector, types, completionType, isArray); + }); + } + if (Array.isArray(schema.oneOf)) { + schema.oneOf.forEach((s) => { + return this.addSchemaValueCompletions(s, separatorAfter, collector, types, completionType, isArray); + }); + } + } + } + + private collectTypes(schema: JSONSchema, types: { [key: string]: boolean }): void { + if (Array.isArray(schema.enum) || isDefined(schema.const)) { + return; + } + const type = schema.type; + if (Array.isArray(type)) { + type.forEach(function (t) { + return (types[t] = true); + }); + } else if (type) { + types[type] = true; + } + } + + private addDefaultValueCompletions( + schema: JSONSchema, + separatorAfter: string, + collector: CompletionsCollector, + arrayDepth = 0 + ): void { + let hasProposals = false; + if (isDefined(schema.default)) { + let type = schema.type; + let value = schema.default; + for (let i = arrayDepth; i > 0; i--) { + value = [value]; + type = "array"; + } + let label; + if (typeof value == "object") { + label = "Default value"; + } else { + label = value.toString().replace(doubleQuotesEscapeRegExp, '"'); + } + collector.add({ + kind: this.getSuggestionKind(type), + label, + insertText: this.getInsertTextForValue(value, separatorAfter, type), + insertTextFormat: InsertTextFormat.Snippet, + detail: "Default value", + }); + hasProposals = true; + } + // if (Array.isArray(schema.examples)) { + // schema.examples.forEach((example) => { + // let type = schema.type; + // let value = example; + // for (let i = arrayDepth; i > 0; i--) { + // value = [value]; + // type = "array"; + // } + // collector.add({ + // kind: this.getSuggestionKind(type), + // label: this.getLabelForValue(value), + // insertText: this.getInsertTextForValue(value, separatorAfter, type), + // insertTextFormat: InsertTextFormat.Snippet, + // }); + // hasProposals = true; + // }); + // } + // this.collectDefaultSnippets(schema, separatorAfter, collector, { + // newLineFirst: true, + // indentFirstObject: true, + // shouldIndentWithTab: true, + // }); + if (!hasProposals && typeof schema.items === "object" && !Array.isArray(schema.items)) { + this.addDefaultValueCompletions(schema.items, separatorAfter, collector, arrayDepth + 1); + } + } + + private addEnumValueCompletions( + schema: JSONSchema, + separatorAfter: string, + collector: CompletionsCollector, + isArray?: boolean + ): void { + if (isDefined(schema.const) && !isArray) { + collector.add({ + kind: this.getSuggestionKind(schema.type), + label: this.getLabelForValue(schema.const), + insertText: this.getInsertTextForValue(schema.const, separatorAfter, schema.type), + insertTextFormat: InsertTextFormat.Snippet, + documentation: schema.description, + }); + } + if (Array.isArray(schema.enum)) { + for (let i = 0, length = schema.enum.length; i < length; i++) { + const enm = schema.enum[i]; + let documentation = schema.description; + if (schema.enumDescriptions && i < schema.enumDescriptions.length) { + documentation = schema.enumDescriptions[i]; + } + collector.add({ + kind: this.getSuggestionKind(schema.type), + label: this.getLabelForValue(enm), + insertText: this.getInsertTextForValue(enm, separatorAfter, schema.type), + insertTextFormat: InsertTextFormat.Snippet, + documentation: documentation, + }); + } + } + } + + private getLabelForValue(value: unknown): string { + if (value === null) { + return "null"; // return string with 'null' value if schema contains null as possible value + } + if (Array.isArray(value)) { + return JSON.stringify(value); + } + return "" + value; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private getInsertTextForGuessedValue(value: any, separatorAfter: string, type: string): string { + switch (typeof value) { + case "object": + if (value === null) { + return "${1:null}" + separatorAfter; + } + return this.getInsertTextForValue(value, separatorAfter, type); + case "string": { + let snippetValue = JSON.stringify(value); + snippetValue = snippetValue.substr(1, snippetValue.length - 2); // remove quotes + snippetValue = this.getInsertTextForPlainText(snippetValue); // escape \ and } + if (type === "string") { + snippetValue = convertToStringValue(snippetValue); + } + return "${1:" + snippetValue + "}" + separatorAfter; + } + case "number": + case "boolean": + return "${1:" + value + "}" + separatorAfter; + } + return this.getInsertTextForValue(value, separatorAfter, type); + } + + private addBooleanValueCompletion(value: boolean, separatorAfter: string, collector: CompletionsCollector): void { + collector.add({ + kind: this.getSuggestionKind("boolean"), + label: value ? "true" : "false", + insertText: this.getInsertTextForValue(value, separatorAfter, "boolean"), + insertTextFormat: InsertTextFormat.Snippet, + documentation: "", + }); + } + + private addNullValueCompletion(separatorAfter: string, collector: CompletionsCollector): void { + collector.add({ + kind: this.getSuggestionKind("null"), + label: "null", + insertText: "null" + separatorAfter, + insertTextFormat: InsertTextFormat.Snippet, + documentation: "", + }); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private getSuggestionKind(type: any): CompletionItemKind { + if (Array.isArray(type)) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const array = type; + type = array.length > 0 ? array[0] : null; + } + if (!type) { + return CompletionItemKind.Value; + } + switch (type) { + case "string": + return CompletionItemKind.Value; + case "object": + return CompletionItemKind.Module; + case "property": + return CompletionItemKind.Property; + default: + return CompletionItemKind.Value; + } + } + + private findItemAtOffset(seqNode: YAMLSeq, offset: number): number { + for (let i = seqNode.items.length - 1; i >= 0; i--) { + const node = seqNode.items[i]; + if (isNode(node)) { + if (node.range) { + if (offset > node.range[1]) { + return i; + } else if (offset >= node.range[0]) { + return i; + } + } + } + } + + return 0; + } +} + +/** + * simplify `{$1:value}` to `value` + */ +function evaluateTab1Symbol(value: string): string { + return value.replace(/\$\{1:(.*)\}/, "$1"); +} + +function isParentCompletionItem(item: CompletionItemBase): item is CompletionItem { + return "parent" in item; +} + +function convertToStringValue(param: unknown): string { + const isNumberExp = /^\d+$/; + let value: string; + if (typeof param === "string") { + value = param; + } else { + value = "" + param; + } + if (value.length === 0) { + return value; + } + + if (value === "true" || value === "false" || value === "null" || isNumberExp.test(value)) { + return `"${value}"`; + } + + if (value.indexOf('"') !== -1) { + value = value.replace(doubleQuotesEscapeRegExp, '"'); + } + + let doQuote = !isNaN(parseInt(value)) || value.charAt(0) === "@"; + + if (!doQuote) { + // need to quote value if in `foo: bar`, `foo : bar` (mapping) or `foo:` (partial map) format + // but `foo:bar` and `:bar` (colon without white-space after it) are just plain string + let idx = value.indexOf(":", 0); + for (; idx > 0 && idx < value.length; idx = value.indexOf(":", idx + 1)) { + if (idx === value.length - 1) { + // `foo:` (partial map) format + doQuote = true; + break; + } + + // there are only two valid kinds of white-space in yaml: space or tab + // ref: https://yaml.org/spec/1.2.1/#id2775170 + const nextChar = value.charAt(idx + 1); + if (nextChar === "\t" || nextChar === " ") { + doQuote = true; + break; + } + } + } + + if (doQuote) { + value = `"${value}"`; + } + + return value; +} + +export function isPrimitiveType(schema: JSONSchema): boolean { + return schema.type !== "object" && !isAnyOfAllOfOneOfType(schema); +} + +export function isAnyOfAllOfOneOfType(schema: JSONSchema): boolean { + return !!(schema.anyOf || schema.allOf || schema.oneOf); +} + +export function getSchemaTypeName(schema: JSONSchema): string { + const closestTitleWithType = schema.type; + if (schema.title) { + return schema.title; + } + if (schema.$id) { + return getSchemaRefTypeTitle(schema.$id); + } + if (schema.$ref) { + return getSchemaRefTypeTitle(schema.$ref); + } + return ( + (Array.isArray(schema.type) + ? schema.type.join(" | ") + : closestTitleWithType + ? schema.type?.concat("(", schema.title ?? "Unknown", ")") + : schema.type ?? "Unknown") ?? "Unknown" + ); +} + +export function getSchemaRefTypeTitle($ref: string): string { + const match = $ref.match(/^(?:.*\/)?(.*?)(?:\.schema\.json)?$/); + let type = !!match && match[1]; + if (!type) { + type = "typeNotFound"; + console.error(`$ref (${$ref}) not parsed properly`); + } + return type; +} From d73e37751e265d6b186e9a4cc84cffa17c2667ff Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Thu, 22 Feb 2024 15:09:37 +0100 Subject: [PATCH 37/98] Add some unit tests for completion --- .../tests/testHelpers.ts | 16 +++++ .../tests/unit/completion.test.ts | 65 +++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 server/packages/workflow-tests-language-service/tests/testHelpers.ts create mode 100644 server/packages/workflow-tests-language-service/tests/unit/completion.test.ts diff --git a/server/packages/workflow-tests-language-service/tests/testHelpers.ts b/server/packages/workflow-tests-language-service/tests/testHelpers.ts new file mode 100644 index 0000000..3fea191 --- /dev/null +++ b/server/packages/workflow-tests-language-service/tests/testHelpers.ts @@ -0,0 +1,16 @@ +import { TextDocument } from "@gxwf/server-common/src/languageTypes"; +import { getLanguageService, YAMLDocument } from "@gxwf/yaml-language-service/src"; +import { GxWorkflowTestsDocument } from "../src/document"; + +export function toYamlDocument(contents: string): { textDoc: TextDocument; yamlDoc: YAMLDocument } { + const textDoc = TextDocument.create("foo://bar/file.gxwf-tests.yaml", "gxwftests", 0, contents); + + const ls = getLanguageService(); + const yamlDoc = ls.parseYAMLDocument(textDoc) as YAMLDocument; + return { textDoc, yamlDoc }; +} + +export function createGxWorkflowTestsDocument(contents: string): GxWorkflowTestsDocument { + const { textDoc, yamlDoc } = toYamlDocument(contents); + return new GxWorkflowTestsDocument(textDoc, yamlDoc); +} diff --git a/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts b/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts new file mode 100644 index 0000000..292d97d --- /dev/null +++ b/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts @@ -0,0 +1,65 @@ +import { container } from "@gxwf/server-common/src/inversify.config"; +import { CompletionList } from "@gxwf/server-common/src/languageTypes"; +import { WorkflowTestsLanguageServiceContainerModule } from "@gxwf/workflow-tests-language-service/src/inversify.config"; +import "reflect-metadata"; +import { WorkflowTestsSchemaService } from "../../src/schema/service"; +import { YAMLCompletionHelper } from "../../src/services/completion/helper"; +import { TYPES } from "../../src/types"; +import { createGxWorkflowTestsDocument } from "../testHelpers"; + +describe("Workflow Tests Completion Service", () => { + let helper: YAMLCompletionHelper; + beforeAll(() => { + container.load(WorkflowTestsLanguageServiceContainerModule); + const schemaService = container.get(TYPES.WorkflowTestsSchemaService); + helper = new YAMLCompletionHelper(schemaService); + }); + + async function getCompletions( + contents: string, + position: { line: number; character: number } + ): Promise { + const documentContext = createGxWorkflowTestsDocument(contents); + + return await helper.doComplete(documentContext, position); + } + + it("should suggest the `New Workflow Test` when the document is empty", async () => { + const contents = ""; + const position = { line: 0, character: 0 }; + + const completions = await getCompletions(contents, position); + + expect(completions).not.toBeNull(); + expect(completions?.items.length).toBe(1); + + expect(completions?.items[0].labelDetails?.detail).toBe("New Workflow Test"); + expect(completions?.items[0].label).toBe("- doc:"); + }); + + it("should suggest the `New Workflow Test` when the document starts with dash", async () => { + const contents = "-"; + const position = { line: 0, character: 1 }; + + const completions = await getCompletions(contents, position); + + expect(completions).not.toBeNull(); + expect(completions?.items.length).toBe(1); + + expect(completions?.items[0].labelDetails?.detail).toBe("New Workflow Test"); + expect(completions?.items[0].label).toBe("- doc:"); + }); + + it("should suggest the `New Workflow Test` when the position is at the beginning of a new line", async () => { + const contents = "- doc:\n\n\n"; + const position = { line: 1, character: 0 }; + + const completions = await getCompletions(contents, position); + + expect(completions).not.toBeNull(); + expect(completions?.items.length).toBe(1); + + expect(completions?.items[0].labelDetails?.detail).toBe("New Workflow Test"); + expect(completions?.items[0].label).toBe("- doc:"); + }); +}); From c27f7fda1c49c211ef700dc8ef48a0423cd97e4d Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Thu, 22 Feb 2024 18:46:29 +0100 Subject: [PATCH 38/98] Refactor isVirtualWorkspace --- client/src/common/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/common/utils.ts b/client/src/common/utils.ts index aa1d3fb..160ef37 100644 --- a/client/src/common/utils.ts +++ b/client/src/common/utils.ts @@ -5,7 +5,7 @@ import { OutputChannel, Uri, workspace } from "vscode"; * @returns true if the workspace is not mounted on a regular filesystem. */ export function isVirtualWorkspace(): boolean { - return workspace.workspaceFolders && workspace.workspaceFolders.every((f) => f.uri.scheme !== "file"); + return (workspace.workspaceFolders ?? []).every((f) => f.uri.scheme !== "file"); } /** From 540f784ffaab7dddd4edb24e354b7d5aef9e4477 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Thu, 22 Feb 2024 18:49:44 +0100 Subject: [PATCH 39/98] Associate server instance with services --- server/packages/server-common/src/languageTypes.ts | 8 ++++++++ server/packages/server-common/src/server.ts | 10 ++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/server/packages/server-common/src/languageTypes.ts b/server/packages/server-common/src/languageTypes.ts index 7331234..f3f2506 100644 --- a/server/packages/server-common/src/languageTypes.ts +++ b/server/packages/server-common/src/languageTypes.ts @@ -169,6 +169,8 @@ export interface LanguageService { * An optional validation profile can be used to provide additional custom diagnostics. */ validate(documentContext: T, useProfile?: ValidationProfile): Promise; + + setServer(server: GalaxyWorkflowLanguageServer): void; } /** @@ -179,6 +181,8 @@ export interface LanguageService { export abstract class LanguageServiceBase implements LanguageService { constructor(@unmanaged() public readonly languageId: string) {} + protected server?: GalaxyWorkflowLanguageServer; + public abstract parseDocument(document: TextDocument): T; public abstract format(document: TextDocument, range: Range, options: FormattingOptions): TextEdit[]; public abstract doHover(documentContext: T, position: Position): Promise; @@ -201,6 +205,10 @@ export abstract class LanguageServiceBase implements } return diagnostics; } + + public setServer(server: GalaxyWorkflowLanguageServer): void { + this.server = server; + } } export interface WorkflowLanguageService extends LanguageService {} diff --git a/server/packages/server-common/src/server.ts b/server/packages/server-common/src/server.ts index 2df10d9..704a1fa 100644 --- a/server/packages/server-common/src/server.ts +++ b/server/packages/server-common/src/server.ts @@ -7,12 +7,12 @@ import { WorkspaceFolder, } from "vscode-languageserver"; import { - TextDocument, - LanguageService, DocumentContext, DocumentsCache, - TYPES, GalaxyWorkflowLanguageServer, + LanguageService, + TYPES, + TextDocument, WorkflowLanguageService, WorkflowTestsLanguageService, } from "./languageTypes"; @@ -21,10 +21,10 @@ import { HoverProvider } from "./providers/hover/hoverProvider"; import { SymbolsProvider } from "./providers/symbolsProvider"; import { CleanWorkflowService } from "./services/cleanWorkflow"; // import { DebugHoverContentContributor } from "./providers/hover/debugHoverContentContributor"; +import { inject, injectable } from "inversify"; import { ConfigService } from "./configService"; import { CompletionProvider } from "./providers/completionProvider"; import { ValidationProfiles } from "./providers/validation/profiles"; -import { injectable, inject } from "inversify"; @injectable() export class GalaxyWorkflowLanguageServerImpl implements GalaxyWorkflowLanguageServer { @@ -41,6 +41,8 @@ export class GalaxyWorkflowLanguageServerImpl implements GalaxyWorkflowLanguageS ) { this.languageServiceMapper.set(workflowLanguageService.languageId, workflowLanguageService); this.languageServiceMapper.set(workflowTestsLanguageService.languageId, workflowTestsLanguageService); + workflowLanguageService.setServer(this); + workflowTestsLanguageService.setServer(this); // Track open, change and close text document events this.trackDocumentChanges(connection); From 33e280bb3694cedaec45dc557f8123b6b0cc913f Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sun, 25 Feb 2024 00:49:38 +0100 Subject: [PATCH 40/98] Add isWorkflowInputType function to utils.ts --- server/packages/server-common/src/utils.ts | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 server/packages/server-common/src/utils.ts diff --git a/server/packages/server-common/src/utils.ts b/server/packages/server-common/src/utils.ts new file mode 100644 index 0000000..4aded71 --- /dev/null +++ b/server/packages/server-common/src/utils.ts @@ -0,0 +1,5 @@ +import { WorkflowInputType } from "./services/requestsDefinitions"; + +export function isWorkflowInputType(input: string): input is WorkflowInputType { + return input === "data_input" || input === "data_collection_input"; +} From 7f3fddd81365a10265ffeee93b7e2a9b38ce6b75 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sun, 25 Feb 2024 01:28:45 +0100 Subject: [PATCH 41/98] Update workflow tests document to include workflow inputs --- client/src/common/index.ts | 3 ++ client/src/common/requestsDefinitions.ts | 17 +++++++ client/src/common/utils.ts | 24 ++++++++++ client/src/requests/gxworkflows.ts | 41 +++++++++++++++++ server/packages/server-common/src/index.ts | 0 .../server-common/src/inversify.config.ts | 6 ++- .../server-common/src/languageTypes.ts | 7 +++ .../src/models/workflowDocument.ts | 26 ++++++++++- .../src/models/workflowTestsDocument.ts | 12 ++++- .../src/providers/workflowDataProvider.ts | 44 +++++++++++++++++++ server/packages/server-common/src/server.ts | 2 + .../src/services/requestsDefinitions.ts | 17 +++++++ .../src/document.ts | 16 +++---- .../src/languageService.ts | 2 +- 14 files changed, 204 insertions(+), 13 deletions(-) create mode 100644 client/src/requests/gxworkflows.ts delete mode 100644 server/packages/server-common/src/index.ts create mode 100644 server/packages/server-common/src/providers/workflowDataProvider.ts diff --git a/client/src/common/index.ts b/client/src/common/index.ts index d57df53..b88c346 100644 --- a/client/src/common/index.ts +++ b/client/src/common/index.ts @@ -5,6 +5,7 @@ import { CleanWorkflowDocumentProvider } from "../providers/cleanWorkflowDocumen import { CleanWorkflowProvider } from "../providers/cleanWorkflowProvider"; import { GitProvider } from "../providers/git"; import { BuiltinGitProvider } from "../providers/git/gitProvider"; +import { setupRequests } from "../requests/gxworkflows"; export function buildBasicLanguageClientOptions(documentSelector: DocumentSelector): LanguageClientOptions { // Options to control the language client @@ -30,6 +31,8 @@ export function initExtension( // Setup gxformat2 language features startLanguageClient(context, gxFormat2Client); + + setupRequests(context, nativeClient, gxFormat2Client); } function initGitProvider(context: ExtensionContext): BuiltinGitProvider { diff --git a/client/src/common/requestsDefinitions.ts b/client/src/common/requestsDefinitions.ts index d3f9eb1..c96b8ce 100644 --- a/client/src/common/requestsDefinitions.ts +++ b/client/src/common/requestsDefinitions.ts @@ -6,6 +6,7 @@ import { RequestType } from "vscode-languageclient"; export namespace LSRequestIdentifiers { export const CLEAN_WORKFLOW_DOCUMENT = "galaxy-workflows-ls.cleanWorkflowDocument"; export const CLEAN_WORKFLOW_CONTENTS = "galaxy-workflows-ls.cleanWorkflowContents"; + export const GET_WORKFLOW_INPUTS = "galaxy-workflows-ls.getWorkflowInputs"; } export interface CleanWorkflowDocumentParams { @@ -35,3 +36,19 @@ export namespace CleanWorkflowContentsRequest { LSRequestIdentifiers.CLEAN_WORKFLOW_CONTENTS ); } + +export interface GetWorkflowInputsParams { + uri: string; +} + +export type WorkflowInputType = "data_input" | "data_collection_input"; + +export interface WorkflowInput { + name: string; + type: WorkflowInputType; + description: string; +} + +export interface GetWorkflowInputsResult { + inputs: WorkflowInput[]; +} diff --git a/client/src/common/utils.ts b/client/src/common/utils.ts index 160ef37..3c568aa 100644 --- a/client/src/common/utils.ts +++ b/client/src/common/utils.ts @@ -45,3 +45,27 @@ export function debugPrintCommandArgs(command: string, args: unknown[], outputCh } outputChannel.appendLine(`---\n`); } + +export function isWorkflowTestsDocument(uri: Uri): boolean { + return uri.path.endsWith("-test.yml"); +} + +export function isNativeWorkflowDocument(uri: Uri): boolean { + return uri.path.endsWith(".ga"); +} + +export async function getAssociatedWorkflowUriFromTestsUri(workflowTestsDocumentUri: Uri): Promise { + const format2WorkflowUri = Uri.parse(workflowTestsDocumentUri.toString().replace("-test.yml", ".yml")); + try { + await workspace.fs.stat(format2WorkflowUri); + return format2WorkflowUri; + } catch { + const nativeWorkflowUri = Uri.parse(workflowTestsDocumentUri.toString().replace("-test.yml", ".ga")); + try { + await workspace.fs.stat(nativeWorkflowUri); + return nativeWorkflowUri; + } catch { + return undefined; + } + } +} diff --git a/client/src/requests/gxworkflows.ts b/client/src/requests/gxworkflows.ts new file mode 100644 index 0000000..92bc5d4 --- /dev/null +++ b/client/src/requests/gxworkflows.ts @@ -0,0 +1,41 @@ +import { ExtensionContext, Uri, workspace } from "vscode"; +import { BaseLanguageClient } from "vscode-languageclient"; +import { GetWorkflowInputsParams, GetWorkflowInputsResult, LSRequestIdentifiers } from "../common/requestsDefinitions"; +import { + getAssociatedWorkflowUriFromTestsUri, + isNativeWorkflowDocument, + isWorkflowTestsDocument, +} from "../common/utils"; + +export function setupRequests( + context: ExtensionContext, + nativeWorkflowClient: BaseLanguageClient, + gxFormat2Client: BaseLanguageClient +): void { + context.subscriptions.push( + gxFormat2Client.onRequest(LSRequestIdentifiers.GET_WORKFLOW_INPUTS, async (params: GetWorkflowInputsParams) => { + let targetUri: Uri | undefined = Uri.parse(params.uri); + if (isWorkflowTestsDocument(targetUri)) { + // If the file is a test file, we need to find the associated workflow file + targetUri = await getAssociatedWorkflowUriFromTestsUri(targetUri); + } + if (!targetUri) { + console.debug("No associated workflow file found for:", params.uri); + return { inputs: [] }; + } + // Open the file to include it in the document cache + await workspace.openTextDocument(targetUri); + + let languageClient = gxFormat2Client; + if (isNativeWorkflowDocument(targetUri)) { + languageClient = nativeWorkflowClient; + } + const requestParams: GetWorkflowInputsParams = { uri: targetUri.toString() }; + const result = await languageClient.sendRequest( + LSRequestIdentifiers.GET_WORKFLOW_INPUTS, + requestParams + ); + return result; + }) + ); +} diff --git a/server/packages/server-common/src/index.ts b/server/packages/server-common/src/index.ts deleted file mode 100644 index e69de29..0000000 diff --git a/server/packages/server-common/src/inversify.config.ts b/server/packages/server-common/src/inversify.config.ts index 2f80697..fce9f36 100644 --- a/server/packages/server-common/src/inversify.config.ts +++ b/server/packages/server-common/src/inversify.config.ts @@ -1,10 +1,12 @@ import { Container } from "inversify"; -import { TYPES, DocumentsCache } from "./languageTypes"; -import { DocumentsCacheImpl } from "./models/documentsCache"; import { ConfigService, ConfigServiceImpl } from "./configService"; +import { DocumentsCache, TYPES, WorkflowDataProvider } from "./languageTypes"; +import { DocumentsCacheImpl } from "./models/documentsCache"; +import { WorkflowDataProviderImpl } from "./providers/workflowDataProvider"; const container = new Container(); container.bind(TYPES.ConfigService).to(ConfigServiceImpl).inSingletonScope(); container.bind(TYPES.DocumentsCache).to(DocumentsCacheImpl).inSingletonScope(); +container.bind(TYPES.WorkflowDataProvider).to(WorkflowDataProviderImpl).inSingletonScope(); export { container }; diff --git a/server/packages/server-common/src/languageTypes.ts b/server/packages/server-common/src/languageTypes.ts index f3f2506..4361a92 100644 --- a/server/packages/server-common/src/languageTypes.ts +++ b/server/packages/server-common/src/languageTypes.ts @@ -54,6 +54,7 @@ import { ASTNodeManager } from "./ast/nodeManager"; import { ConfigService } from "./configService"; import { WorkflowDocument } from "./models/workflowDocument"; import { WorkflowTestsDocument } from "./models/workflowTestsDocument"; +import { GetWorkflowInputsResult } from "./services/requestsDefinitions"; export { CodeAction, @@ -218,6 +219,7 @@ export interface GalaxyWorkflowLanguageServer { connection: Connection; documentsCache: DocumentsCache; configService: ConfigService; + workflowDataProvider: WorkflowDataProvider; start(): void; getLanguageServiceById(languageId: string): LanguageService; } @@ -232,6 +234,10 @@ export interface DocumentsCache { get schemesToSkip(): string[]; } +export interface WorkflowDataProvider { + getWorkflowInputs(workflowDocumentUri: string): Promise; +} + const TYPES = { DocumentsCache: Symbol.for("DocumentsCache"), ConfigService: Symbol.for("ConfigService"), @@ -239,6 +245,7 @@ const TYPES = { WorkflowLanguageService: Symbol.for("WorkflowLanguageService"), WorkflowTestsLanguageService: Symbol.for("WorkflowTestsLanguageService"), GalaxyWorkflowLanguageServer: Symbol.for("GalaxyWorkflowLanguageServer"), + WorkflowDataProvider: Symbol.for("WorkflowDataProvider"), }; export { TYPES }; diff --git a/server/packages/server-common/src/models/workflowDocument.ts b/server/packages/server-common/src/models/workflowDocument.ts index 6607556..348a2dc 100644 --- a/server/packages/server-common/src/models/workflowDocument.ts +++ b/server/packages/server-common/src/models/workflowDocument.ts @@ -1,8 +1,32 @@ +import { GetWorkflowInputsResult } from "../services/requestsDefinitions"; +import { isWorkflowInputType } from "../utils"; import { DocumentBase } from "./document"; /** * This class abstracts the common logic of workflow documents. */ export abstract class WorkflowDocument extends DocumentBase { - // TODO: add workflow document specific logic + /** + * Returns the inputs of the workflow. + */ + public getWorkflowInputs(): GetWorkflowInputsResult { + const result: GetWorkflowInputsResult = { inputs: [] }; + const stepNodes = this.nodeManager.getStepNodes(); + stepNodes.forEach((step) => { + const stepTypeNode = step.properties.find((property) => property.keyNode.value === "type"); + const stepTypeValue = String(stepTypeNode?.valueNode?.value); + if (isWorkflowInputType(stepTypeValue)) { + const labelNode = step.properties.find((property) => property.keyNode.value === "label"); + const labelValue = String(labelNode?.valueNode?.value); + const annotationNode = step.properties.find((property) => property.keyNode.value === "annotation"); + const annotationValue = String(annotationNode?.valueNode?.value); + result.inputs.push({ + name: labelValue ?? "UNKNOWN", + description: annotationValue, + type: stepTypeValue, + }); + } + }); + return result; + } } diff --git a/server/packages/server-common/src/models/workflowTestsDocument.ts b/server/packages/server-common/src/models/workflowTestsDocument.ts index 0260b0b..99e4738 100644 --- a/server/packages/server-common/src/models/workflowTestsDocument.ts +++ b/server/packages/server-common/src/models/workflowTestsDocument.ts @@ -1,8 +1,18 @@ +import { WorkflowDataProvider } from "../languageTypes"; +import { WorkflowInput } from "../services/requestsDefinitions"; import { DocumentBase } from "./document"; /** * This class contains information about a document containing workflow tests. */ export abstract class WorkflowTestsDocument extends DocumentBase { - // TODO: implement workflow test document specific logic + protected abstract readonly workflowDataProvider?: WorkflowDataProvider; + + /** + * Returns the inputs of the associated workflow if available or an empty array otherwise. + */ + public async getWorkflowInputs(): Promise { + const result = await this.workflowDataProvider?.getWorkflowInputs(this.textDocument.uri); + return result?.inputs ?? []; + } } diff --git a/server/packages/server-common/src/providers/workflowDataProvider.ts b/server/packages/server-common/src/providers/workflowDataProvider.ts new file mode 100644 index 0000000..0176ab4 --- /dev/null +++ b/server/packages/server-common/src/providers/workflowDataProvider.ts @@ -0,0 +1,44 @@ +import { inject, injectable } from "inversify"; +import { Connection } from "vscode-languageserver"; +import { DocumentsCache, TYPES, WorkflowDataProvider, WorkflowDocument } from "../languageTypes"; +import { + GetWorkflowInputsParams, + GetWorkflowInputsResult, + LSRequestIdentifiers, +} from "../services/requestsDefinitions"; + +@injectable() +export class WorkflowDataProviderImpl implements WorkflowDataProvider { + constructor( + @inject(TYPES.Connection) public readonly connection: Connection, + @inject(TYPES.DocumentsCache) public readonly documentsCache: DocumentsCache + ) { + // Register the request handler for getting workflow inputs + connection.onRequest(LSRequestIdentifiers.GET_WORKFLOW_INPUTS, (params: GetWorkflowInputsParams) => { + // if we receive a request to get workflow inputs, we can expect that the workflow document is in the cache + // because the client should have opened it before sending the request. + const workflowDocument = this.getWorkflowDocument(params.uri); + return workflowDocument ? workflowDocument.getWorkflowInputs() : { inputs: [] }; + }); + } + + /** + * Returns the inputs of the associated workflow given the URI of the workflow document or the associated test document. + * @param workflowDocumentUri The URI of the workflow document or the associated test document. + * @returns The inputs of the associated workflow. + */ + public async getWorkflowInputs(workflowDocumentUri: string): Promise { + const params: GetWorkflowInputsParams = { + uri: workflowDocumentUri.toString(), + }; + // The URI could be of the associated test document. Since we don't know which kind of workflow document + // it is (.ga or format2), we need to ask the client to get the workflow inputs. + // The client will then delegate the request to the appropriate language server after making sure + // that the workflow document is in the cache by opening it. + return this.connection.sendRequest(LSRequestIdentifiers.GET_WORKFLOW_INPUTS, params); + } + + private getWorkflowDocument(uri: string): WorkflowDocument | undefined { + return this.documentsCache.get(uri) as WorkflowDocument; + } +} diff --git a/server/packages/server-common/src/server.ts b/server/packages/server-common/src/server.ts index 704a1fa..a52aac4 100644 --- a/server/packages/server-common/src/server.ts +++ b/server/packages/server-common/src/server.ts @@ -13,6 +13,7 @@ import { LanguageService, TYPES, TextDocument, + WorkflowDataProvider, WorkflowLanguageService, WorkflowTestsLanguageService, } from "./languageTypes"; @@ -36,6 +37,7 @@ export class GalaxyWorkflowLanguageServerImpl implements GalaxyWorkflowLanguageS @inject(TYPES.Connection) public readonly connection: Connection, @inject(TYPES.DocumentsCache) public readonly documentsCache: DocumentsCache, @inject(TYPES.ConfigService) public readonly configService: ConfigService, + @inject(TYPES.WorkflowDataProvider) public readonly workflowDataProvider: WorkflowDataProvider, @inject(TYPES.WorkflowLanguageService) public readonly workflowLanguageService: WorkflowLanguageService, @inject(TYPES.WorkflowTestsLanguageService) workflowTestsLanguageService: WorkflowTestsLanguageService ) { diff --git a/server/packages/server-common/src/services/requestsDefinitions.ts b/server/packages/server-common/src/services/requestsDefinitions.ts index 190d968..6163bd2 100644 --- a/server/packages/server-common/src/services/requestsDefinitions.ts +++ b/server/packages/server-common/src/services/requestsDefinitions.ts @@ -6,6 +6,7 @@ import { RequestType } from "vscode-languageserver"; export namespace LSRequestIdentifiers { export const CLEAN_WORKFLOW_DOCUMENT = "galaxy-workflows-ls.cleanWorkflowDocument"; export const CLEAN_WORKFLOW_CONTENTS = "galaxy-workflows-ls.cleanWorkflowContents"; + export const GET_WORKFLOW_INPUTS = "galaxy-workflows-ls.getWorkflowInputs"; } export interface CleanWorkflowDocumentParams { @@ -35,3 +36,19 @@ export namespace CleanWorkflowContentsRequest { LSRequestIdentifiers.CLEAN_WORKFLOW_CONTENTS ); } + +export interface GetWorkflowInputsParams { + uri: string; +} + +export type WorkflowInputType = "data_input" | "data_collection_input"; + +export interface WorkflowInput { + name: string; + type: WorkflowInputType; + description: string; +} + +export interface GetWorkflowInputsResult { + inputs: WorkflowInput[]; +} diff --git a/server/packages/workflow-tests-language-service/src/document.ts b/server/packages/workflow-tests-language-service/src/document.ts index 2fe3c09..c62688f 100644 --- a/server/packages/workflow-tests-language-service/src/document.ts +++ b/server/packages/workflow-tests-language-service/src/document.ts @@ -1,18 +1,18 @@ -import { TextDocument, WorkflowTestsDocument } from "@gxwf/server-common/src/languageTypes"; +import { TextDocument, WorkflowDataProvider, WorkflowTestsDocument } from "@gxwf/server-common/src/languageTypes"; import { YAMLDocument } from "@gxwf/yaml-language-service/src"; /** * This class represents (YAML) document containing tests definitions for a Galaxy workflow. */ export class GxWorkflowTestsDocument extends WorkflowTestsDocument { - private _yamlDocument: YAMLDocument; + protected readonly workflowDataProvider?: WorkflowDataProvider; - constructor(textDocument: TextDocument, yamlDocument: YAMLDocument) { + constructor( + textDocument: TextDocument, + public readonly yamlDocument: YAMLDocument, + workflowDataProvider?: WorkflowDataProvider + ) { super(textDocument, yamlDocument); - this._yamlDocument = yamlDocument; - } - - public get yamlDocument(): YAMLDocument { - return this._yamlDocument; + this.workflowDataProvider = workflowDataProvider; } } diff --git a/server/packages/workflow-tests-language-service/src/languageService.ts b/server/packages/workflow-tests-language-service/src/languageService.ts index c3a2133..98ffc4f 100644 --- a/server/packages/workflow-tests-language-service/src/languageService.ts +++ b/server/packages/workflow-tests-language-service/src/languageService.ts @@ -39,7 +39,7 @@ export class GxWorkflowTestsLanguageServiceImpl extends LanguageServiceBase Date: Sat, 27 Apr 2024 14:05:40 +0200 Subject: [PATCH 42/98] Fix tests config --- package.json | 2 +- tsconfig.json | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 39b4418..cf3ade4 100644 --- a/package.json +++ b/package.json @@ -222,7 +222,7 @@ "watch": "concurrently --kill-others \"npm run watch-server\" \"npm run watch-client\"", "watch-server": "cd server && npm run watch", "watch-client": "cd client && npm run watch", - "test": "jest", + "test": "npm run test-client && npm run test-server", "test-client": "cd client && npm test", "test-server": "cd server && npm test", "test-compile": "tsc --project ./client --outDir client/out", diff --git a/tsconfig.json b/tsconfig.json index 729c0c4..0d25d73 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,8 @@ "target": "es2019", "lib": ["ES2019", "WebWorker"], "module": "commonjs", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, "moduleResolution": "node", "resolveJsonModule": true, "esModuleInterop": true, From d46859794d0ead6a085611f6551b20f3ae51e52b Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sat, 27 Apr 2024 14:13:18 +0200 Subject: [PATCH 43/98] Add property completions for inputs of class File --- .../src/services/completion/helper.ts | 120 +++++++++++++----- 1 file changed, 90 insertions(+), 30 deletions(-) diff --git a/server/packages/workflow-tests-language-service/src/services/completion/helper.ts b/server/packages/workflow-tests-language-service/src/services/completion/helper.ts index 0d0df99..5bb8e8b 100644 --- a/server/packages/workflow-tests-language-service/src/services/completion/helper.ts +++ b/server/packages/workflow-tests-language-service/src/services/completion/helper.ts @@ -22,6 +22,7 @@ import { Position, Range, TextEdit, +WorkflowTestsDocument, } from "@gxwf/server-common/src/languageTypes"; import { YamlNode } from "@gxwf/yaml-language-service/src/parser/astTypes"; import { YAMLSubDocument } from "@gxwf/yaml-language-service/src/parser/yamlDocument"; @@ -116,7 +117,7 @@ ${this.indentation}${this.indentation}$0 if (!node) { result.items.push(this.newTestSnippetCompletion); - return result; + return Promise.resolve(result); } const currentWord = textBuffer.getCurrentWord(offset); @@ -128,7 +129,7 @@ ${this.indentation}${this.indentation}$0 // we suggest a new test snippet if (lineContent.match(/^(\n|-|- {1}|-\n)$/)) { result.items.push(this.newTestSnippetCompletion); - return result; + return Promise.resolve(result); } let overwriteRange: Range | null = null; @@ -188,19 +189,11 @@ ${this.indentation}${this.indentation}$0 (item: CompletionItem) => item.parent?.schema === schema && item.kind === parentCompletionKind ); - if ( - !parentCompletion || - !parentCompletion.parent || - !parentCompletion.parent.insertTexts || - !completionItem.insertText - ) { - return; + if (!completionItem.insertText) { + completionItem.insertText = completionItem.label; } - if (parentCompletion.parent.insertTexts.includes(completionItem.insertText)) { - // already exists in the parent - return; - } else if (!parentCompletion) { + if (!parentCompletion) { // create a new parent parentCompletion = { ...completionItem, @@ -520,7 +513,7 @@ ${this.indentation}${this.indentation}$0 } } - this.addPropertyCompletions( + await this.addPropertyCompletions( documentContext, currentDoc, node, @@ -531,6 +524,10 @@ ${this.indentation}${this.indentation}$0 overwriteRange ); + if (collector.getNumberOfProposals() > 0) { + return collector.result; + } + if (!schema && currentWord.length > 0 && text.charAt(offset - currentWord.length - 1) !== '"') { collector.add({ kind: CompletionItemKind.Property, @@ -643,10 +640,6 @@ ${this.indentation}${this.indentation}$0 } } - if (!type) { - return value; - } - if (propertySchema.enum) { if (!value && propertySchema.enum.length === 1) { value = " " + this.getInsertTextForGuessedValue(propertySchema.enum[0], "", type); @@ -929,7 +922,7 @@ ${this.indentation}${this.indentation}$0 }); } - private addPropertyCompletions( + private async addPropertyCompletions( documentContext: DocumentContext, doc: YAMLSubDocument, node: YAMLMap, @@ -938,9 +931,10 @@ ${this.indentation}${this.indentation}$0 collector: CompletionsCollector, textBuffer: TextBuffer, overwriteRange: Range - ): void { + ): Promise { const didCallFromAutoComplete = true; - const matchingSchemas = this.schemaService.getMatchingSchemas(documentContext, -1, didCallFromAutoComplete); + const nodeOffset = textBuffer.getOffsetAt(overwriteRange.start); + let matchingSchemas = this.schemaService.getMatchingSchemas(documentContext, nodeOffset, didCallFromAutoComplete); const existingKey = textBuffer.getText(overwriteRange); const lineContent = textBuffer.getLineContent(overwriteRange.start.line); const hasOnlyWhitespace = lineContent.trim().length === 0; @@ -950,6 +944,60 @@ ${this.indentation}${this.indentation}$0 const matchOriginal = matchingSchemas.find(function (it) { return it.node.internalNode === originalNode && it.schema.properties; }); + + // if the parent is the `job` key, then we need to add the `job` properties from the document context + if (nodeParent && isPair(nodeParent) && isScalar(nodeParent.key) && nodeParent.key.value === "job") { + const testDocument = documentContext as WorkflowTestsDocument; + const workflowInputs = await testDocument.getWorkflowInputs(); + // create a completion item for the inputs excluding the ones already defined + workflowInputs.forEach((input) => { + collector.add({ + kind: CompletionItemKind.Property, + label: input.name, + insertText: `${input.name}:`, + insertTextFormat: InsertTextFormat.Snippet, + documentation: this.fromMarkup(input.description), + }); + }); + return; + } + + //If the parent is a workflow input, then we need to add the properties from the document context + if (nodeParent && isPair(nodeParent) && isScalar(nodeParent.key)) { + const testDocument = documentContext as WorkflowTestsDocument; + const workflowInputs = await testDocument.getWorkflowInputs(); + const nodeParentKey = nodeParent.key.value; + const matchingWorkflowInput = workflowInputs.find((input) => input.name === nodeParentKey); + if (matchingWorkflowInput) { + const type = matchingWorkflowInput.type; + switch (type) { + case "data_input": + if (node.items.length === 1 && isScalar(node.items[0].key) && node.items[0].key.value === "") { + collector.add( + { + kind: CompletionItemKind.Property, + label: "class", + insertText: "class: File", + insertTextFormat: InsertTextFormat.Snippet, + }, + false + ); + return; + } + + matchingSchemas = matchingSchemas.filter( + (schema) => + schema.schema.title && ["LocationFile", "PathFile", "CompositeDataFile"].includes(schema.schema.title) + ); + break; + case "data_collection_input": + // The valid schema is "Collection" + matchingSchemas = matchingSchemas.filter((schema) => schema.schema.title === "Collection"); + break; + } + } + } + const oneOfSchema = matchingSchemas .filter((schema) => schema.schema.oneOf) .map((oneOfSchema) => oneOfSchema.schema.oneOf)[0]; @@ -964,18 +1012,30 @@ ${this.indentation}${this.indentation}$0 } }); } + + function hasSameRange(nodeA: YAMLMap, nodeB: YAMLMap): boolean { + // loop through ranges of each node and compare them + if (nodeA.range && nodeB.range && nodeA.range.length === nodeB.range.length) { + for (let i = 0; i < nodeA.range.length; i++) { + if (nodeA.range[i] === nodeB.range[i]) { + continue; + } else { + return false; + } + } + return true; + } + return false; + } + for (const schema of matchingSchemas) { + const internalNode = schema.node.internalNode as YAMLMap; if ( - (schema.node.internalNode === node && !matchOriginal) || - (schema.node.internalNode === originalNode && !hasColon) || + // (internalNode.range === node.range && !matchOriginal) || + (hasSameRange(internalNode, node) && !matchOriginal) || + (internalNode === originalNode && !hasColon) || (schema.node.parent?.internalNode === originalNode && !hasColon) ) { - // this.collectDefaultSnippets(schema.schema, separatorAfter, collector, { - // newLineFirst: false, - // indentFirstObject: false, - // shouldIndentWithTab: isInArray, - // }); - const schemaProperties = schema.schema.properties; if (schemaProperties) { const maxProperties = schema.schema.maxProperties; @@ -1489,7 +1549,7 @@ ${this.indentation}${this.indentation}$0 } // eslint-disable-next-line @typescript-eslint/no-explicit-any - private getInsertTextForGuessedValue(value: any, separatorAfter: string, type: string): string { + private getInsertTextForGuessedValue(value: any, separatorAfter: string, type?: string): string { switch (typeof value) { case "object": if (value === null) { From 1084cf44a5a05ab52ea0c786c03d5abe5c08e0c5 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sun, 28 Apr 2024 12:10:04 +0200 Subject: [PATCH 44/98] Add more completion tests --- .../tests/unit/completion.test.ts | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts b/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts index 292d97d..444218c 100644 --- a/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts +++ b/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts @@ -62,4 +62,54 @@ describe("Workflow Tests Completion Service", () => { expect(completions?.items[0].labelDetails?.detail).toBe("New Workflow Test"); expect(completions?.items[0].label).toBe("- doc:"); }); + + it("should suggest the `job` and `outputs` entries when the position is at the same level as `doc`", async () => { + const template = ` +- doc: The docs + $`; + const { contents, position } = parseTemplate(template); + + const completions = await getCompletions(contents, position); + + expect(completions).not.toBeNull(); + const jobCompletion = completions?.items.find((item) => item.label === "job"); + const outputCompletion = completions?.items.find((item) => item.label === "outputs"); + expect(jobCompletion).not.toBeUndefined(); + expect(outputCompletion).not.toBeUndefined(); + }); + + it("should suggest the `job` entry as first suggestion when the position is at the Test definition level and starts with a `j`", async () => { + const template = ` +- doc: The docs + j$`; + const { contents, position } = parseTemplate(template); + + const completions = await getCompletions(contents, position); + + expect(completions).not.toBeNull(); + const jobCompletion = completions!.items[0]!; + expect(jobCompletion.label).toBe("job"); + }); }); + +function parseTemplate( + template: string, + char?: string +): { contents: string; position: { line: number; character: number } } { + if (!char) { + char = "$"; + } + let position = { line: 0, character: 0 }; + const contents = template.replace(char, ""); + + const lines = template.split("\n"); + for (let i = 0; i < lines.length; i++) { + const character = lines[i].indexOf(char); + if (character !== -1) { + position = { line: i, character }; + return { contents, position }; + } + } + + return { contents, position }; +} From 4e56e38bb4bef7303f229d7c1a44ac8681d336c1 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sun, 28 Apr 2024 12:14:55 +0200 Subject: [PATCH 45/98] Fix some completion issues --- .../src/services/completion/helper.ts | 103 ++++++++++++++---- 1 file changed, 81 insertions(+), 22 deletions(-) diff --git a/server/packages/workflow-tests-language-service/src/services/completion/helper.ts b/server/packages/workflow-tests-language-service/src/services/completion/helper.ts index 5bb8e8b..84275ca 100644 --- a/server/packages/workflow-tests-language-service/src/services/completion/helper.ts +++ b/server/packages/workflow-tests-language-service/src/services/completion/helper.ts @@ -197,19 +197,18 @@ ${this.indentation}${this.indentation}$0 // create a new parent parentCompletion = { ...completionItem, - label: schemaType, documentation: schemaDescription, sortText: "_" + schemaType, // this parent completion goes first, kind: parentCompletionKind, }; parentCompletion.label = parentCompletion.label || completionItem.label; - if (parentCompletion.parent) { - parentCompletion.parent.insertTexts = [completionItem.insertText]; - } result.items.push(parentCompletion); + } else if (parentCompletion.parent?.insertTexts?.includes(completionItem.insertText)) { + // already exists in the parent + return; } else { // add to the existing parent - parentCompletion.parent.insertTexts.push(completionItem.insertText); + parentCompletion.parent?.insertTexts?.push(completionItem.insertText); } }; @@ -524,10 +523,6 @@ ${this.indentation}${this.indentation}$0 overwriteRange ); - if (collector.getNumberOfProposals() > 0) { - return collector.result; - } - if (!schema && currentWord.length > 0 && text.charAt(offset - currentWord.length - 1) !== '"') { collector.add({ kind: CompletionItemKind.Property, @@ -547,19 +542,78 @@ ${this.indentation}${this.indentation}$0 this.finalizeParentCompletion(result); - const uniqueItems = result.items.filter( - (arr, index, self) => - index === - self.findIndex( - (item) => item.label === arr.label && item.insertText === arr.insertText && item.kind === arr.kind - ) - ); + result.items = this.mergeCompletionItems(result.items); + + console.debug("COMPLETION RESULT:", result); + return result; + } + + /** + * Returns a new list of completion items with unique labels. + * If the label is the same, the information is merged. + * @param items The list of completion items to merge + * @returns A list of completion items with unique labels + */ + mergeCompletionItems(items: CompletionItem[]): CompletionItem[] { + const uniqueItems: CompletionItem[] = []; + const existingItems: { [key: string]: CompletionItem } = {}; + + items.forEach((item) => { + const key = `${item.label}-${item.insertText}`; + if (!existingItems[key]) { + existingItems[key] = item; + uniqueItems.push(item); + } else { + const existingItem = existingItems[key]; + if (item.documentation && existingItem.documentation) { + existingItem.documentation = this.mergeMarkupContent(existingItem.documentation, item.documentation); + } + } + }); + + return uniqueItems; + } - if (uniqueItems?.length > 0) { - result.items = uniqueItems; + /** + * Merges two MarkupContent objects into one. + * @param existing The existing MarkupContent object + * @param newContent The new MarkupContent object + * @returns The merged MarkupContent object + */ + mergeMarkupContent(existing?: string | MarkupContent, newContent?: string | MarkupContent): MarkupContent { + const existingContent = this.getMarkupContent(existing); + const newContentContent = this.getMarkupContent(newContent); + + if (!existingContent) { + return newContentContent; } - return result; + if (!newContentContent) { + return existingContent; + } + + return { + kind: MarkupKind.Markdown, + value: `${existingContent.value}\n\n${newContentContent.value}`, + }; + } + + /** + * Returns a MarkupContent object from a string or MarkupContent object. + * @param content The content to convert + * @returns The MarkupContent object + */ + getMarkupContent(content?: string | MarkupContent): MarkupContent { + if (!content) { + content = ""; + } + if (typeof content === "string") { + return { + kind: MarkupKind.Markdown, + value: content, + }; + } + return content; } updateCompletionText(completionItem: CompletionItem, text: string): void { @@ -890,13 +944,15 @@ ${this.indentation}${this.indentation}$0 }; result.items.forEach((completionItem) => { - if (isParentCompletionItem(completionItem) && completionItem.parent && completionItem.parent.insertTexts) { + if (isParentCompletionItem(completionItem) && completionItem.parent) { const indent = completionItem.parent.indent || ""; + let insertText = completionItem.insertText || ""; + if (completionItem.parent.insertTexts) { const reindexedTexts = reindexText(completionItem.parent.insertTexts); // add indent to each object property and join completion item texts - let insertText = reindexedTexts.join(`\n${indent}`); + insertText = reindexedTexts.join(`\n${indent}`); // trim $1 from end of completion if (insertText.endsWith("$1")) { @@ -904,8 +960,10 @@ ${this.indentation}${this.indentation}$0 } completionItem.insertText = this.arrayPrefixIndentation + insertText; + } + if (completionItem.textEdit) { - completionItem.textEdit.newText = completionItem.insertText; + completionItem.textEdit.newText = insertText; } // remove $x or use {$x:value} in documentation const mdText = insertText.replace(/\${[0-9]+[:|](.*)}/g, (s, arg) => arg).replace(/\$([0-9]+)/g, ""); @@ -992,6 +1050,7 @@ ${this.indentation}${this.indentation}$0 break; case "data_collection_input": // The valid schema is "Collection" + // TODO add class: Collection matchingSchemas = matchingSchemas.filter((schema) => schema.schema.title === "Collection"); break; } From 038135cda9b8dba255a48d698462041c5a1a8f0b Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sun, 28 Apr 2024 12:17:38 +0200 Subject: [PATCH 46/98] Use resultText when no proposals match --- .../src/services/completion/helper.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/server/packages/workflow-tests-language-service/src/services/completion/helper.ts b/server/packages/workflow-tests-language-service/src/services/completion/helper.ts index 84275ca..f8c48e3 100644 --- a/server/packages/workflow-tests-language-service/src/services/completion/helper.ts +++ b/server/packages/workflow-tests-language-service/src/services/completion/helper.ts @@ -22,7 +22,7 @@ import { Position, Range, TextEdit, -WorkflowTestsDocument, + WorkflowTestsDocument, } from "@gxwf/server-common/src/languageTypes"; import { YamlNode } from "@gxwf/yaml-language-service/src/parser/astTypes"; import { YAMLSubDocument } from "@gxwf/yaml-language-service/src/parser/yamlDocument"; @@ -749,7 +749,7 @@ ${this.indentation}${this.indentation}$0 value = " $1"; break; default: - return propertyText; + return resultText; } } } @@ -949,17 +949,17 @@ ${this.indentation}${this.indentation}$0 let insertText = completionItem.insertText || ""; if (completionItem.parent.insertTexts) { - const reindexedTexts = reindexText(completionItem.parent.insertTexts); + const reindexedTexts = reindexText(completionItem.parent.insertTexts); - // add indent to each object property and join completion item texts + // add indent to each object property and join completion item texts insertText = reindexedTexts.join(`\n${indent}`); - // trim $1 from end of completion - if (insertText.endsWith("$1")) { - insertText = insertText.substring(0, insertText.length - 2); - } + // trim $1 from end of completion + if (insertText.endsWith("$1")) { + insertText = insertText.substring(0, insertText.length - 2); + } - completionItem.insertText = this.arrayPrefixIndentation + insertText; + completionItem.insertText = this.arrayPrefixIndentation + insertText; } if (completionItem.textEdit) { From d318fdcc61966e6a7a14c68f418405be50a676bf Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Wed, 1 May 2024 13:26:33 +0200 Subject: [PATCH 47/98] Remove unused code --- .../src/services/completion/helper.ts | 107 +----------------- 1 file changed, 1 insertion(+), 106 deletions(-) diff --git a/server/packages/workflow-tests-language-service/src/services/completion/helper.ts b/server/packages/workflow-tests-language-service/src/services/completion/helper.ts index f8c48e3..6fcedf7 100644 --- a/server/packages/workflow-tests-language-service/src/services/completion/helper.ts +++ b/server/packages/workflow-tests-language-service/src/services/completion/helper.ts @@ -291,11 +291,6 @@ ${this.indentation}${this.indentation}$0 proposed, }; - // Not supported in this version - // if (this.customTags.length > 0) { - // this.getCustomTagValueCompletions(collector); - // } - if (lineContent.endsWith("\n")) { lineContent = lineContent.substring(0, lineContent.length - 1); } @@ -303,37 +298,6 @@ ${this.indentation}${this.indentation}$0 try { const schema = this.schemaService.schema; - // Modeline not supported in this version - // if (!schema || schema.errors.length) { - // if (position.line === 0 && position.character === 0 && !isModeline(lineContent)) { - // const inlineSchemaCompletion = { - // kind: CompletionItemKind.Text, - // label: "Inline schema", - // insertText: "# yaml-language-server: $schema=", - // insertTextFormat: InsertTextFormat.PlainText, - // }; - // result.items.push(inlineSchemaCompletion); - // } - // } - - // if (isModeline(lineContent) || isInComment(doc.tokens, offset)) { - // const schemaIndex = lineContent.indexOf("$schema="); - // if (schemaIndex !== -1 && schemaIndex + "$schema=".length <= position.character) { - // this.schemaService.getAllSchemas().forEach((schema) => { - // const schemaIdCompletion: CompletionItem = { - // kind: CompletionItemKind.Constant, - // label: schema.name ?? schema.uri, - // detail: schema.description, - // insertText: schema.uri, - // insertTextFormat: InsertTextFormat.PlainText, - // insertTextMode: InsertTextMode.asIs, - // }; - // result.items.push(schemaIdCompletion); - // }); - // } - // return result; - // } - if (!schema || schema.errors.length) { return result; } @@ -1219,50 +1183,7 @@ ${this.indentation}${this.indentation}$0 Array.isArray(nodeParent.items) ); } - - // if (schema.schema.propertyNames && schema.schema.additionalProperties && schema.schema.type === "object") { - // const propertyNameSchema = asSchema(schema.schema.propertyNames); - // const label = propertyNameSchema.title || "property"; - // collector.add({ - // kind: CompletionItemKind.Property, - // label, - // insertText: "$" + `{1:${label}}: `, - // insertTextFormat: InsertTextFormat.Snippet, - // documentation: - // this.fromMarkup(propertyNameSchema.markdownDescription) || propertyNameSchema.description || "", - // }); - // } } - - // if (nodeParent && schema.node.internalNode === nodeParent && schema.schema.defaultSnippets) { - // // For some reason the first item in the array needs to be treated differently, otherwise - // // the indentation will not be correct - // if (node.items.length === 1) { - // this.collectDefaultSnippets( - // schema.schema, - // separatorAfter, - // collector, - // { - // newLineFirst: false, - // indentFirstObject: false, - // shouldIndentWithTab: true, - // }, - // 1 - // ); - // } else { - // this.collectDefaultSnippets( - // schema.schema, - // separatorAfter, - // collector, - // { - // newLineFirst: false, - // indentFirstObject: true, - // shouldIndentWithTab: false, - // }, - // 1 - // ); - // } - // } } } @@ -1302,11 +1223,6 @@ ${this.indentation}${this.indentation}$0 for (const s of matchingSchemas) { if (s.node.internalNode === node && s.schema) { if (s.schema.items) { - // this.collectDefaultSnippets(s.schema, separatorAfter, collector, { - // newLineFirst: false, - // indentFirstObject: false, - // shouldIndentWithTab: false, - // }); if (isSeq(node) && node.items) { if (Array.isArray(s.schema.items)) { const index = this.findItemAtOffset(node, offset); @@ -1537,28 +1453,7 @@ ${this.indentation}${this.indentation}$0 }); hasProposals = true; } - // if (Array.isArray(schema.examples)) { - // schema.examples.forEach((example) => { - // let type = schema.type; - // let value = example; - // for (let i = arrayDepth; i > 0; i--) { - // value = [value]; - // type = "array"; - // } - // collector.add({ - // kind: this.getSuggestionKind(type), - // label: this.getLabelForValue(value), - // insertText: this.getInsertTextForValue(value, separatorAfter, type), - // insertTextFormat: InsertTextFormat.Snippet, - // }); - // hasProposals = true; - // }); - // } - // this.collectDefaultSnippets(schema, separatorAfter, collector, { - // newLineFirst: true, - // indentFirstObject: true, - // shouldIndentWithTab: true, - // }); + if (!hasProposals && typeof schema.items === "object" && !Array.isArray(schema.items)) { this.addDefaultValueCompletions(schema.items, separatorAfter, collector, arrayDepth + 1); } From ef9ba78cc705f98fa8adb16cdede69fffc19c4d3 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Wed, 1 May 2024 13:27:40 +0200 Subject: [PATCH 48/98] Add tests for wf input completion --- .../tests/testHelpers.ts | 11 +- .../tests/unit/completion.test.ts | 107 +++++++++++++++++- 2 files changed, 109 insertions(+), 9 deletions(-) diff --git a/server/packages/workflow-tests-language-service/tests/testHelpers.ts b/server/packages/workflow-tests-language-service/tests/testHelpers.ts index 3fea191..cc584f5 100644 --- a/server/packages/workflow-tests-language-service/tests/testHelpers.ts +++ b/server/packages/workflow-tests-language-service/tests/testHelpers.ts @@ -1,5 +1,5 @@ -import { TextDocument } from "@gxwf/server-common/src/languageTypes"; -import { getLanguageService, YAMLDocument } from "@gxwf/yaml-language-service/src"; +import { TextDocument, WorkflowDataProvider } from "@gxwf/server-common/src/languageTypes"; +import { YAMLDocument, getLanguageService } from "@gxwf/yaml-language-service/src"; import { GxWorkflowTestsDocument } from "../src/document"; export function toYamlDocument(contents: string): { textDoc: TextDocument; yamlDoc: YAMLDocument } { @@ -10,7 +10,10 @@ export function toYamlDocument(contents: string): { textDoc: TextDocument; yamlD return { textDoc, yamlDoc }; } -export function createGxWorkflowTestsDocument(contents: string): GxWorkflowTestsDocument { +export function createGxWorkflowTestsDocument( + contents: string, + workflowDataProvider?: WorkflowDataProvider +): GxWorkflowTestsDocument { const { textDoc, yamlDoc } = toYamlDocument(contents); - return new GxWorkflowTestsDocument(textDoc, yamlDoc); + return new GxWorkflowTestsDocument(textDoc, yamlDoc, workflowDataProvider); } diff --git a/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts b/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts index 444218c..337b608 100644 --- a/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts +++ b/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts @@ -1,5 +1,6 @@ import { container } from "@gxwf/server-common/src/inversify.config"; -import { CompletionList } from "@gxwf/server-common/src/languageTypes"; +import { CompletionItem, CompletionList, WorkflowDataProvider } from "@gxwf/server-common/src/languageTypes"; +import { WorkflowInput } from "@gxwf/server-common/src/services/requestsDefinitions"; import { WorkflowTestsLanguageServiceContainerModule } from "@gxwf/workflow-tests-language-service/src/inversify.config"; import "reflect-metadata"; import { WorkflowTestsSchemaService } from "../../src/schema/service"; @@ -17,9 +18,10 @@ describe("Workflow Tests Completion Service", () => { async function getCompletions( contents: string, - position: { line: number; character: number } + position: { line: number; character: number }, + workflowDataProvider?: WorkflowDataProvider ): Promise { - const documentContext = createGxWorkflowTestsDocument(contents); + const documentContext = createGxWorkflowTestsDocument(contents, workflowDataProvider); return await helper.doComplete(documentContext, position); } @@ -72,10 +74,11 @@ describe("Workflow Tests Completion Service", () => { const completions = await getCompletions(contents, position); expect(completions).not.toBeNull(); + expect(completions?.items.length).toBe(2); const jobCompletion = completions?.items.find((item) => item.label === "job"); const outputCompletion = completions?.items.find((item) => item.label === "outputs"); - expect(jobCompletion).not.toBeUndefined(); - expect(outputCompletion).not.toBeUndefined(); + expect(jobCompletion).toBeDefined(); + expect(outputCompletion).toBeDefined(); }); it("should suggest the `job` entry as first suggestion when the position is at the Test definition level and starts with a `j`", async () => { @@ -90,6 +93,86 @@ describe("Workflow Tests Completion Service", () => { const jobCompletion = completions!.items[0]!; expect(jobCompletion.label).toBe("job"); }); + + describe("Workflow Inputs Completion", () => { + let workflowDataProviderMock: WorkflowDataProvider; + const FAKE_DATASET_INPUT: WorkflowInput = { + name: "My fake dataset", + description: "This is a simple dataset", + type: "data_input", + }; + const FAKE_DATASET_COLLECTION_INPUT: WorkflowInput = { + name: "My fake collection", + description: "This is a collection", + type: "data_collection_input", + }; + const FAKE_WORKFLOW_INPUTS: WorkflowInput[] = [FAKE_DATASET_INPUT, FAKE_DATASET_COLLECTION_INPUT]; + + beforeAll(() => { + workflowDataProviderMock = { + async getWorkflowInputs(_workflowDocumentUri: string) { + return { + inputs: FAKE_WORKFLOW_INPUTS, + }; + }, + }; + }); + + it("should suggest all the defined inputs of the workflow when no inputs are defined in the test", async () => { + const template = ` +- doc: The docs + job: + $`; + const { contents, position } = parseTemplate(template); + + const completions = await getCompletions(contents, position, workflowDataProviderMock); + + expect(completions).not.toBeNull(); + expect(completions?.items.length).toBe(FAKE_WORKFLOW_INPUTS.length); + for (let index = 0; index < FAKE_WORKFLOW_INPUTS.length; index++) { + const workflowInput = FAKE_WORKFLOW_INPUTS[index]; + const completionItem = completions!.items[index]; + expectCompletionItemToMatchWorkflowInput(completionItem, workflowInput); + } + }); + + it("should not suggest an existing input when suggesting inputs", async () => { + const existingInput = FAKE_DATASET_INPUT; + const expectedNumOfRemainingInputs = FAKE_WORKFLOW_INPUTS.length - 1; + const template = ` +- doc: The docs + job: + ${existingInput.name}: + $`; + const { contents, position } = parseTemplate(template); + + const completions = await getCompletions(contents, position, workflowDataProviderMock); + + expect(completions).not.toBeNull(); + expect(completions?.items.length).toBe(expectedNumOfRemainingInputs); + const existingTestInput = completions?.items.find((item) => item.label === existingInput.name); + expect(existingTestInput).toBeUndefined(); + }); + + describe("Dataset Input Completions", () => { + it("should suggest the File class if there is nothing defined", async () => { + const datasetInput = FAKE_DATASET_INPUT; + const template = ` +- doc: The docs + job: + ${datasetInput.name}: + $`; + const { contents, position } = parseTemplate(template); + + const completions = await getCompletions(contents, position, workflowDataProviderMock); + + expect(completions).not.toBeNull(); + expect(completions?.items.length).toBe(1); + expect(completions?.items[0].label).toBe("class"); + expect(completions?.items[0].insertText).toBe("class: File"); + }); + }); + }); }); function parseTemplate( @@ -113,3 +196,17 @@ function parseTemplate( return { contents, position }; } + +function expectCompletionItemDocumentationToContain(completionItem: CompletionItem, value: string): void { + expect(completionItem.documentation).toBeDefined(); + if (typeof completionItem.documentation === "string") { + expect(completionItem.documentation).toContain(value); + } else { + expect(completionItem.documentation?.value).toContain(value); + } +} + +function expectCompletionItemToMatchWorkflowInput(completionItem: CompletionItem, workflowInput: WorkflowInput): void { + expect(completionItem.label).toEqual(workflowInput.name); + expectCompletionItemDocumentationToContain(completionItem, workflowInput.description); +} From c40a074ad40948f9e45023c4145c90edb947e865 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Wed, 1 May 2024 13:40:27 +0200 Subject: [PATCH 49/98] Fix range check on new node contained in sequence element --- .../src/services/completion/helper.ts | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/server/packages/workflow-tests-language-service/src/services/completion/helper.ts b/server/packages/workflow-tests-language-service/src/services/completion/helper.ts index 6fcedf7..89523ae 100644 --- a/server/packages/workflow-tests-language-service/src/services/completion/helper.ts +++ b/server/packages/workflow-tests-language-service/src/services/completion/helper.ts @@ -29,7 +29,7 @@ import { YAMLSubDocument } from "@gxwf/yaml-language-service/src/parser/yamlDocu import { indexOf, isMapContainsEmptyPair } from "@gxwf/yaml-language-service/src/utils"; import { guessIndentation } from "@gxwf/yaml-language-service/src/utils/indentationGuesser"; import { TextBuffer } from "@gxwf/yaml-language-service/src/utils/textBuffer"; -import { Node, Pair, YAMLMap, YAMLSeq, isMap, isNode, isPair, isScalar, isSeq } from "yaml"; +import { Node, Pair, YAMLMap, YAMLSeq, Range as YamlRange, isMap, isNode, isPair, isScalar, isSeq } from "yaml"; import { isDefined, isString } from "../../schema/adapter"; import { JSONSchema, JSONSchemaRef } from "../../schema/jsonSchema"; import { WorkflowTestsSchemaService } from "../../schema/service"; @@ -448,7 +448,18 @@ ${this.indentation}${this.indentation}$0 } } else if (isSeq(node)) { if (lineContent.charAt(position.character - 1) !== "-") { - const map = this.createTempObjNode(currentWord, node, currentDoc); + /** + * It the indentation of the current line matches the indentation of the item in the sequence node + * then we are at the same level as the item in the sequence so it should be a sibling of the item + */ + let range: YamlRange | undefined = undefined; + const lastItem = node.items[node.items.length - 1] as YAMLMap; + if (lastItem) { + node = lastItem; + range = lastItem.range ?? undefined; + } + + const map = this.createTempObjNode(currentWord, node, currentDoc, range); map.items = []; currentDoc.updateFromInternalDocument(); for (const pair of node.items) { @@ -834,13 +845,17 @@ ${this.indentation}${this.indentation}$0 return { insertText, insertIndex }; } - private createTempObjNode(currentWord: string, node: Node, currentDoc: YAMLSubDocument): YAMLMap { + private createTempObjNode(currentWord: string, node: Node, currentDoc: YAMLSubDocument, range?: YamlRange): YAMLMap { + range = range || node.range || undefined; const obj: { [key: string]: unknown } = {}; // Add index signature to allow indexing with a string obj[currentWord] = null; const map: YAMLMap = currentDoc.internalDocument.createNode(obj) as YAMLMap; - map.range = node.range; - (map.items[0].key as Node).range = node.range; - (map.items[0].value as Node).range = node.range; + //********************************** + //TODO: the range here is not correct, it should be the range of the current line + //********************************** + map.range = range; + (map.items[0].key as Node).range = range; + (map.items[0].value as Node).range = range; return map; } From dd1e6c88a94a2621e410c73c91db56caada6aab7 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Wed, 1 May 2024 13:47:57 +0200 Subject: [PATCH 50/98] Fix duplicate suggestion for existing nodes --- .../src/services/completion/helper.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/server/packages/workflow-tests-language-service/src/services/completion/helper.ts b/server/packages/workflow-tests-language-service/src/services/completion/helper.ts index 89523ae..1316f30 100644 --- a/server/packages/workflow-tests-language-service/src/services/completion/helper.ts +++ b/server/packages/workflow-tests-language-service/src/services/completion/helper.ts @@ -463,11 +463,7 @@ ${this.indentation}${this.indentation}$0 map.items = []; currentDoc.updateFromInternalDocument(); for (const pair of node.items) { - if (isMap(pair)) { - pair.items.forEach((value) => { - map.items.push(value); - }); - } + map.items.push(pair as Pair); } node = map; } From 018490da8c41a3dfae6ebb910b4614e56abc2aac Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Wed, 1 May 2024 14:00:07 +0200 Subject: [PATCH 51/98] Fix value completion by matching range --- .../src/services/completion/helper.ts | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/server/packages/workflow-tests-language-service/src/services/completion/helper.ts b/server/packages/workflow-tests-language-service/src/services/completion/helper.ts index 1316f30..09504c6 100644 --- a/server/packages/workflow-tests-language-service/src/services/completion/helper.ts +++ b/server/packages/workflow-tests-language-service/src/services/completion/helper.ts @@ -1047,26 +1047,10 @@ ${this.indentation}${this.indentation}$0 }); } - function hasSameRange(nodeA: YAMLMap, nodeB: YAMLMap): boolean { - // loop through ranges of each node and compare them - if (nodeA.range && nodeB.range && nodeA.range.length === nodeB.range.length) { - for (let i = 0; i < nodeA.range.length; i++) { - if (nodeA.range[i] === nodeB.range[i]) { - continue; - } else { - return false; - } - } - return true; - } - return false; - } - for (const schema of matchingSchemas) { - const internalNode = schema.node.internalNode as YAMLMap; + const internalNode = schema.node.internalNode as HasRange; if ( - // (internalNode.range === node.range && !matchOriginal) || - (hasSameRange(internalNode, node) && !matchOriginal) || + (rangeMatches(internalNode, node as HasRange) && !matchOriginal) || (internalNode === originalNode && !hasColon) || (schema.node.parent?.internalNode === originalNode && !hasColon) ) { @@ -1230,9 +1214,10 @@ ${this.indentation}${this.indentation}$0 if (node && (parentKey !== null || isSeq(node))) { const separatorAfter = ""; const didCallFromAutoComplete = true; - const matchingSchemas = this.schemaService.getMatchingSchemas(documentContext, -1, didCallFromAutoComplete); + const matchingSchemas = this.schemaService.getMatchingSchemas(documentContext, offset, didCallFromAutoComplete); for (const s of matchingSchemas) { - if (s.node.internalNode === node && s.schema) { + const internalNode = s.node.internalNode as HasRange; + if (rangeMatches(internalNode, node as HasRange) && s.schema) { if (s.schema.items) { if (isSeq(node) && node.items) { if (Array.isArray(s.schema.items)) { @@ -1695,3 +1680,14 @@ export function getSchemaRefTypeTitle($ref: string): string { } return type; } + +interface HasRange { + range: YamlRange; +} + +function rangeMatches(nodeA: HasRange, nodeB: HasRange): boolean { + if (nodeA.range && nodeB.range && nodeA.range.length === nodeB.range.length) { + return nodeA.range.every((value, index) => value === nodeB.range[index]); + } + return false; +} From 801d3fa0a4b3b089c48b7eee17a2c806818df729 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sat, 4 May 2024 20:47:14 +0200 Subject: [PATCH 52/98] Add documentation for class completion --- .../src/services/completion/helper.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/server/packages/workflow-tests-language-service/src/services/completion/helper.ts b/server/packages/workflow-tests-language-service/src/services/completion/helper.ts index 09504c6..a10851f 100644 --- a/server/packages/workflow-tests-language-service/src/services/completion/helper.ts +++ b/server/packages/workflow-tests-language-service/src/services/completion/helper.ts @@ -1012,6 +1012,7 @@ ${this.indentation}${this.indentation}$0 label: "class", insertText: "class: File", insertTextFormat: InsertTextFormat.Snippet, + documentation: this.fromMarkup("The class of the input. Default is `File` for this type of input."), }, false ); From 0962c8b743520fd23052e73d7ad124accac33f39 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sun, 5 May 2024 01:12:30 +0200 Subject: [PATCH 53/98] Add more tests --- .../tests/unit/completion.test.ts | 41 +++++++++++++++---- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts b/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts index 337b608..4362e7e 100644 --- a/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts +++ b/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts @@ -66,6 +66,7 @@ describe("Workflow Tests Completion Service", () => { }); it("should suggest the `job` and `outputs` entries when the position is at the same level as `doc`", async () => { + const expectedLabels = ["job", "outputs"]; const template = ` - doc: The docs $`; @@ -75,10 +76,9 @@ describe("Workflow Tests Completion Service", () => { expect(completions).not.toBeNull(); expect(completions?.items.length).toBe(2); - const jobCompletion = completions?.items.find((item) => item.label === "job"); - const outputCompletion = completions?.items.find((item) => item.label === "outputs"); - expect(jobCompletion).toBeDefined(); - expect(outputCompletion).toBeDefined(); + for (const completionItem of completions!.items) { + expect(expectedLabels).toContain(completionItem.label); + } }); it("should suggest the `job` entry as first suggestion when the position is at the Test definition level and starts with a `j`", async () => { @@ -155,21 +155,46 @@ describe("Workflow Tests Completion Service", () => { }); describe("Dataset Input Completions", () => { - it("should suggest the File class if there is nothing defined", async () => { + it("should suggest the 3 possible File classes if there is nothing defined", async () => { + const DATA_INPUT_TYPE_OPTIONS = ["PathFile", "LocationFile", "CompositeDataFile"]; + const datasetInput = FAKE_DATASET_INPUT; + const template = ` +- doc: The docs + job: + ${datasetInput.name}: + $`; + const { contents, position } = parseTemplate(template); + + const completions = await getCompletions(contents, position, workflowDataProviderMock); + + expect(completions).not.toBeNull(); + expect(completions?.items.length).toBe(3); + for (const completionItem of completions!.items) { + expect(completionItem.label).toContain("class"); + expect(completionItem.insertText).toContain("class: File"); + expect(DATA_INPUT_TYPE_OPTIONS).toContain(completionItem.label.replace("class ", "").trim()); + } + }); + + it("should suggest possible attributes for a (PathFile) File input", async () => { const datasetInput = FAKE_DATASET_INPUT; + const expectedAttributes = ["name", "info", "dbkey", "filetype", "deferred"]; const template = ` - doc: The docs job: ${datasetInput.name}: + class: File + path: /path/to/file $`; const { contents, position } = parseTemplate(template); const completions = await getCompletions(contents, position, workflowDataProviderMock); expect(completions).not.toBeNull(); - expect(completions?.items.length).toBe(1); - expect(completions?.items[0].label).toBe("class"); - expect(completions?.items[0].insertText).toBe("class: File"); + for (const expectedAttribute of expectedAttributes) { + const completionItem = completions?.items.find((item) => item.label === expectedAttribute); + expect(completionItem).toBeDefined(); + } }); }); }); From 389b034634211fc71d581d862de5f4f028dfddcf Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sun, 5 May 2024 01:13:59 +0200 Subject: [PATCH 54/98] Improve File class suggestions --- .../src/services/completion/helper.ts | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/server/packages/workflow-tests-language-service/src/services/completion/helper.ts b/server/packages/workflow-tests-language-service/src/services/completion/helper.ts index a10851f..850d00d 100644 --- a/server/packages/workflow-tests-language-service/src/services/completion/helper.ts +++ b/server/packages/workflow-tests-language-service/src/services/completion/helper.ts @@ -1003,26 +1003,31 @@ ${this.indentation}${this.indentation}$0 const matchingWorkflowInput = workflowInputs.find((input) => input.name === nodeParentKey); if (matchingWorkflowInput) { const type = matchingWorkflowInput.type; + const DATA_INPUT_TYPE_OPTIONS = ["PathFile", "LocationFile", "CompositeDataFile"]; switch (type) { case "data_input": + matchingSchemas = matchingSchemas.filter( + (schema) => schema.schema.title && DATA_INPUT_TYPE_OPTIONS.includes(schema.schema.title) + ); if (node.items.length === 1 && isScalar(node.items[0].key) && node.items[0].key.value === "") { - collector.add( - { - kind: CompletionItemKind.Property, - label: "class", - insertText: "class: File", - insertTextFormat: InsertTextFormat.Snippet, - documentation: this.fromMarkup("The class of the input. Default is `File` for this type of input."), - }, - false - ); + for (const schema of matchingSchemas) { + const firstRequired = schema.schema.required?.find((key) => key !== "class") ?? ""; + collector.add( + { + kind: CompletionItemKind.Property, + label: `class ${schema.schema.title}`, + insertText: `class: File\n${firstRequired}: \${1:${firstRequired}}`, + insertTextFormat: InsertTextFormat.Snippet, + documentation: this.fromMarkup( + `The class of the input. This type of input requires the \`${firstRequired}\` attribute.` + ), + sortText: `${DATA_INPUT_TYPE_OPTIONS.indexOf(schema.schema.title!)}`, + }, + false + ); + } return; } - - matchingSchemas = matchingSchemas.filter( - (schema) => - schema.schema.title && ["LocationFile", "PathFile", "CompositeDataFile"].includes(schema.schema.title) - ); break; case "data_collection_input": // The valid schema is "Collection" From 14c82d80b32f70a74166af72d59b9e71fa385027 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Mon, 6 May 2024 00:46:43 +0200 Subject: [PATCH 55/98] Implement WorkflowDataProvider.getWorkflowOutputs --- client/src/common/requestsDefinitions.ts | 15 +++++++++ client/src/requests/gxworkflows.ts | 26 +++++++++++++++ .../server-common/src/languageTypes.ts | 3 +- .../src/models/workflowDocument.ts | 33 ++++++++++++++++++- .../src/models/workflowTestsDocument.ts | 10 +++++- .../src/providers/workflowDataProvider.ts | 26 +++++++++++++++ .../src/services/requestsDefinitions.ts | 15 +++++++++ 7 files changed, 125 insertions(+), 3 deletions(-) diff --git a/client/src/common/requestsDefinitions.ts b/client/src/common/requestsDefinitions.ts index c96b8ce..651fdad 100644 --- a/client/src/common/requestsDefinitions.ts +++ b/client/src/common/requestsDefinitions.ts @@ -7,6 +7,7 @@ export namespace LSRequestIdentifiers { export const CLEAN_WORKFLOW_DOCUMENT = "galaxy-workflows-ls.cleanWorkflowDocument"; export const CLEAN_WORKFLOW_CONTENTS = "galaxy-workflows-ls.cleanWorkflowContents"; export const GET_WORKFLOW_INPUTS = "galaxy-workflows-ls.getWorkflowInputs"; + export const GET_WORKFLOW_OUTPUTS = "galaxy-workflows-ls.getWorkflowOutputs"; } export interface CleanWorkflowDocumentParams { @@ -52,3 +53,17 @@ export interface WorkflowInput { export interface GetWorkflowInputsResult { inputs: WorkflowInput[]; } + +export interface GetWorkflowOutputsParams { + uri: string; +} + +export interface WorkflowOutput { + label: string; + output_name: string; + uuid: string; +} + +export interface GetWorkflowOutputsResult { + outputs: WorkflowOutput[]; +} diff --git a/client/src/requests/gxworkflows.ts b/client/src/requests/gxworkflows.ts index 92bc5d4..5bd1fce 100644 --- a/client/src/requests/gxworkflows.ts +++ b/client/src/requests/gxworkflows.ts @@ -38,4 +38,30 @@ export function setupRequests( return result; }) ); + context.subscriptions.push( + gxFormat2Client.onRequest(LSRequestIdentifiers.GET_WORKFLOW_OUTPUTS, async (params: GetWorkflowInputsParams) => { + let targetUri: Uri | undefined = Uri.parse(params.uri); + if (isWorkflowTestsDocument(targetUri)) { + // If the file is a test file, we need to find the associated workflow file + targetUri = await getAssociatedWorkflowUriFromTestsUri(targetUri); + } + if (!targetUri) { + console.debug("No associated workflow file found for:", params.uri); + return { inputs: [] }; + } + // Open the file to include it in the document cache + await workspace.openTextDocument(targetUri); + + let languageClient = gxFormat2Client; + if (isNativeWorkflowDocument(targetUri)) { + languageClient = nativeWorkflowClient; + } + const requestParams: GetWorkflowInputsParams = { uri: targetUri.toString() }; + const result = await languageClient.sendRequest( + LSRequestIdentifiers.GET_WORKFLOW_OUTPUTS, + requestParams + ); + return result; + }) + ); } diff --git a/server/packages/server-common/src/languageTypes.ts b/server/packages/server-common/src/languageTypes.ts index 4361a92..af3596c 100644 --- a/server/packages/server-common/src/languageTypes.ts +++ b/server/packages/server-common/src/languageTypes.ts @@ -54,7 +54,7 @@ import { ASTNodeManager } from "./ast/nodeManager"; import { ConfigService } from "./configService"; import { WorkflowDocument } from "./models/workflowDocument"; import { WorkflowTestsDocument } from "./models/workflowTestsDocument"; -import { GetWorkflowInputsResult } from "./services/requestsDefinitions"; +import { GetWorkflowInputsResult, GetWorkflowOutputsResult } from "./services/requestsDefinitions"; export { CodeAction, @@ -236,6 +236,7 @@ export interface DocumentsCache { export interface WorkflowDataProvider { getWorkflowInputs(workflowDocumentUri: string): Promise; + getWorkflowOutputs(workflowDocumentUri: string): Promise; } const TYPES = { diff --git a/server/packages/server-common/src/models/workflowDocument.ts b/server/packages/server-common/src/models/workflowDocument.ts index 348a2dc..e0a37b0 100644 --- a/server/packages/server-common/src/models/workflowDocument.ts +++ b/server/packages/server-common/src/models/workflowDocument.ts @@ -1,4 +1,4 @@ -import { GetWorkflowInputsResult } from "../services/requestsDefinitions"; +import { GetWorkflowInputsResult, GetWorkflowOutputsResult } from "../services/requestsDefinitions"; import { isWorkflowInputType } from "../utils"; import { DocumentBase } from "./document"; @@ -29,4 +29,35 @@ export abstract class WorkflowDocument extends DocumentBase { }); return result; } + + /** + * Returns the outputs of the workflow. + */ + public getWorkflowOutputs(): GetWorkflowOutputsResult { + const result: GetWorkflowOutputsResult = { outputs: [] }; + const stepNodes = this.nodeManager.getStepNodes(); + stepNodes.forEach((step) => { + const workflowOutputsNode = step.properties.find((property) => property.keyNode.value === "workflow_outputs"); + if (workflowOutputsNode && workflowOutputsNode.valueNode?.type === "array") { + const workflowOutputs = workflowOutputsNode.valueNode.items; + workflowOutputs.forEach((workflowOutput) => { + if (workflowOutput.type !== "object") { + return; + } + const labelNode = workflowOutput.properties.find((property) => property.keyNode.value === "label"); + const labelValue = String(labelNode?.valueNode?.value); + const outputNameNode = workflowOutput.properties.find((property) => property.keyNode.value === "output_name"); + const outputNameValue = String(outputNameNode?.valueNode?.value); + const uuidNode = workflowOutput.properties.find((property) => property.keyNode.value === "uuid"); + const uuidValue = String(uuidNode?.valueNode?.value); + result.outputs.push({ + label: labelValue ?? "UNKNOWN", + output_name: outputNameValue, + uuid: uuidValue, + }); + }); + } + }); + return result; + } } diff --git a/server/packages/server-common/src/models/workflowTestsDocument.ts b/server/packages/server-common/src/models/workflowTestsDocument.ts index 99e4738..74cd50c 100644 --- a/server/packages/server-common/src/models/workflowTestsDocument.ts +++ b/server/packages/server-common/src/models/workflowTestsDocument.ts @@ -1,5 +1,5 @@ import { WorkflowDataProvider } from "../languageTypes"; -import { WorkflowInput } from "../services/requestsDefinitions"; +import { WorkflowInput, WorkflowOutput } from "../services/requestsDefinitions"; import { DocumentBase } from "./document"; /** @@ -15,4 +15,12 @@ export abstract class WorkflowTestsDocument extends DocumentBase { const result = await this.workflowDataProvider?.getWorkflowInputs(this.textDocument.uri); return result?.inputs ?? []; } + + /** + * Returns the outputs of the associated workflow if available or an empty array otherwise. + */ + public async getWorkflowOutputs(): Promise { + const result = await this.workflowDataProvider?.getWorkflowOutputs(this.textDocument.uri); + return result?.outputs ?? []; + } } diff --git a/server/packages/server-common/src/providers/workflowDataProvider.ts b/server/packages/server-common/src/providers/workflowDataProvider.ts index 0176ab4..cdc1ae7 100644 --- a/server/packages/server-common/src/providers/workflowDataProvider.ts +++ b/server/packages/server-common/src/providers/workflowDataProvider.ts @@ -4,6 +4,8 @@ import { DocumentsCache, TYPES, WorkflowDataProvider, WorkflowDocument } from ". import { GetWorkflowInputsParams, GetWorkflowInputsResult, + GetWorkflowOutputsParams, + GetWorkflowOutputsResult, LSRequestIdentifiers, } from "../services/requestsDefinitions"; @@ -20,6 +22,14 @@ export class WorkflowDataProviderImpl implements WorkflowDataProvider { const workflowDocument = this.getWorkflowDocument(params.uri); return workflowDocument ? workflowDocument.getWorkflowInputs() : { inputs: [] }; }); + + // Register the request handler for getting workflow outputs + connection.onRequest(LSRequestIdentifiers.GET_WORKFLOW_OUTPUTS, (params: GetWorkflowOutputsParams) => { + // if we receive a request to get workflow outputs, we can expect that the workflow document is in the cache + // because the client should have opened it before sending the request. + const workflowDocument = this.getWorkflowDocument(params.uri); + return workflowDocument ? workflowDocument.getWorkflowOutputs() : { outputs: [] }; + }); } /** @@ -38,6 +48,22 @@ export class WorkflowDataProviderImpl implements WorkflowDataProvider { return this.connection.sendRequest(LSRequestIdentifiers.GET_WORKFLOW_INPUTS, params); } + /** + * Returns the outputs of the associated workflow given the URI of the workflow document or the associated test document. + * @param workflowDocumentUri The URI of the workflow document or the associated test document. + * @returns The outputs of the associated workflow. + */ + public async getWorkflowOutputs(workflowDocumentUri: string): Promise { + const params: GetWorkflowOutputsParams = { + uri: workflowDocumentUri.toString(), + }; + // The URI could be of the associated test document. Since we don't know which kind of workflow document + // it is (.ga or format2), we need to ask the client to get the workflow outputs. + // The client will then delegate the request to the appropriate language server after making sure + // that the workflow document is in the cache by opening it. + return this.connection.sendRequest(LSRequestIdentifiers.GET_WORKFLOW_OUTPUTS, params); + } + private getWorkflowDocument(uri: string): WorkflowDocument | undefined { return this.documentsCache.get(uri) as WorkflowDocument; } diff --git a/server/packages/server-common/src/services/requestsDefinitions.ts b/server/packages/server-common/src/services/requestsDefinitions.ts index 6163bd2..c4d7604 100644 --- a/server/packages/server-common/src/services/requestsDefinitions.ts +++ b/server/packages/server-common/src/services/requestsDefinitions.ts @@ -7,6 +7,7 @@ export namespace LSRequestIdentifiers { export const CLEAN_WORKFLOW_DOCUMENT = "galaxy-workflows-ls.cleanWorkflowDocument"; export const CLEAN_WORKFLOW_CONTENTS = "galaxy-workflows-ls.cleanWorkflowContents"; export const GET_WORKFLOW_INPUTS = "galaxy-workflows-ls.getWorkflowInputs"; + export const GET_WORKFLOW_OUTPUTS = "galaxy-workflows-ls.getWorkflowOutputs"; } export interface CleanWorkflowDocumentParams { @@ -52,3 +53,17 @@ export interface WorkflowInput { export interface GetWorkflowInputsResult { inputs: WorkflowInput[]; } + +export interface GetWorkflowOutputsParams { + uri: string; +} + +export interface WorkflowOutput { + label: string; + output_name: string; + uuid: string; +} + +export interface GetWorkflowOutputsResult { + outputs: WorkflowOutput[]; +} From 18e7c75739684aeff2defb450298d1710f67daf7 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Mon, 6 May 2024 00:48:10 +0200 Subject: [PATCH 56/98] Add workflow outputs autocompletion --- .../src/services/completion/helper.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/server/packages/workflow-tests-language-service/src/services/completion/helper.ts b/server/packages/workflow-tests-language-service/src/services/completion/helper.ts index 850d00d..8b21981 100644 --- a/server/packages/workflow-tests-language-service/src/services/completion/helper.ts +++ b/server/packages/workflow-tests-language-service/src/services/completion/helper.ts @@ -995,6 +995,20 @@ ${this.indentation}${this.indentation}$0 return; } + if (nodeParent && isPair(nodeParent) && isScalar(nodeParent.key) && nodeParent.key.value === "outputs") { + const testDocument = documentContext as WorkflowTestsDocument; + const workflowOutputs = await testDocument.getWorkflowOutputs(); + workflowOutputs.forEach((output) => { + collector.add({ + kind: CompletionItemKind.Property, + label: output.label, + insertText: `${output.label}:`, + insertTextFormat: InsertTextFormat.Snippet, + }); + }); + return; + } + //If the parent is a workflow input, then we need to add the properties from the document context if (nodeParent && isPair(nodeParent) && isScalar(nodeParent.key)) { const testDocument = documentContext as WorkflowTestsDocument; From 29b5d3f9213c6b7e5da3a5a302e78a3e2ed9eb66 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Mon, 6 May 2024 01:34:09 +0200 Subject: [PATCH 57/98] Refactor workflowDataProvider and reduce duplication --- client/src/common/requestsDefinitions.ts | 7 +-- client/src/requests/gxworkflows.ts | 60 ++++++++----------- .../src/providers/workflowDataProvider.ts | 11 ++-- .../src/services/requestsDefinitions.ts | 7 +-- 4 files changed, 34 insertions(+), 51 deletions(-) diff --git a/client/src/common/requestsDefinitions.ts b/client/src/common/requestsDefinitions.ts index 651fdad..c7df417 100644 --- a/client/src/common/requestsDefinitions.ts +++ b/client/src/common/requestsDefinitions.ts @@ -38,7 +38,8 @@ export namespace CleanWorkflowContentsRequest { ); } -export interface GetWorkflowInputsParams { +export interface TargetWorkflowDocumentParams { + /** The URI of the target workflow document. */ uri: string; } @@ -54,10 +55,6 @@ export interface GetWorkflowInputsResult { inputs: WorkflowInput[]; } -export interface GetWorkflowOutputsParams { - uri: string; -} - export interface WorkflowOutput { label: string; output_name: string; diff --git a/client/src/requests/gxworkflows.ts b/client/src/requests/gxworkflows.ts index 5bd1fce..ecfa4ed 100644 --- a/client/src/requests/gxworkflows.ts +++ b/client/src/requests/gxworkflows.ts @@ -1,6 +1,11 @@ import { ExtensionContext, Uri, workspace } from "vscode"; import { BaseLanguageClient } from "vscode-languageclient"; -import { GetWorkflowInputsParams, GetWorkflowInputsResult, LSRequestIdentifiers } from "../common/requestsDefinitions"; +import { + GetWorkflowInputsResult, + GetWorkflowOutputsResult, + LSRequestIdentifiers, + TargetWorkflowDocumentParams, +} from "../common/requestsDefinitions"; import { getAssociatedWorkflowUriFromTestsUri, isNativeWorkflowDocument, @@ -12,16 +17,16 @@ export function setupRequests( nativeWorkflowClient: BaseLanguageClient, gxFormat2Client: BaseLanguageClient ): void { - context.subscriptions.push( - gxFormat2Client.onRequest(LSRequestIdentifiers.GET_WORKFLOW_INPUTS, async (params: GetWorkflowInputsParams) => { + function createRequestHandler(requestIdentifier: string) { + return async (params: TargetWorkflowDocumentParams) => { let targetUri: Uri | undefined = Uri.parse(params.uri); if (isWorkflowTestsDocument(targetUri)) { - // If the file is a test file, we need to find the associated workflow file + // If the target is a test file, we need to find the associated workflow file targetUri = await getAssociatedWorkflowUriFromTestsUri(targetUri); } if (!targetUri) { console.debug("No associated workflow file found for:", params.uri); - return { inputs: [] }; + return undefined; } // Open the file to include it in the document cache await workspace.openTextDocument(targetUri); @@ -30,38 +35,23 @@ export function setupRequests( if (isNativeWorkflowDocument(targetUri)) { languageClient = nativeWorkflowClient; } - const requestParams: GetWorkflowInputsParams = { uri: targetUri.toString() }; - const result = await languageClient.sendRequest( - LSRequestIdentifiers.GET_WORKFLOW_INPUTS, - requestParams - ); + const requestParams: TargetWorkflowDocumentParams = { uri: targetUri.toString() }; + const result = await languageClient.sendRequest(requestIdentifier, requestParams); return result; - }) - ); + }; + } + context.subscriptions.push( - gxFormat2Client.onRequest(LSRequestIdentifiers.GET_WORKFLOW_OUTPUTS, async (params: GetWorkflowInputsParams) => { - let targetUri: Uri | undefined = Uri.parse(params.uri); - if (isWorkflowTestsDocument(targetUri)) { - // If the file is a test file, we need to find the associated workflow file - targetUri = await getAssociatedWorkflowUriFromTestsUri(targetUri); - } - if (!targetUri) { - console.debug("No associated workflow file found for:", params.uri); - return { inputs: [] }; - } - // Open the file to include it in the document cache - await workspace.openTextDocument(targetUri); + gxFormat2Client.onRequest( + LSRequestIdentifiers.GET_WORKFLOW_INPUTS, + createRequestHandler(LSRequestIdentifiers.GET_WORKFLOW_INPUTS) + ) + ); - let languageClient = gxFormat2Client; - if (isNativeWorkflowDocument(targetUri)) { - languageClient = nativeWorkflowClient; - } - const requestParams: GetWorkflowInputsParams = { uri: targetUri.toString() }; - const result = await languageClient.sendRequest( - LSRequestIdentifiers.GET_WORKFLOW_OUTPUTS, - requestParams - ); - return result; - }) + context.subscriptions.push( + gxFormat2Client.onRequest( + LSRequestIdentifiers.GET_WORKFLOW_OUTPUTS, + createRequestHandler(LSRequestIdentifiers.GET_WORKFLOW_OUTPUTS) + ) ); } diff --git a/server/packages/server-common/src/providers/workflowDataProvider.ts b/server/packages/server-common/src/providers/workflowDataProvider.ts index cdc1ae7..9dce162 100644 --- a/server/packages/server-common/src/providers/workflowDataProvider.ts +++ b/server/packages/server-common/src/providers/workflowDataProvider.ts @@ -2,11 +2,10 @@ import { inject, injectable } from "inversify"; import { Connection } from "vscode-languageserver"; import { DocumentsCache, TYPES, WorkflowDataProvider, WorkflowDocument } from "../languageTypes"; import { - GetWorkflowInputsParams, GetWorkflowInputsResult, - GetWorkflowOutputsParams, GetWorkflowOutputsResult, LSRequestIdentifiers, + TargetWorkflowDocumentParams, } from "../services/requestsDefinitions"; @injectable() @@ -16,7 +15,7 @@ export class WorkflowDataProviderImpl implements WorkflowDataProvider { @inject(TYPES.DocumentsCache) public readonly documentsCache: DocumentsCache ) { // Register the request handler for getting workflow inputs - connection.onRequest(LSRequestIdentifiers.GET_WORKFLOW_INPUTS, (params: GetWorkflowInputsParams) => { + connection.onRequest(LSRequestIdentifiers.GET_WORKFLOW_INPUTS, (params: TargetWorkflowDocumentParams) => { // if we receive a request to get workflow inputs, we can expect that the workflow document is in the cache // because the client should have opened it before sending the request. const workflowDocument = this.getWorkflowDocument(params.uri); @@ -24,7 +23,7 @@ export class WorkflowDataProviderImpl implements WorkflowDataProvider { }); // Register the request handler for getting workflow outputs - connection.onRequest(LSRequestIdentifiers.GET_WORKFLOW_OUTPUTS, (params: GetWorkflowOutputsParams) => { + connection.onRequest(LSRequestIdentifiers.GET_WORKFLOW_OUTPUTS, (params: TargetWorkflowDocumentParams) => { // if we receive a request to get workflow outputs, we can expect that the workflow document is in the cache // because the client should have opened it before sending the request. const workflowDocument = this.getWorkflowDocument(params.uri); @@ -38,7 +37,7 @@ export class WorkflowDataProviderImpl implements WorkflowDataProvider { * @returns The inputs of the associated workflow. */ public async getWorkflowInputs(workflowDocumentUri: string): Promise { - const params: GetWorkflowInputsParams = { + const params: TargetWorkflowDocumentParams = { uri: workflowDocumentUri.toString(), }; // The URI could be of the associated test document. Since we don't know which kind of workflow document @@ -54,7 +53,7 @@ export class WorkflowDataProviderImpl implements WorkflowDataProvider { * @returns The outputs of the associated workflow. */ public async getWorkflowOutputs(workflowDocumentUri: string): Promise { - const params: GetWorkflowOutputsParams = { + const params: TargetWorkflowDocumentParams = { uri: workflowDocumentUri.toString(), }; // The URI could be of the associated test document. Since we don't know which kind of workflow document diff --git a/server/packages/server-common/src/services/requestsDefinitions.ts b/server/packages/server-common/src/services/requestsDefinitions.ts index c4d7604..01f96ca 100644 --- a/server/packages/server-common/src/services/requestsDefinitions.ts +++ b/server/packages/server-common/src/services/requestsDefinitions.ts @@ -38,7 +38,8 @@ export namespace CleanWorkflowContentsRequest { ); } -export interface GetWorkflowInputsParams { +export interface TargetWorkflowDocumentParams { + /** The URI of the target workflow document. */ uri: string; } @@ -54,10 +55,6 @@ export interface GetWorkflowInputsResult { inputs: WorkflowInput[]; } -export interface GetWorkflowOutputsParams { - uri: string; -} - export interface WorkflowOutput { label: string; output_name: string; From 86aa6ec0d3822cbd6366c4767066f5fbdb7a2f18 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Tue, 7 May 2024 00:23:49 +0200 Subject: [PATCH 58/98] Add tests for output completion --- .../tests/unit/completion.test.ts | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts b/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts index 4362e7e..75ae085 100644 --- a/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts +++ b/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts @@ -1,6 +1,6 @@ import { container } from "@gxwf/server-common/src/inversify.config"; import { CompletionItem, CompletionList, WorkflowDataProvider } from "@gxwf/server-common/src/languageTypes"; -import { WorkflowInput } from "@gxwf/server-common/src/services/requestsDefinitions"; +import { WorkflowInput, WorkflowOutput } from "@gxwf/server-common/src/services/requestsDefinitions"; import { WorkflowTestsLanguageServiceContainerModule } from "@gxwf/workflow-tests-language-service/src/inversify.config"; import "reflect-metadata"; import { WorkflowTestsSchemaService } from "../../src/schema/service"; @@ -107,6 +107,18 @@ describe("Workflow Tests Completion Service", () => { type: "data_collection_input", }; const FAKE_WORKFLOW_INPUTS: WorkflowInput[] = [FAKE_DATASET_INPUT, FAKE_DATASET_COLLECTION_INPUT]; + const FAKE_WORKFLOW_OUTPUTS: WorkflowOutput[] = [ + { + label: "My output", + output_name: "output", + uuid: "1234-5678-91011-1213", + }, + { + label: "My second output", + output_name: "output2", + uuid: "1234-5678-91011-1214", + }, + ]; beforeAll(() => { workflowDataProviderMock = { @@ -115,6 +127,11 @@ describe("Workflow Tests Completion Service", () => { inputs: FAKE_WORKFLOW_INPUTS, }; }, + async getWorkflowOutputs(_workflowDocumentUri: string) { + return { + outputs: FAKE_WORKFLOW_OUTPUTS, + }; + }, }; }); @@ -197,6 +214,25 @@ describe("Workflow Tests Completion Service", () => { } }); }); + describe("Dataset Output Completions", () => { + it("should suggest all the defined outputs of the workflow when no outputs are defined in the test", async () => { + const template = ` +- doc: The docs + outputs: + $`; + const { contents, position } = parseTemplate(template); + + const completions = await getCompletions(contents, position, workflowDataProviderMock); + + expect(completions).not.toBeNull(); + expect(completions?.items.length).toBe(FAKE_WORKFLOW_OUTPUTS.length); + for (let index = 0; index < FAKE_WORKFLOW_OUTPUTS.length; index++) { + const workflowOutput = FAKE_WORKFLOW_OUTPUTS[index]; + const completionItem = completions!.items[index]; + expect(completionItem.label).toEqual(workflowOutput.label); + } + }); + }); }); }); From 72510833370e9d81b16b5187357072c80becfe15 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Tue, 7 May 2024 00:25:46 +0200 Subject: [PATCH 59/98] Handle both "-test.yml" and "-tests.yml" file extensions --- client/src/common/utils.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/client/src/common/utils.ts b/client/src/common/utils.ts index 3c568aa..0979b87 100644 --- a/client/src/common/utils.ts +++ b/client/src/common/utils.ts @@ -47,7 +47,7 @@ export function debugPrintCommandArgs(command: string, args: unknown[], outputCh } export function isWorkflowTestsDocument(uri: Uri): boolean { - return uri.path.endsWith("-test.yml"); + return uri.path.endsWith("-test.yml") || uri.path.endsWith("-tests.yml"); } export function isNativeWorkflowDocument(uri: Uri): boolean { @@ -55,12 +55,16 @@ export function isNativeWorkflowDocument(uri: Uri): boolean { } export async function getAssociatedWorkflowUriFromTestsUri(workflowTestsDocumentUri: Uri): Promise { - const format2WorkflowUri = Uri.parse(workflowTestsDocumentUri.toString().replace("-test.yml", ".yml")); + const format2WorkflowUri = Uri.parse( + workflowTestsDocumentUri.toString().replace("-test.yml", ".yml").replace("-tests.yml", ".yml") + ); try { await workspace.fs.stat(format2WorkflowUri); return format2WorkflowUri; } catch { - const nativeWorkflowUri = Uri.parse(workflowTestsDocumentUri.toString().replace("-test.yml", ".ga")); + const nativeWorkflowUri = Uri.parse( + workflowTestsDocumentUri.toString().replace("-test.yml", ".ga").replace("-tests.yml", ".ga") + ); try { await workspace.fs.stat(nativeWorkflowUri); return nativeWorkflowUri; From 2abe492285e99c48eaaf843f811cbdc390b2a302 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Wed, 8 May 2024 00:57:54 +0200 Subject: [PATCH 60/98] Fix completion with names including colon --- .../src/services/completion/helper.ts | 21 +++++++------------ .../tests/unit/completion.test.ts | 16 +++++++++++++- .../yaml-language-service/src/utils/index.ts | 13 +++++++++++- 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/server/packages/workflow-tests-language-service/src/services/completion/helper.ts b/server/packages/workflow-tests-language-service/src/services/completion/helper.ts index 8b21981..d18ec02 100644 --- a/server/packages/workflow-tests-language-service/src/services/completion/helper.ts +++ b/server/packages/workflow-tests-language-service/src/services/completion/helper.ts @@ -26,7 +26,7 @@ import { } from "@gxwf/server-common/src/languageTypes"; import { YamlNode } from "@gxwf/yaml-language-service/src/parser/astTypes"; import { YAMLSubDocument } from "@gxwf/yaml-language-service/src/parser/yamlDocument"; -import { indexOf, isMapContainsEmptyPair } from "@gxwf/yaml-language-service/src/utils"; +import { HasRange, indexOf, isMapContainsEmptyPair, rangeMatches } from "@gxwf/yaml-language-service/src/utils"; import { guessIndentation } from "@gxwf/yaml-language-service/src/utils/indentationGuesser"; import { TextBuffer } from "@gxwf/yaml-language-service/src/utils/textBuffer"; import { Node, Pair, YAMLMap, YAMLSeq, Range as YamlRange, isMap, isNode, isPair, isScalar, isSeq } from "yaml"; @@ -987,7 +987,7 @@ ${this.indentation}${this.indentation}$0 collector.add({ kind: CompletionItemKind.Property, label: input.name, - insertText: `${input.name}:`, + insertText: `${this.quoteIfColon(input.name)}:`, insertTextFormat: InsertTextFormat.Snippet, documentation: this.fromMarkup(input.description), }); @@ -1002,7 +1002,7 @@ ${this.indentation}${this.indentation}$0 collector.add({ kind: CompletionItemKind.Property, label: output.label, - insertText: `${output.label}:`, + insertText: `${this.quoteIfColon(output.label)}:`, insertTextFormat: InsertTextFormat.Snippet, }); }); @@ -1323,6 +1323,10 @@ ${this.indentation}${this.indentation}$0 return undefined; } + private quoteIfColon(value: string): string { + return value.includes(":") ? `'${value}'` : `${value}`; + } + private doesSupportMarkdown(): boolean { // Forcing markdown for now return true; @@ -1700,14 +1704,3 @@ export function getSchemaRefTypeTitle($ref: string): string { } return type; } - -interface HasRange { - range: YamlRange; -} - -function rangeMatches(nodeA: HasRange, nodeB: HasRange): boolean { - if (nodeA.range && nodeB.range && nodeA.range.length === nodeB.range.length) { - return nodeA.range.every((value, index) => value === nodeB.range[index]); - } - return false; -} diff --git a/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts b/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts index 75ae085..099f61c 100644 --- a/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts +++ b/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts @@ -101,12 +101,21 @@ describe("Workflow Tests Completion Service", () => { description: "This is a simple dataset", type: "data_input", }; + const FAKE_DATASET_INPUT_COLON: WorkflowInput = { + name: "Input dataset: fake", + description: "This is a simple dataset with a colon in the name", + type: "data_input", + }; const FAKE_DATASET_COLLECTION_INPUT: WorkflowInput = { name: "My fake collection", description: "This is a collection", type: "data_collection_input", }; - const FAKE_WORKFLOW_INPUTS: WorkflowInput[] = [FAKE_DATASET_INPUT, FAKE_DATASET_COLLECTION_INPUT]; + const FAKE_WORKFLOW_INPUTS: WorkflowInput[] = [ + FAKE_DATASET_INPUT, + FAKE_DATASET_COLLECTION_INPUT, + FAKE_DATASET_INPUT_COLON, + ]; const FAKE_WORKFLOW_OUTPUTS: WorkflowOutput[] = [ { label: "My output", @@ -118,6 +127,11 @@ describe("Workflow Tests Completion Service", () => { output_name: "output2", uuid: "1234-5678-91011-1214", }, + { + label: "My third output: with colon", + output_name: "output3", + uuid: "1234-5678-91011-1215", + }, ]; beforeAll(() => { diff --git a/server/packages/yaml-language-service/src/utils/index.ts b/server/packages/yaml-language-service/src/utils/index.ts index cb6c134..8dfd322 100644 --- a/server/packages/yaml-language-service/src/utils/index.ts +++ b/server/packages/yaml-language-service/src/utils/index.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Document, Node, YAMLMap, YAMLSeq, isDocument, isScalar, visit } from "yaml"; +import { Document, Node, Range, YAMLMap, YAMLSeq, isDocument, isScalar, visit } from "yaml"; import { YamlNode } from "../parser/astTypes"; import { CharCode } from "../parser/charCode"; @@ -56,3 +56,14 @@ export function isMapContainsEmptyPair(map: YAMLMap): boolean { const pair = map.items[0]; return isScalar(pair.key) && isScalar(pair.value) && pair.key.value === "" && !pair.value.value; } + +export interface HasRange { + range: Range; +} + +export function rangeMatches(nodeA: HasRange, nodeB: HasRange): boolean { + if (nodeA.range && nodeB.range && nodeA.range.length === nodeB.range.length) { + return nodeA.range.every((value, index) => value === nodeB.range[index]); + } + return false; +} From 6ca40af269b96b5b363f9ebbe4f61ab50c6b6b39 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Thu, 9 May 2024 02:33:36 +0200 Subject: [PATCH 61/98] Add test for input names containing colon --- .../tests/unit/completion.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts b/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts index 099f61c..53887d3 100644 --- a/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts +++ b/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts @@ -167,6 +167,18 @@ describe("Workflow Tests Completion Service", () => { } }); + it("should suggest the input including quotes if the name contains colon", async () => { + const template = ` +- doc: The docs + job: + Input$`; + const { contents, position } = parseTemplate(template); + + const completions = await getCompletions(contents, position, workflowDataProviderMock); + + expect(completions).not.toBeNull(); + }); + it("should not suggest an existing input when suggesting inputs", async () => { const existingInput = FAKE_DATASET_INPUT; const expectedNumOfRemainingInputs = FAKE_WORKFLOW_INPUTS.length - 1; From 7ae8421cd42cbef1168c66fdea86e46dafe12477 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Thu, 9 May 2024 02:34:37 +0200 Subject: [PATCH 62/98] Refactor ASTNode types for yaml compatibility --- .../src/nativeWorkflowDocument.ts | 12 ++- .../tests/testHelpers.ts | 4 +- .../packages/server-common/src/ast/types.ts | 74 +++++++++++++------ 3 files changed, 65 insertions(+), 25 deletions(-) diff --git a/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts b/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts index bd9db44..a049cd6 100644 --- a/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts +++ b/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts @@ -1,3 +1,4 @@ +import { ASTNode, ParsedDocument } from "@gxwf/server-common/src/ast/types"; import { TextDocument, WorkflowDocument } from "@gxwf/server-common/src/languageTypes"; import { JSONDocument } from "vscode-json-languageservice"; @@ -8,7 +9,16 @@ export class NativeWorkflowDocument extends WorkflowDocument { private _jsonDocument: JSONDocument; constructor(textDocument: TextDocument, jsonDocument: JSONDocument) { - super(textDocument, { ...jsonDocument, internalDocument: jsonDocument }); + const parsedDocument: ParsedDocument = { + ...{ + root: jsonDocument.root as ASTNode, + getNodeFromOffset(offset: number) { + return jsonDocument.getNodeFromOffset(offset) as ASTNode | undefined; + }, + }, + internalDocument: jsonDocument, + }; + super(textDocument, parsedDocument); this._jsonDocument = jsonDocument; } diff --git a/server/gx-workflow-ls-native/tests/testHelpers.ts b/server/gx-workflow-ls-native/tests/testHelpers.ts index 768be97..b1eff01 100644 --- a/server/gx-workflow-ls-native/tests/testHelpers.ts +++ b/server/gx-workflow-ls-native/tests/testHelpers.ts @@ -1,6 +1,6 @@ import { ASTNode } from "@gxwf/server-common/src/ast/types"; -import { getLanguageService, JSONDocument } from "vscode-json-languageservice"; import { TextDocument } from "@gxwf/server-common/src/languageTypes"; +import { getLanguageService, JSONDocument } from "vscode-json-languageservice"; import { NativeWorkflowDocument } from "../src/nativeWorkflowDocument"; export function toJsonDocument(contents: string): { textDoc: TextDocument; jsonDoc: JSONDocument } { @@ -14,7 +14,7 @@ export function toJsonDocument(contents: string): { textDoc: TextDocument; jsonD export function getJsonDocumentRoot(contents: string): ASTNode { const { jsonDoc } = toJsonDocument(contents); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return jsonDoc.root!; + return jsonDoc.root! as ASTNode; } export function createNativeWorkflowDocument(contents: string): NativeWorkflowDocument { diff --git a/server/packages/server-common/src/ast/types.ts b/server/packages/server-common/src/ast/types.ts index 4813459..2c3ae8c 100644 --- a/server/packages/server-common/src/ast/types.ts +++ b/server/packages/server-common/src/ast/types.ts @@ -1,26 +1,56 @@ -import { - ArrayASTNode, - ASTNode, - BaseASTNode, - BooleanASTNode, - NullASTNode, - NumberASTNode, - ObjectASTNode, - PropertyASTNode, - StringASTNode, -} from "vscode-json-languageservice"; +export type ASTNode = + | ObjectASTNode + | PropertyASTNode + | ArrayASTNode + | StringASTNode + | NumberASTNode + | BooleanASTNode + | NullASTNode; -export { - ArrayASTNode, - ASTNode, - BaseASTNode, - BooleanASTNode, - NullASTNode, - NumberASTNode, - ObjectASTNode, - PropertyASTNode, - StringASTNode, -}; +export interface BaseASTNode { + readonly type: "object" | "array" | "property" | "string" | "number" | "boolean" | "null"; + readonly parent?: ASTNode; + readonly offset: number; + readonly length: number; + readonly children?: ASTNode[]; + readonly value?: string | boolean | number | null; + readonly internalNode: unknown; + getNodeFromOffsetEndInclusive(offset: number): ASTNode | undefined; +} +export interface ObjectASTNode extends BaseASTNode { + readonly type: "object"; + readonly properties: PropertyASTNode[]; + readonly children: ASTNode[]; +} +export interface PropertyASTNode extends BaseASTNode { + readonly type: "property"; + readonly keyNode: StringASTNode; + readonly valueNode?: ASTNode; + readonly colonOffset?: number; + readonly children: ASTNode[]; +} +export interface ArrayASTNode extends BaseASTNode { + readonly type: "array"; + readonly items: ASTNode[]; + readonly children: ASTNode[]; +} +export interface StringASTNode extends BaseASTNode { + readonly type: "string"; + readonly value: string; +} +export interface NumberASTNode extends BaseASTNode { + readonly type: "number"; + readonly value: number; + readonly isInteger: boolean; +} +export interface BooleanASTNode extends BaseASTNode { + readonly type: "boolean"; + readonly value: boolean; +} +export interface NullASTNode extends BaseASTNode { + readonly type: "null"; + readonly value: null; +} export interface ParsedDocument { root?: ASTNode; From b9ccdb1d9714cd875dd498b282971d9d0034fd8b Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Fri, 10 May 2024 01:44:56 +0200 Subject: [PATCH 63/98] Fix getParent for non-pair nodes --- server/packages/yaml-language-service/src/utils/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/packages/yaml-language-service/src/utils/index.ts b/server/packages/yaml-language-service/src/utils/index.ts index 8dfd322..d681b4a 100644 --- a/server/packages/yaml-language-service/src/utils/index.ts +++ b/server/packages/yaml-language-service/src/utils/index.ts @@ -26,7 +26,7 @@ export function getIndentation(lineContent: string, lineOffset: number): number export function getParent(doc: Document, nodeToFind: YamlNode): YamlNode | undefined { let parentNode: Node | undefined = undefined; visit(doc, (_, node, path) => { - if (node === nodeToFind) { + if (node === nodeToFind || rangeMatches(node as HasRange, nodeToFind as HasRange)) { parentNode = path[path.length - 1] as Node; return visit.BREAK; } From 4397cec3f5bc590f556fe82d2929437313016f65 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Fri, 10 May 2024 01:47:18 +0200 Subject: [PATCH 64/98] Move input and output getters to native document --- .../src/gxFormat2WorkflowDocument.ts | 11 ++++ .../src/nativeWorkflowDocument.ts | 57 +++++++++++++++++++ .../src/models/workflowDocument.ts | 50 +--------------- 3 files changed, 70 insertions(+), 48 deletions(-) diff --git a/server/gx-workflow-ls-format2/src/gxFormat2WorkflowDocument.ts b/server/gx-workflow-ls-format2/src/gxFormat2WorkflowDocument.ts index 633c817..d82d64e 100644 --- a/server/gx-workflow-ls-format2/src/gxFormat2WorkflowDocument.ts +++ b/server/gx-workflow-ls-format2/src/gxFormat2WorkflowDocument.ts @@ -1,4 +1,8 @@ import { TextDocument, WorkflowDocument } from "@gxwf/server-common/src/languageTypes"; +import { + GetWorkflowInputsResult, + GetWorkflowOutputsResult, +} from "@gxwf/server-common/src/services/requestsDefinitions"; import { YAMLDocument } from "@gxwf/yaml-language-service/src"; /** @@ -14,4 +18,11 @@ export class GxFormat2WorkflowDocument extends WorkflowDocument { public get yamlDocument(): YAMLDocument { return this._yamlDocument; } + + public getWorkflowInputs(): GetWorkflowInputsResult { + throw new Error("Method not implemented."); + } + public getWorkflowOutputs(): GetWorkflowOutputsResult { + throw new Error("Method not implemented."); + } } diff --git a/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts b/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts index a049cd6..02cb8b2 100644 --- a/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts +++ b/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts @@ -1,5 +1,10 @@ import { ASTNode, ParsedDocument } from "@gxwf/server-common/src/ast/types"; import { TextDocument, WorkflowDocument } from "@gxwf/server-common/src/languageTypes"; +import { + GetWorkflowInputsResult, + GetWorkflowOutputsResult, +} from "@gxwf/server-common/src/services/requestsDefinitions"; +import { isWorkflowInputType } from "@gxwf/server-common/src/utils"; import { JSONDocument } from "vscode-json-languageservice"; /** @@ -25,4 +30,56 @@ export class NativeWorkflowDocument extends WorkflowDocument { public get jsonDocument(): JSONDocument { return this._jsonDocument; } + + public getWorkflowInputs(): GetWorkflowInputsResult { + const result: GetWorkflowInputsResult = { inputs: [] }; + const stepNodes = this.nodeManager.getStepNodes(); + stepNodes.forEach((step) => { + const stepTypeNode = step.properties.find((property) => property.keyNode.value === "type"); + const stepTypeValue = String(stepTypeNode?.valueNode?.value); + if (isWorkflowInputType(stepTypeValue)) { + const labelNode = step.properties.find((property) => property.keyNode.value === "label"); + const labelValue = String(labelNode?.valueNode?.value); + const annotationNode = step.properties.find((property) => property.keyNode.value === "annotation"); + const annotationValue = String(annotationNode?.valueNode?.value); + result.inputs.push({ + name: labelValue ?? "UNKNOWN", + description: annotationValue, + type: stepTypeValue, + }); + } + }); + return result; + } + + /** + * Returns the outputs of the workflow. + */ + public getWorkflowOutputs(): GetWorkflowOutputsResult { + const result: GetWorkflowOutputsResult = { outputs: [] }; + const stepNodes = this.nodeManager.getStepNodes(); + stepNodes.forEach((step) => { + const workflowOutputsNode = step.properties.find((property) => property.keyNode.value === "workflow_outputs"); + if (workflowOutputsNode && workflowOutputsNode.valueNode?.type === "array") { + const workflowOutputs = workflowOutputsNode.valueNode.items; + workflowOutputs.forEach((workflowOutput) => { + if (workflowOutput.type !== "object") { + return; + } + const labelNode = workflowOutput.properties.find((property) => property.keyNode.value === "label"); + const labelValue = String(labelNode?.valueNode?.value); + const outputNameNode = workflowOutput.properties.find((property) => property.keyNode.value === "output_name"); + const outputNameValue = String(outputNameNode?.valueNode?.value); + const uuidNode = workflowOutput.properties.find((property) => property.keyNode.value === "uuid"); + const uuidValue = String(uuidNode?.valueNode?.value); + result.outputs.push({ + label: labelValue ?? "UNKNOWN", + output_name: outputNameValue, + uuid: uuidValue, + }); + }); + } + }); + return result; + } } diff --git a/server/packages/server-common/src/models/workflowDocument.ts b/server/packages/server-common/src/models/workflowDocument.ts index e0a37b0..7cab890 100644 --- a/server/packages/server-common/src/models/workflowDocument.ts +++ b/server/packages/server-common/src/models/workflowDocument.ts @@ -1,5 +1,4 @@ import { GetWorkflowInputsResult, GetWorkflowOutputsResult } from "../services/requestsDefinitions"; -import { isWorkflowInputType } from "../utils"; import { DocumentBase } from "./document"; /** @@ -9,55 +8,10 @@ export abstract class WorkflowDocument extends DocumentBase { /** * Returns the inputs of the workflow. */ - public getWorkflowInputs(): GetWorkflowInputsResult { - const result: GetWorkflowInputsResult = { inputs: [] }; - const stepNodes = this.nodeManager.getStepNodes(); - stepNodes.forEach((step) => { - const stepTypeNode = step.properties.find((property) => property.keyNode.value === "type"); - const stepTypeValue = String(stepTypeNode?.valueNode?.value); - if (isWorkflowInputType(stepTypeValue)) { - const labelNode = step.properties.find((property) => property.keyNode.value === "label"); - const labelValue = String(labelNode?.valueNode?.value); - const annotationNode = step.properties.find((property) => property.keyNode.value === "annotation"); - const annotationValue = String(annotationNode?.valueNode?.value); - result.inputs.push({ - name: labelValue ?? "UNKNOWN", - description: annotationValue, - type: stepTypeValue, - }); - } - }); - return result; - } + public abstract getWorkflowInputs(): GetWorkflowInputsResult; /** * Returns the outputs of the workflow. */ - public getWorkflowOutputs(): GetWorkflowOutputsResult { - const result: GetWorkflowOutputsResult = { outputs: [] }; - const stepNodes = this.nodeManager.getStepNodes(); - stepNodes.forEach((step) => { - const workflowOutputsNode = step.properties.find((property) => property.keyNode.value === "workflow_outputs"); - if (workflowOutputsNode && workflowOutputsNode.valueNode?.type === "array") { - const workflowOutputs = workflowOutputsNode.valueNode.items; - workflowOutputs.forEach((workflowOutput) => { - if (workflowOutput.type !== "object") { - return; - } - const labelNode = workflowOutput.properties.find((property) => property.keyNode.value === "label"); - const labelValue = String(labelNode?.valueNode?.value); - const outputNameNode = workflowOutput.properties.find((property) => property.keyNode.value === "output_name"); - const outputNameValue = String(outputNameNode?.valueNode?.value); - const uuidNode = workflowOutput.properties.find((property) => property.keyNode.value === "uuid"); - const uuidValue = String(uuidNode?.valueNode?.value); - result.outputs.push({ - label: labelValue ?? "UNKNOWN", - output_name: outputNameValue, - uuid: uuidValue, - }); - }); - } - }); - return result; - } + public abstract getWorkflowOutputs(): GetWorkflowOutputsResult; } From f0ee612ecdd86f362f2d37e698f37cb19f4e4647 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Fri, 10 May 2024 19:58:32 +0200 Subject: [PATCH 65/98] Fix associated workflow uri for format2 workflows --- client/src/common/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/common/utils.ts b/client/src/common/utils.ts index 0979b87..1d69a51 100644 --- a/client/src/common/utils.ts +++ b/client/src/common/utils.ts @@ -56,7 +56,7 @@ export function isNativeWorkflowDocument(uri: Uri): boolean { export async function getAssociatedWorkflowUriFromTestsUri(workflowTestsDocumentUri: Uri): Promise { const format2WorkflowUri = Uri.parse( - workflowTestsDocumentUri.toString().replace("-test.yml", ".yml").replace("-tests.yml", ".yml") + workflowTestsDocumentUri.toString().replace("-test.yml", ".gxwf.yml").replace("-tests.yml", ".gxwf.yml") ); try { await workspace.fs.stat(format2WorkflowUri); From 630d47888e792aee667ef400c5800f919ae6cc0c Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Fri, 10 May 2024 20:36:55 +0200 Subject: [PATCH 66/98] Refactor to remove RequestType dependency --- client/src/commands/cleanWorkflow.ts | 11 +++++++++-- client/src/common/requestsDefinitions.ts | 13 ------------- client/src/providers/cleanWorkflowProvider.ts | 11 +++++++++-- .../src/services/cleanWorkflow.ts | 19 ++++++++++--------- .../src/services/requestsDefinitions.ts | 14 +------------- 5 files changed, 29 insertions(+), 39 deletions(-) diff --git a/client/src/commands/cleanWorkflow.ts b/client/src/commands/cleanWorkflow.ts index 74a8e23..30e3b49 100644 --- a/client/src/commands/cleanWorkflow.ts +++ b/client/src/commands/cleanWorkflow.ts @@ -1,6 +1,10 @@ import { window } from "vscode"; -import { CleanWorkflowDocumentParams, CleanWorkflowDocumentRequest } from "../common/requestsDefinitions"; import { CustomCommand, getCommandFullIdentifier } from "."; +import { + CleanWorkflowDocumentParams, + CleanWorkflowDocumentResult, + LSRequestIdentifiers, +} from "../common/requestsDefinitions"; /** * Command to 'clean' the selected workflow document. @@ -17,7 +21,10 @@ export class CleanWorkflowCommand extends CustomCommand { const { document } = window.activeTextEditor; const params: CleanWorkflowDocumentParams = { uri: this.client.code2ProtocolConverter.asUri(document.uri) }; - const result = await this.client.sendRequest(CleanWorkflowDocumentRequest.type, params); + const result = await this.client.sendRequest( + LSRequestIdentifiers.CLEAN_WORKFLOW_DOCUMENT, + params + ); if (!result) { throw new Error("Cannot clean the requested document. The server returned no result."); } diff --git a/client/src/common/requestsDefinitions.ts b/client/src/common/requestsDefinitions.ts index c7df417..28643d3 100644 --- a/client/src/common/requestsDefinitions.ts +++ b/client/src/common/requestsDefinitions.ts @@ -1,5 +1,4 @@ /* eslint-disable @typescript-eslint/no-namespace */ -import { RequestType } from "vscode-languageclient"; // TODO: Move the contents of this file to a shared lib https://github.com/Microsoft/vscode/issues/15829 @@ -26,18 +25,6 @@ export interface CleanWorkflowContentsResult { contents: string; } -export namespace CleanWorkflowDocumentRequest { - export const type = new RequestType( - LSRequestIdentifiers.CLEAN_WORKFLOW_DOCUMENT - ); -} - -export namespace CleanWorkflowContentsRequest { - export const type = new RequestType( - LSRequestIdentifiers.CLEAN_WORKFLOW_CONTENTS - ); -} - export interface TargetWorkflowDocumentParams { /** The URI of the target workflow document. */ uri: string; diff --git a/client/src/providers/cleanWorkflowProvider.ts b/client/src/providers/cleanWorkflowProvider.ts index 69ea414..485d60d 100644 --- a/client/src/providers/cleanWorkflowProvider.ts +++ b/client/src/providers/cleanWorkflowProvider.ts @@ -1,6 +1,10 @@ import { Uri, window, workspace } from "vscode"; import { BaseLanguageClient } from "vscode-languageclient"; -import { CleanWorkflowContentsParams, CleanWorkflowContentsRequest } from "../common/requestsDefinitions"; +import { + CleanWorkflowContentsParams, + CleanWorkflowContentsResult, + LSRequestIdentifiers, +} from "../common/requestsDefinitions"; import { getWorkspaceScheme, replaceUriScheme } from "../common/utils"; import { GitProvider } from "./git"; @@ -56,7 +60,10 @@ export class CleanWorkflowProvider { const params: CleanWorkflowContentsParams = { contents: contents, }; - const result = await this.languageClient.sendRequest(CleanWorkflowContentsRequest.type, params); + const result = await this.languageClient.sendRequest( + LSRequestIdentifiers.CLEAN_WORKFLOW_CONTENTS, + params + ); if (!result) { throw new Error("Cannot clean the requested document contents. The server returned no content"); } diff --git a/server/packages/server-common/src/services/cleanWorkflow.ts b/server/packages/server-common/src/services/cleanWorkflow.ts index f9f1012..d612f2d 100644 --- a/server/packages/server-common/src/services/cleanWorkflow.ts +++ b/server/packages/server-common/src/services/cleanWorkflow.ts @@ -1,16 +1,15 @@ import { ApplyWorkspaceEditParams, Range, TextDocumentEdit, TextEdit } from "vscode-languageserver"; import { TextDocument } from "vscode-languageserver-textdocument"; -import { GalaxyWorkflowLanguageServer, WorkflowDocument } from "../languageTypes"; import { ServiceBase } from "."; +import { ASTNode, PropertyASTNode } from "../ast/types"; +import { GalaxyWorkflowLanguageServer, WorkflowDocument } from "../languageTypes"; import { CleanWorkflowContentsParams, - CleanWorkflowContentsRequest, CleanWorkflowContentsResult, CleanWorkflowDocumentParams, - CleanWorkflowDocumentRequest, CleanWorkflowDocumentResult, + LSRequestIdentifiers, } from "./requestsDefinitions"; -import { ASTNode, PropertyASTNode } from "../ast/types"; /** * Service for handling workflow `cleaning` requests. @@ -29,16 +28,18 @@ export class CleanWorkflowService extends ServiceBase { } protected listenToRequests(): void { - this.server.connection.onRequest(CleanWorkflowContentsRequest.type, (params) => - this.onCleanWorkflowContentsRequest(params) + this.server.connection.onRequest( + LSRequestIdentifiers.CLEAN_WORKFLOW_CONTENTS, + (params: CleanWorkflowContentsParams) => this.onCleanWorkflowContentsRequest(params) ); - this.server.connection.onRequest(CleanWorkflowDocumentRequest.type, (params) => - this.onCleanWorkflowDocumentRequest(params) + this.server.connection.onRequest( + LSRequestIdentifiers.CLEAN_WORKFLOW_DOCUMENT, + (params: CleanWorkflowDocumentParams) => this.onCleanWorkflowDocumentRequest(params) ); } /** - * Processes a `CleanWorkflowContentsRequest` by returning the `clean` contents + * Processes a `CLEAN_WORKFLOW_DOCUMENT` request by returning the `clean` contents * of a workflow document given the raw text contents of the workflow document. * @param params The request parameters containing the raw text contents of the workflow * @returns The `clean` contents of the workflow document diff --git a/server/packages/server-common/src/services/requestsDefinitions.ts b/server/packages/server-common/src/services/requestsDefinitions.ts index 01f96ca..f9251e1 100644 --- a/server/packages/server-common/src/services/requestsDefinitions.ts +++ b/server/packages/server-common/src/services/requestsDefinitions.ts @@ -1,5 +1,4 @@ /* eslint-disable @typescript-eslint/no-namespace */ -import { RequestType } from "vscode-languageserver"; // TODO: Move the contents of this file to a shared lib https://github.com/Microsoft/vscode/issues/15829 @@ -26,23 +25,12 @@ export interface CleanWorkflowContentsResult { contents: string; } -export namespace CleanWorkflowDocumentRequest { - export const type = new RequestType( - LSRequestIdentifiers.CLEAN_WORKFLOW_DOCUMENT - ); -} - -export namespace CleanWorkflowContentsRequest { - export const type = new RequestType( - LSRequestIdentifiers.CLEAN_WORKFLOW_CONTENTS - ); -} - export interface TargetWorkflowDocumentParams { /** The URI of the target workflow document. */ uri: string; } +// TODO: rename to File and Collection export type WorkflowInputType = "data_input" | "data_collection_input"; export interface WorkflowInput { From 5dc82daf2f3b5f87e0a63e25e5952a982b6f2d0e Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sat, 11 May 2024 17:44:28 +0200 Subject: [PATCH 67/98] Refactor move shared types --- client/src/commands/cleanWorkflow.ts | 2 +- client/src/providers/cleanWorkflowProvider.ts | 2 +- client/src/requests/gxworkflows.ts | 2 +- .../src/gxFormat2WorkflowDocument.ts | 5 +- .../src/nativeWorkflowDocument.ts | 7 +-- server/gx-workflow-ls-native/tsconfig.json | 3 +- .../server-common/src/languageTypes.ts | 2 +- .../src/models/workflowDocument.ts | 2 +- .../src/models/workflowTestsDocument.ts | 2 +- .../src/providers/workflowDataProvider.ts | 4 +- .../src/services/cleanWorkflow.ts | 6 +-- .../src/services/requestsDefinitions.ts | 54 ------------------- server/packages/server-common/src/utils.ts | 2 +- server/packages/server-common/tsconfig.json | 9 ++-- .../tests/unit/completion.test.ts | 2 +- server/tsconfig.json | 1 + .../src}/requestsDefinitions.ts | 2 - 17 files changed, 23 insertions(+), 84 deletions(-) delete mode 100644 server/packages/server-common/src/services/requestsDefinitions.ts rename {client/src/common => shared/src}/requestsDefinitions.ts (92%) diff --git a/client/src/commands/cleanWorkflow.ts b/client/src/commands/cleanWorkflow.ts index 30e3b49..8fd7b77 100644 --- a/client/src/commands/cleanWorkflow.ts +++ b/client/src/commands/cleanWorkflow.ts @@ -4,7 +4,7 @@ import { CleanWorkflowDocumentParams, CleanWorkflowDocumentResult, LSRequestIdentifiers, -} from "../common/requestsDefinitions"; +} from "../../../shared/src/requestsDefinitions"; /** * Command to 'clean' the selected workflow document. diff --git a/client/src/providers/cleanWorkflowProvider.ts b/client/src/providers/cleanWorkflowProvider.ts index 485d60d..d2c25be 100644 --- a/client/src/providers/cleanWorkflowProvider.ts +++ b/client/src/providers/cleanWorkflowProvider.ts @@ -4,7 +4,7 @@ import { CleanWorkflowContentsParams, CleanWorkflowContentsResult, LSRequestIdentifiers, -} from "../common/requestsDefinitions"; +} from "../../../shared/src/requestsDefinitions"; import { getWorkspaceScheme, replaceUriScheme } from "../common/utils"; import { GitProvider } from "./git"; diff --git a/client/src/requests/gxworkflows.ts b/client/src/requests/gxworkflows.ts index ecfa4ed..161c127 100644 --- a/client/src/requests/gxworkflows.ts +++ b/client/src/requests/gxworkflows.ts @@ -5,7 +5,7 @@ import { GetWorkflowOutputsResult, LSRequestIdentifiers, TargetWorkflowDocumentParams, -} from "../common/requestsDefinitions"; +} from "../../../shared/src/requestsDefinitions"; import { getAssociatedWorkflowUriFromTestsUri, isNativeWorkflowDocument, diff --git a/server/gx-workflow-ls-format2/src/gxFormat2WorkflowDocument.ts b/server/gx-workflow-ls-format2/src/gxFormat2WorkflowDocument.ts index d82d64e..87c60ed 100644 --- a/server/gx-workflow-ls-format2/src/gxFormat2WorkflowDocument.ts +++ b/server/gx-workflow-ls-format2/src/gxFormat2WorkflowDocument.ts @@ -1,9 +1,6 @@ import { TextDocument, WorkflowDocument } from "@gxwf/server-common/src/languageTypes"; -import { - GetWorkflowInputsResult, - GetWorkflowOutputsResult, -} from "@gxwf/server-common/src/services/requestsDefinitions"; import { YAMLDocument } from "@gxwf/yaml-language-service/src"; +import { GetWorkflowInputsResult, GetWorkflowOutputsResult } from "../../../shared/src/requestsDefinitions"; /** * This class provides information about a gxformat2 workflow document structure. diff --git a/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts b/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts index 02cb8b2..6aaae53 100644 --- a/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts +++ b/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts @@ -1,11 +1,8 @@ import { ASTNode, ParsedDocument } from "@gxwf/server-common/src/ast/types"; import { TextDocument, WorkflowDocument } from "@gxwf/server-common/src/languageTypes"; -import { - GetWorkflowInputsResult, - GetWorkflowOutputsResult, -} from "@gxwf/server-common/src/services/requestsDefinitions"; import { isWorkflowInputType } from "@gxwf/server-common/src/utils"; import { JSONDocument } from "vscode-json-languageservice"; +import { GetWorkflowInputsResult, GetWorkflowOutputsResult } from "../../../shared/src/requestsDefinitions"; /** * This class provides information about a Native workflow document structure. @@ -45,7 +42,7 @@ export class NativeWorkflowDocument extends WorkflowDocument { result.inputs.push({ name: labelValue ?? "UNKNOWN", description: annotationValue, - type: stepTypeValue, + type: stepTypeValue as "data_input" | "data_collection_input", }); } }); diff --git a/server/gx-workflow-ls-native/tsconfig.json b/server/gx-workflow-ls-native/tsconfig.json index 0929ac2..198d885 100644 --- a/server/gx-workflow-ls-native/tsconfig.json +++ b/server/gx-workflow-ls-native/tsconfig.json @@ -15,6 +15,5 @@ "paths": { "@schemas/*": ["../../workflow-languages/schemas/*"] } - }, - "include": ["src/**/*.ts", "tests/**/*.ts"] + } } diff --git a/server/packages/server-common/src/languageTypes.ts b/server/packages/server-common/src/languageTypes.ts index af3596c..fc316ce 100644 --- a/server/packages/server-common/src/languageTypes.ts +++ b/server/packages/server-common/src/languageTypes.ts @@ -50,11 +50,11 @@ import { HoverParams, } from "vscode-languageserver/browser"; import { URI } from "vscode-uri"; +import { GetWorkflowInputsResult, GetWorkflowOutputsResult } from "../../../../shared/src/requestsDefinitions"; import { ASTNodeManager } from "./ast/nodeManager"; import { ConfigService } from "./configService"; import { WorkflowDocument } from "./models/workflowDocument"; import { WorkflowTestsDocument } from "./models/workflowTestsDocument"; -import { GetWorkflowInputsResult, GetWorkflowOutputsResult } from "./services/requestsDefinitions"; export { CodeAction, diff --git a/server/packages/server-common/src/models/workflowDocument.ts b/server/packages/server-common/src/models/workflowDocument.ts index 7cab890..8c6fd1c 100644 --- a/server/packages/server-common/src/models/workflowDocument.ts +++ b/server/packages/server-common/src/models/workflowDocument.ts @@ -1,4 +1,4 @@ -import { GetWorkflowInputsResult, GetWorkflowOutputsResult } from "../services/requestsDefinitions"; +import { GetWorkflowInputsResult, GetWorkflowOutputsResult } from "../../../../../shared/src/requestsDefinitions"; import { DocumentBase } from "./document"; /** diff --git a/server/packages/server-common/src/models/workflowTestsDocument.ts b/server/packages/server-common/src/models/workflowTestsDocument.ts index 74cd50c..b8fa1f1 100644 --- a/server/packages/server-common/src/models/workflowTestsDocument.ts +++ b/server/packages/server-common/src/models/workflowTestsDocument.ts @@ -1,5 +1,5 @@ +import { WorkflowInput, WorkflowOutput } from "../../../../../shared/src/requestsDefinitions"; import { WorkflowDataProvider } from "../languageTypes"; -import { WorkflowInput, WorkflowOutput } from "../services/requestsDefinitions"; import { DocumentBase } from "./document"; /** diff --git a/server/packages/server-common/src/providers/workflowDataProvider.ts b/server/packages/server-common/src/providers/workflowDataProvider.ts index 9dce162..5a89fa9 100644 --- a/server/packages/server-common/src/providers/workflowDataProvider.ts +++ b/server/packages/server-common/src/providers/workflowDataProvider.ts @@ -1,12 +1,12 @@ import { inject, injectable } from "inversify"; import { Connection } from "vscode-languageserver"; -import { DocumentsCache, TYPES, WorkflowDataProvider, WorkflowDocument } from "../languageTypes"; import { GetWorkflowInputsResult, GetWorkflowOutputsResult, LSRequestIdentifiers, TargetWorkflowDocumentParams, -} from "../services/requestsDefinitions"; +} from "../../../../../shared/src/requestsDefinitions"; +import { DocumentsCache, TYPES, WorkflowDataProvider, WorkflowDocument } from "../languageTypes"; @injectable() export class WorkflowDataProviderImpl implements WorkflowDataProvider { diff --git a/server/packages/server-common/src/services/cleanWorkflow.ts b/server/packages/server-common/src/services/cleanWorkflow.ts index d612f2d..49a6706 100644 --- a/server/packages/server-common/src/services/cleanWorkflow.ts +++ b/server/packages/server-common/src/services/cleanWorkflow.ts @@ -1,15 +1,15 @@ import { ApplyWorkspaceEditParams, Range, TextDocumentEdit, TextEdit } from "vscode-languageserver"; import { TextDocument } from "vscode-languageserver-textdocument"; import { ServiceBase } from "."; -import { ASTNode, PropertyASTNode } from "../ast/types"; -import { GalaxyWorkflowLanguageServer, WorkflowDocument } from "../languageTypes"; import { CleanWorkflowContentsParams, CleanWorkflowContentsResult, CleanWorkflowDocumentParams, CleanWorkflowDocumentResult, LSRequestIdentifiers, -} from "./requestsDefinitions"; +} from "../../../../../shared/src/requestsDefinitions"; +import { ASTNode, PropertyASTNode } from "../ast/types"; +import { GalaxyWorkflowLanguageServer, WorkflowDocument } from "../languageTypes"; /** * Service for handling workflow `cleaning` requests. diff --git a/server/packages/server-common/src/services/requestsDefinitions.ts b/server/packages/server-common/src/services/requestsDefinitions.ts deleted file mode 100644 index f9251e1..0000000 --- a/server/packages/server-common/src/services/requestsDefinitions.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* eslint-disable @typescript-eslint/no-namespace */ - -// TODO: Move the contents of this file to a shared lib https://github.com/Microsoft/vscode/issues/15829 - -export namespace LSRequestIdentifiers { - export const CLEAN_WORKFLOW_DOCUMENT = "galaxy-workflows-ls.cleanWorkflowDocument"; - export const CLEAN_WORKFLOW_CONTENTS = "galaxy-workflows-ls.cleanWorkflowContents"; - export const GET_WORKFLOW_INPUTS = "galaxy-workflows-ls.getWorkflowInputs"; - export const GET_WORKFLOW_OUTPUTS = "galaxy-workflows-ls.getWorkflowOutputs"; -} - -export interface CleanWorkflowDocumentParams { - uri: string; -} - -export interface CleanWorkflowDocumentResult { - error: string; -} - -export interface CleanWorkflowContentsParams { - contents: string; -} - -export interface CleanWorkflowContentsResult { - contents: string; -} - -export interface TargetWorkflowDocumentParams { - /** The URI of the target workflow document. */ - uri: string; -} - -// TODO: rename to File and Collection -export type WorkflowInputType = "data_input" | "data_collection_input"; - -export interface WorkflowInput { - name: string; - type: WorkflowInputType; - description: string; -} - -export interface GetWorkflowInputsResult { - inputs: WorkflowInput[]; -} - -export interface WorkflowOutput { - label: string; - output_name: string; - uuid: string; -} - -export interface GetWorkflowOutputsResult { - outputs: WorkflowOutput[]; -} diff --git a/server/packages/server-common/src/utils.ts b/server/packages/server-common/src/utils.ts index 4aded71..6b99333 100644 --- a/server/packages/server-common/src/utils.ts +++ b/server/packages/server-common/src/utils.ts @@ -1,4 +1,4 @@ -import { WorkflowInputType } from "./services/requestsDefinitions"; +import { WorkflowInputType } from "../../../../shared/src/requestsDefinitions"; export function isWorkflowInputType(input: string): input is WorkflowInputType { return input === "data_input" || input === "data_collection_input"; diff --git a/server/packages/server-common/tsconfig.json b/server/packages/server-common/tsconfig.json index 300dd9a..1a5daca 100644 --- a/server/packages/server-common/tsconfig.json +++ b/server/packages/server-common/tsconfig.json @@ -13,8 +13,9 @@ "strict": true, "composite": true, "declaration": true, - "rootDirs": ["src", "tests"], - "baseUrl": "." - }, - "include": ["src/**/*"] + "baseUrl": ".", + "paths": { + "@schemas/*": ["../../../workflow-languages/schemas/*"] + } + } } diff --git a/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts b/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts index 53887d3..acc7697 100644 --- a/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts +++ b/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts @@ -1,8 +1,8 @@ import { container } from "@gxwf/server-common/src/inversify.config"; import { CompletionItem, CompletionList, WorkflowDataProvider } from "@gxwf/server-common/src/languageTypes"; -import { WorkflowInput, WorkflowOutput } from "@gxwf/server-common/src/services/requestsDefinitions"; import { WorkflowTestsLanguageServiceContainerModule } from "@gxwf/workflow-tests-language-service/src/inversify.config"; import "reflect-metadata"; +import { WorkflowInput, WorkflowOutput } from "../../../../../shared/src/requestsDefinitions"; import { WorkflowTestsSchemaService } from "../../src/schema/service"; import { YAMLCompletionHelper } from "../../src/services/completion/helper"; import { TYPES } from "../../src/types"; diff --git a/server/tsconfig.json b/server/tsconfig.json index 244f2a1..33489a2 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -11,6 +11,7 @@ "sourceMap": true, "strict": true, "composite": true, + "baseUrl": ".", "paths": { "@schemas/*": ["../workflow-languages/schemas/*"] } diff --git a/client/src/common/requestsDefinitions.ts b/shared/src/requestsDefinitions.ts similarity index 92% rename from client/src/common/requestsDefinitions.ts rename to shared/src/requestsDefinitions.ts index 28643d3..19b9fd2 100644 --- a/client/src/common/requestsDefinitions.ts +++ b/shared/src/requestsDefinitions.ts @@ -1,7 +1,5 @@ /* eslint-disable @typescript-eslint/no-namespace */ -// TODO: Move the contents of this file to a shared lib https://github.com/Microsoft/vscode/issues/15829 - export namespace LSRequestIdentifiers { export const CLEAN_WORKFLOW_DOCUMENT = "galaxy-workflows-ls.cleanWorkflowDocument"; export const CLEAN_WORKFLOW_CONTENTS = "galaxy-workflows-ls.cleanWorkflowContents"; From a642627bd0a4ae5512060f96921a092acb03db8c Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sat, 11 May 2024 18:27:47 +0200 Subject: [PATCH 68/98] Implement getWorkflowInputs for format2 documents --- .../src/gxFormat2WorkflowDocument.ts | 31 +++++++++++++++- .../tests/integration/document.test.ts | 37 +++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 server/gx-workflow-ls-format2/tests/integration/document.test.ts diff --git a/server/gx-workflow-ls-format2/src/gxFormat2WorkflowDocument.ts b/server/gx-workflow-ls-format2/src/gxFormat2WorkflowDocument.ts index 87c60ed..9e84873 100644 --- a/server/gx-workflow-ls-format2/src/gxFormat2WorkflowDocument.ts +++ b/server/gx-workflow-ls-format2/src/gxFormat2WorkflowDocument.ts @@ -1,3 +1,4 @@ +import { PropertyASTNode } from "@gxwf/server-common/src/ast/types"; import { TextDocument, WorkflowDocument } from "@gxwf/server-common/src/languageTypes"; import { YAMLDocument } from "@gxwf/yaml-language-service/src"; import { GetWorkflowInputsResult, GetWorkflowOutputsResult } from "../../../shared/src/requestsDefinitions"; @@ -17,8 +18,36 @@ export class GxFormat2WorkflowDocument extends WorkflowDocument { } public getWorkflowInputs(): GetWorkflowInputsResult { - throw new Error("Method not implemented."); + const result: GetWorkflowInputsResult = { inputs: [] }; + const inputs = this.nodeManager.getNodeFromPath("inputs"); + if (inputs?.type === "property") { + const inputList = inputs.valueNode?.children; + if (inputList) { + inputList.forEach((input) => { + if (input.type !== "property" || !input.keyNode) return; + const inputName = String(input.keyNode.value); + const inputTypeNode = input.valueNode?.children?.find( + (prop) => prop.type === "property" && prop.keyNode.value === "type" + ) as PropertyASTNode; + const inputType = + !inputTypeNode || inputTypeNode.valueNode?.value === "File" ? "data_input" : "data_collection_input"; + const inputDocNode = input.valueNode?.children?.find( + (prop) => prop.type === "property" && prop.keyNode.value === "doc" + ) as PropertyASTNode; + const inputDescription = String(inputDocNode?.valueNode?.value ?? ""); + if (inputType) { + result.inputs.push({ + name: inputName, + description: inputDescription, + type: inputType, + }); + } + }); + } + } + return result; } + public getWorkflowOutputs(): GetWorkflowOutputsResult { throw new Error("Method not implemented."); } diff --git a/server/gx-workflow-ls-format2/tests/integration/document.test.ts b/server/gx-workflow-ls-format2/tests/integration/document.test.ts new file mode 100644 index 0000000..01f8180 --- /dev/null +++ b/server/gx-workflow-ls-format2/tests/integration/document.test.ts @@ -0,0 +1,37 @@ +import { createFormat2WorkflowDocument } from "../testHelpers"; + +describe("GxFormat2WorkflowDocument", () => { + it("should get workflow inputs", () => { + const documentContent = ` +class: GalaxyWorkflow +inputs: + input_1: data + input_2: + type: File + doc: This is the input 2 +outputs: + output_1: + outputSource: first_cat/out_file1 +steps: + first_cat: + tool_id: cat + in: + input1: input_1 + `; + const document = createFormat2WorkflowDocument(documentContent); + const result = document.getWorkflowInputs(); + expect(result.inputs.length).toBe(2); + expect(result.inputs).toEqual([ + { + name: "input_1", + description: "", + type: "data_input", + }, + { + name: "input_2", + description: "This is the input 2", + type: "data_input", + }, + ]); + }); +}); From 71dfa85c22e2e2701f1fc6097f31ec9eabe18ffb Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sun, 12 May 2024 02:25:46 +0200 Subject: [PATCH 69/98] Add test for getting format2 workflow inputs --- .../tests/integration/document.test.ts | 44 +++++++++++++------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/server/gx-workflow-ls-format2/tests/integration/document.test.ts b/server/gx-workflow-ls-format2/tests/integration/document.test.ts index 01f8180..eb28b39 100644 --- a/server/gx-workflow-ls-format2/tests/integration/document.test.ts +++ b/server/gx-workflow-ls-format2/tests/integration/document.test.ts @@ -2,35 +2,53 @@ import { createFormat2WorkflowDocument } from "../testHelpers"; describe("GxFormat2WorkflowDocument", () => { it("should get workflow inputs", () => { - const documentContent = ` + const TEST_WORKFLOW_CONTENT = ` class: GalaxyWorkflow inputs: input_1: data input_2: type: File doc: This is the input 2 -outputs: - output_1: - outputSource: first_cat/out_file1 -steps: - first_cat: - tool_id: cat - in: - input1: input_1 + the_collection: + type: collection + doc: This is a collection + input_int: integer + text_param: + optional: true + default: text value + restrictOnConnections: true + type: text `; - const document = createFormat2WorkflowDocument(documentContent); + const document = createFormat2WorkflowDocument(TEST_WORKFLOW_CONTENT); + const result = document.getWorkflowInputs(); - expect(result.inputs.length).toBe(2); + + expect(result.inputs.length).toBe(5); expect(result.inputs).toEqual([ { name: "input_1", description: "", - type: "data_input", + type: "data", }, { name: "input_2", description: "This is the input 2", - type: "data_input", + type: "File", + }, + { + name: "the_collection", + description: "This is a collection", + type: "collection", + }, + { + name: "input_int", + description: "", + type: "integer", + }, + { + name: "text_param", + description: "", + type: "text", }, ]); }); From 15f2d2458af6768c8f33845cc0208e978ff0197d Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sun, 12 May 2024 02:43:11 +0200 Subject: [PATCH 70/98] Define input types from format2 --- shared/src/requestsDefinitions.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/shared/src/requestsDefinitions.ts b/shared/src/requestsDefinitions.ts index 19b9fd2..7759f1f 100644 --- a/shared/src/requestsDefinitions.ts +++ b/shared/src/requestsDefinitions.ts @@ -28,7 +28,19 @@ export interface TargetWorkflowDocumentParams { uri: string; } -export type WorkflowInputType = "data_input" | "data_collection_input"; +export type WorkflowInputType = + | "null" + | "boolean" + | "int" + | "long" + | "float" + | "double" + | "string" + | "integer" + | "text" + | "File" + | "data" + | "collection"; export interface WorkflowInput { name: string; From beab66f926b199eaf17ba317d898e9c59a49ffd1 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sun, 12 May 2024 02:44:55 +0200 Subject: [PATCH 71/98] Fix input type parsing for format2 documents --- .../src/gxFormat2WorkflowDocument.ts | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/server/gx-workflow-ls-format2/src/gxFormat2WorkflowDocument.ts b/server/gx-workflow-ls-format2/src/gxFormat2WorkflowDocument.ts index 9e84873..0ece41c 100644 --- a/server/gx-workflow-ls-format2/src/gxFormat2WorkflowDocument.ts +++ b/server/gx-workflow-ls-format2/src/gxFormat2WorkflowDocument.ts @@ -1,7 +1,11 @@ import { PropertyASTNode } from "@gxwf/server-common/src/ast/types"; import { TextDocument, WorkflowDocument } from "@gxwf/server-common/src/languageTypes"; import { YAMLDocument } from "@gxwf/yaml-language-service/src"; -import { GetWorkflowInputsResult, GetWorkflowOutputsResult } from "../../../shared/src/requestsDefinitions"; +import { + GetWorkflowInputsResult, + GetWorkflowOutputsResult, + WorkflowInputType, +} from "../../../shared/src/requestsDefinitions"; /** * This class provides information about a gxformat2 workflow document structure. @@ -26,22 +30,16 @@ export class GxFormat2WorkflowDocument extends WorkflowDocument { inputList.forEach((input) => { if (input.type !== "property" || !input.keyNode) return; const inputName = String(input.keyNode.value); - const inputTypeNode = input.valueNode?.children?.find( - (prop) => prop.type === "property" && prop.keyNode.value === "type" - ) as PropertyASTNode; - const inputType = - !inputTypeNode || inputTypeNode.valueNode?.value === "File" ? "data_input" : "data_collection_input"; + const inputType = this.getInputType(input); const inputDocNode = input.valueNode?.children?.find( (prop) => prop.type === "property" && prop.keyNode.value === "doc" ) as PropertyASTNode; const inputDescription = String(inputDocNode?.valueNode?.value ?? ""); - if (inputType) { - result.inputs.push({ - name: inputName, - description: inputDescription, - type: inputType, - }); - } + result.inputs.push({ + name: inputName, + description: inputDescription, + type: inputType, + }); }); } } @@ -51,4 +49,18 @@ export class GxFormat2WorkflowDocument extends WorkflowDocument { public getWorkflowOutputs(): GetWorkflowOutputsResult { throw new Error("Method not implemented."); } + + private getInputType(input: PropertyASTNode): WorkflowInputType { + let inputType: WorkflowInputType = "data"; + const inputTypeNode = input.valueNode?.children?.find( + (prop) => prop.type === "property" && prop.keyNode.value === "type" + ) as PropertyASTNode; + if (inputTypeNode) { + inputType = String(inputTypeNode.valueNode?.value) as WorkflowInputType; + } else { + // If the type property is not specified, it might be defined in the value node itself + inputType = input.valueNode?.value as WorkflowInputType; + } + return inputType; + } } From b31245b31894ca66b83a3ed7db8a9a421ceb1e4d Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sun, 12 May 2024 18:20:00 +0200 Subject: [PATCH 72/98] Adapt format1 input types to format2 --- .../src/gxFormat2WorkflowDocument.ts | 4 ++-- .../src/nativeWorkflowDocument.ts | 19 +++++++++++++++++-- .../src/services/completion/helper.ts | 5 +++-- .../tests/unit/completion.test.ts | 6 +++--- 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/server/gx-workflow-ls-format2/src/gxFormat2WorkflowDocument.ts b/server/gx-workflow-ls-format2/src/gxFormat2WorkflowDocument.ts index 0ece41c..257e41d 100644 --- a/server/gx-workflow-ls-format2/src/gxFormat2WorkflowDocument.ts +++ b/server/gx-workflow-ls-format2/src/gxFormat2WorkflowDocument.ts @@ -30,7 +30,7 @@ export class GxFormat2WorkflowDocument extends WorkflowDocument { inputList.forEach((input) => { if (input.type !== "property" || !input.keyNode) return; const inputName = String(input.keyNode.value); - const inputType = this.getInputType(input); + const inputType = this.extractInputType(input); const inputDocNode = input.valueNode?.children?.find( (prop) => prop.type === "property" && prop.keyNode.value === "doc" ) as PropertyASTNode; @@ -50,7 +50,7 @@ export class GxFormat2WorkflowDocument extends WorkflowDocument { throw new Error("Method not implemented."); } - private getInputType(input: PropertyASTNode): WorkflowInputType { + private extractInputType(input: PropertyASTNode): WorkflowInputType { let inputType: WorkflowInputType = "data"; const inputTypeNode = input.valueNode?.children?.find( (prop) => prop.type === "property" && prop.keyNode.value === "type" diff --git a/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts b/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts index 6aaae53..eb643c6 100644 --- a/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts +++ b/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts @@ -2,7 +2,11 @@ import { ASTNode, ParsedDocument } from "@gxwf/server-common/src/ast/types"; import { TextDocument, WorkflowDocument } from "@gxwf/server-common/src/languageTypes"; import { isWorkflowInputType } from "@gxwf/server-common/src/utils"; import { JSONDocument } from "vscode-json-languageservice"; -import { GetWorkflowInputsResult, GetWorkflowOutputsResult } from "../../../shared/src/requestsDefinitions"; +import { + GetWorkflowInputsResult, + GetWorkflowOutputsResult, + WorkflowInputType, +} from "../../../shared/src/requestsDefinitions"; /** * This class provides information about a Native workflow document structure. @@ -42,7 +46,7 @@ export class NativeWorkflowDocument extends WorkflowDocument { result.inputs.push({ name: labelValue ?? "UNKNOWN", description: annotationValue, - type: stepTypeValue as "data_input" | "data_collection_input", + type: this.getInputType(stepTypeValue), }); } }); @@ -79,4 +83,15 @@ export class NativeWorkflowDocument extends WorkflowDocument { }); return result; } + + private getInputType(typeName: string): WorkflowInputType { + switch (typeName) { + case "data_input": + return "data"; + case "data_collection_input": + return "collection"; + default: + return "data"; + } + } } diff --git a/server/packages/workflow-tests-language-service/src/services/completion/helper.ts b/server/packages/workflow-tests-language-service/src/services/completion/helper.ts index d18ec02..5daa795 100644 --- a/server/packages/workflow-tests-language-service/src/services/completion/helper.ts +++ b/server/packages/workflow-tests-language-service/src/services/completion/helper.ts @@ -1019,7 +1019,8 @@ ${this.indentation}${this.indentation}$0 const type = matchingWorkflowInput.type; const DATA_INPUT_TYPE_OPTIONS = ["PathFile", "LocationFile", "CompositeDataFile"]; switch (type) { - case "data_input": + case "data": + case "File": matchingSchemas = matchingSchemas.filter( (schema) => schema.schema.title && DATA_INPUT_TYPE_OPTIONS.includes(schema.schema.title) ); @@ -1043,7 +1044,7 @@ ${this.indentation}${this.indentation}$0 return; } break; - case "data_collection_input": + case "collection": // The valid schema is "Collection" // TODO add class: Collection matchingSchemas = matchingSchemas.filter((schema) => schema.schema.title === "Collection"); diff --git a/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts b/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts index acc7697..4cab049 100644 --- a/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts +++ b/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts @@ -99,17 +99,17 @@ describe("Workflow Tests Completion Service", () => { const FAKE_DATASET_INPUT: WorkflowInput = { name: "My fake dataset", description: "This is a simple dataset", - type: "data_input", + type: "data", }; const FAKE_DATASET_INPUT_COLON: WorkflowInput = { name: "Input dataset: fake", description: "This is a simple dataset with a colon in the name", - type: "data_input", + type: "File", }; const FAKE_DATASET_COLLECTION_INPUT: WorkflowInput = { name: "My fake collection", description: "This is a collection", - type: "data_collection_input", + type: "collection", }; const FAKE_WORKFLOW_INPUTS: WorkflowInput[] = [ FAKE_DATASET_INPUT, From 2c9575f30ed4bab42f1485d9e91f52337a7ba0f1 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Mon, 13 May 2024 01:00:50 +0200 Subject: [PATCH 73/98] Refactor rename WorkflowInputType to WorkflowDataType --- .../src/gxFormat2WorkflowDocument.ts | 10 +++++----- .../src/nativeWorkflowDocument.ts | 4 ++-- server/packages/server-common/src/utils.ts | 4 ++-- shared/src/requestsDefinitions.ts | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/server/gx-workflow-ls-format2/src/gxFormat2WorkflowDocument.ts b/server/gx-workflow-ls-format2/src/gxFormat2WorkflowDocument.ts index 257e41d..6d09471 100644 --- a/server/gx-workflow-ls-format2/src/gxFormat2WorkflowDocument.ts +++ b/server/gx-workflow-ls-format2/src/gxFormat2WorkflowDocument.ts @@ -4,7 +4,7 @@ import { YAMLDocument } from "@gxwf/yaml-language-service/src"; import { GetWorkflowInputsResult, GetWorkflowOutputsResult, - WorkflowInputType, + WorkflowDataType, } from "../../../shared/src/requestsDefinitions"; /** @@ -50,16 +50,16 @@ export class GxFormat2WorkflowDocument extends WorkflowDocument { throw new Error("Method not implemented."); } - private extractInputType(input: PropertyASTNode): WorkflowInputType { - let inputType: WorkflowInputType = "data"; + private extractInputType(input: PropertyASTNode): WorkflowDataType { + let inputType: WorkflowDataType = "data"; const inputTypeNode = input.valueNode?.children?.find( (prop) => prop.type === "property" && prop.keyNode.value === "type" ) as PropertyASTNode; if (inputTypeNode) { - inputType = String(inputTypeNode.valueNode?.value) as WorkflowInputType; + inputType = String(inputTypeNode.valueNode?.value) as WorkflowDataType; } else { // If the type property is not specified, it might be defined in the value node itself - inputType = input.valueNode?.value as WorkflowInputType; + inputType = input.valueNode?.value as WorkflowDataType; } return inputType; } diff --git a/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts b/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts index eb643c6..99ae2a2 100644 --- a/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts +++ b/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts @@ -5,7 +5,7 @@ import { JSONDocument } from "vscode-json-languageservice"; import { GetWorkflowInputsResult, GetWorkflowOutputsResult, - WorkflowInputType, + WorkflowDataType, } from "../../../shared/src/requestsDefinitions"; /** @@ -84,7 +84,7 @@ export class NativeWorkflowDocument extends WorkflowDocument { return result; } - private getInputType(typeName: string): WorkflowInputType { + private getInputType(typeName: string): WorkflowDataType { switch (typeName) { case "data_input": return "data"; diff --git a/server/packages/server-common/src/utils.ts b/server/packages/server-common/src/utils.ts index 6b99333..1cd11a6 100644 --- a/server/packages/server-common/src/utils.ts +++ b/server/packages/server-common/src/utils.ts @@ -1,5 +1,5 @@ -import { WorkflowInputType } from "../../../../shared/src/requestsDefinitions"; +import { WorkflowDataType } from "../../../../shared/src/requestsDefinitions"; -export function isWorkflowInputType(input: string): input is WorkflowInputType { +export function isWorkflowInputType(input: string): input is WorkflowDataType { return input === "data_input" || input === "data_collection_input"; } diff --git a/shared/src/requestsDefinitions.ts b/shared/src/requestsDefinitions.ts index 7759f1f..836ff25 100644 --- a/shared/src/requestsDefinitions.ts +++ b/shared/src/requestsDefinitions.ts @@ -28,7 +28,7 @@ export interface TargetWorkflowDocumentParams { uri: string; } -export type WorkflowInputType = +export type WorkflowDataType = | "null" | "boolean" | "int" @@ -44,7 +44,7 @@ export type WorkflowInputType = export interface WorkflowInput { name: string; - type: WorkflowInputType; + type: WorkflowDataType; description: string; } From 3e69b47bb6f50242c95aab1e426eefcebd0b2c7c Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Wed, 15 May 2024 09:12:39 +0200 Subject: [PATCH 74/98] Implement format2 wf output completion --- .../src/gxFormat2WorkflowDocument.ts | 23 +++++++++++- .../tests/integration/document.test.ts | 37 ++++++++++++++++--- .../src/nativeWorkflowDocument.ts | 7 +--- .../src/services/completion/helper.ts | 6 +-- .../tests/unit/completion.test.ts | 19 ++++------ shared/src/requestsDefinitions.ts | 10 +++-- 6 files changed, 72 insertions(+), 30 deletions(-) diff --git a/server/gx-workflow-ls-format2/src/gxFormat2WorkflowDocument.ts b/server/gx-workflow-ls-format2/src/gxFormat2WorkflowDocument.ts index 6d09471..bf4d4c4 100644 --- a/server/gx-workflow-ls-format2/src/gxFormat2WorkflowDocument.ts +++ b/server/gx-workflow-ls-format2/src/gxFormat2WorkflowDocument.ts @@ -37,7 +37,7 @@ export class GxFormat2WorkflowDocument extends WorkflowDocument { const inputDescription = String(inputDocNode?.valueNode?.value ?? ""); result.inputs.push({ name: inputName, - description: inputDescription, + doc: inputDescription, type: inputType, }); }); @@ -47,7 +47,26 @@ export class GxFormat2WorkflowDocument extends WorkflowDocument { } public getWorkflowOutputs(): GetWorkflowOutputsResult { - throw new Error("Method not implemented."); + const result: GetWorkflowOutputsResult = { outputs: [] }; + const output = this.nodeManager.getNodeFromPath("outputs"); + if (output?.type === "property") { + const outputList = output.valueNode?.children; + if (outputList) { + outputList.forEach((output) => { + if (output.type !== "property" || !output.keyNode) return; + const outputName = String(output.keyNode.value); + const outputDocNode = output.valueNode?.children?.find( + (prop) => prop.type === "property" && prop.keyNode.value === "doc" + ) as PropertyASTNode; + const outputDoc = String(outputDocNode?.valueNode?.value ?? ""); + result.outputs.push({ + name: outputName, + doc: outputDoc, + }); + }); + } + } + return result; } private extractInputType(input: PropertyASTNode): WorkflowDataType { diff --git a/server/gx-workflow-ls-format2/tests/integration/document.test.ts b/server/gx-workflow-ls-format2/tests/integration/document.test.ts index eb28b39..d449b3c 100644 --- a/server/gx-workflow-ls-format2/tests/integration/document.test.ts +++ b/server/gx-workflow-ls-format2/tests/integration/document.test.ts @@ -27,29 +27,56 @@ inputs: expect(result.inputs).toEqual([ { name: "input_1", - description: "", + doc: "", type: "data", }, { name: "input_2", - description: "This is the input 2", + doc: "This is the input 2", type: "File", }, { name: "the_collection", - description: "This is a collection", + doc: "This is a collection", type: "collection", }, { name: "input_int", - description: "", + doc: "", type: "integer", }, { name: "text_param", - description: "", + doc: "", type: "text", }, ]); }); + + it("should get workflow outputs", () => { + const TEST_WORKFLOW_CONTENT = ` +class: GalaxyWorkflow +outputs: + output_1: + outputSource: second_cat/out_file1 + output_2: + outputSource: first_cat/out_file2 + doc: This is the output 2 + `; + const document = createFormat2WorkflowDocument(TEST_WORKFLOW_CONTENT); + + const result = document.getWorkflowOutputs(); + + expect(result.outputs.length).toBe(2); + expect(result.outputs).toEqual([ + { + name: "output_1", + doc: "", + }, + { + name: "output_2", + doc: "This is the output 2", + }, + ]); + }); }); diff --git a/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts b/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts index 99ae2a2..3107895 100644 --- a/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts +++ b/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts @@ -45,7 +45,7 @@ export class NativeWorkflowDocument extends WorkflowDocument { const annotationValue = String(annotationNode?.valueNode?.value); result.inputs.push({ name: labelValue ?? "UNKNOWN", - description: annotationValue, + doc: annotationValue, type: this.getInputType(stepTypeValue), }); } @@ -69,13 +69,10 @@ export class NativeWorkflowDocument extends WorkflowDocument { } const labelNode = workflowOutput.properties.find((property) => property.keyNode.value === "label"); const labelValue = String(labelNode?.valueNode?.value); - const outputNameNode = workflowOutput.properties.find((property) => property.keyNode.value === "output_name"); - const outputNameValue = String(outputNameNode?.valueNode?.value); const uuidNode = workflowOutput.properties.find((property) => property.keyNode.value === "uuid"); const uuidValue = String(uuidNode?.valueNode?.value); result.outputs.push({ - label: labelValue ?? "UNKNOWN", - output_name: outputNameValue, + name: labelValue, uuid: uuidValue, }); }); diff --git a/server/packages/workflow-tests-language-service/src/services/completion/helper.ts b/server/packages/workflow-tests-language-service/src/services/completion/helper.ts index 5daa795..43031b8 100644 --- a/server/packages/workflow-tests-language-service/src/services/completion/helper.ts +++ b/server/packages/workflow-tests-language-service/src/services/completion/helper.ts @@ -989,7 +989,7 @@ ${this.indentation}${this.indentation}$0 label: input.name, insertText: `${this.quoteIfColon(input.name)}:`, insertTextFormat: InsertTextFormat.Snippet, - documentation: this.fromMarkup(input.description), + documentation: this.fromMarkup(input.doc), }); }); return; @@ -1001,8 +1001,8 @@ ${this.indentation}${this.indentation}$0 workflowOutputs.forEach((output) => { collector.add({ kind: CompletionItemKind.Property, - label: output.label, - insertText: `${this.quoteIfColon(output.label)}:`, + label: output.name, + insertText: `${this.quoteIfColon(output.name)}:`, insertTextFormat: InsertTextFormat.Snippet, }); }); diff --git a/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts b/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts index 4cab049..cdedcc4 100644 --- a/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts +++ b/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts @@ -98,17 +98,17 @@ describe("Workflow Tests Completion Service", () => { let workflowDataProviderMock: WorkflowDataProvider; const FAKE_DATASET_INPUT: WorkflowInput = { name: "My fake dataset", - description: "This is a simple dataset", + doc: "This is a simple dataset", type: "data", }; const FAKE_DATASET_INPUT_COLON: WorkflowInput = { name: "Input dataset: fake", - description: "This is a simple dataset with a colon in the name", + doc: "This is a simple dataset with a colon in the name", type: "File", }; const FAKE_DATASET_COLLECTION_INPUT: WorkflowInput = { name: "My fake collection", - description: "This is a collection", + doc: "This is a collection", type: "collection", }; const FAKE_WORKFLOW_INPUTS: WorkflowInput[] = [ @@ -118,18 +118,15 @@ describe("Workflow Tests Completion Service", () => { ]; const FAKE_WORKFLOW_OUTPUTS: WorkflowOutput[] = [ { - label: "My output", - output_name: "output", + name: "My output", uuid: "1234-5678-91011-1213", }, { - label: "My second output", - output_name: "output2", + name: "My second output", uuid: "1234-5678-91011-1214", }, { - label: "My third output: with colon", - output_name: "output3", + name: "My third output: with colon", uuid: "1234-5678-91011-1215", }, ]; @@ -255,7 +252,7 @@ describe("Workflow Tests Completion Service", () => { for (let index = 0; index < FAKE_WORKFLOW_OUTPUTS.length; index++) { const workflowOutput = FAKE_WORKFLOW_OUTPUTS[index]; const completionItem = completions!.items[index]; - expect(completionItem.label).toEqual(workflowOutput.label); + expect(completionItem.label).toEqual(workflowOutput.name); } }); }); @@ -295,5 +292,5 @@ function expectCompletionItemDocumentationToContain(completionItem: CompletionIt function expectCompletionItemToMatchWorkflowInput(completionItem: CompletionItem, workflowInput: WorkflowInput): void { expect(completionItem.label).toEqual(workflowInput.name); - expectCompletionItemDocumentationToContain(completionItem, workflowInput.description); + expectCompletionItemDocumentationToContain(completionItem, workflowInput.doc); } diff --git a/shared/src/requestsDefinitions.ts b/shared/src/requestsDefinitions.ts index 836ff25..39c2754 100644 --- a/shared/src/requestsDefinitions.ts +++ b/shared/src/requestsDefinitions.ts @@ -45,17 +45,19 @@ export type WorkflowDataType = export interface WorkflowInput { name: string; type: WorkflowDataType; - description: string; + doc: string; } export interface GetWorkflowInputsResult { inputs: WorkflowInput[]; } +//TODO: unify format1 and format2 output definitions export interface WorkflowOutput { - label: string; - output_name: string; - uuid: string; + name: string; + uuid?: string; + doc?: string; + type?: WorkflowDataType; } export interface GetWorkflowOutputsResult { From f64009637096aad193592ca6a36eff5f21237f4e Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Wed, 15 May 2024 11:28:33 +0200 Subject: [PATCH 75/98] Add test for native wf inputs --- .../tests/unit/nativeWorkflowDocument.test.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/server/gx-workflow-ls-native/tests/unit/nativeWorkflowDocument.test.ts b/server/gx-workflow-ls-native/tests/unit/nativeWorkflowDocument.test.ts index 3aa6217..8d5eced 100644 --- a/server/gx-workflow-ls-native/tests/unit/nativeWorkflowDocument.test.ts +++ b/server/gx-workflow-ls-native/tests/unit/nativeWorkflowDocument.test.ts @@ -14,4 +14,36 @@ describe("NativeWorkflowDocument", () => { expect(stepNodes).toHaveLength(expectedNumSteps); }); }); + describe("getWorkflowInputs", () => { + it.each([ + ["", 0], + [TestWorkflowProvider.workflows.validation.withoutSteps, 0], + [TestWorkflowProvider.workflows.validation.withOneStep, 1], + [TestWorkflowProvider.workflows.validation.withThreeSteps, 2], + [TestWorkflowProvider.workflows.validation.withWorkflowOutputLabels, 1], + [TestWorkflowProvider.workflows.validation.withoutWorkflowOutputLabels, 1], + ])("returns the expected number of inputs", (wf_content: string, expectedNumInputs: number) => { + const document = createNativeWorkflowDocument(wf_content); + const result = document.getWorkflowInputs(); + expect(result.inputs).toHaveLength(expectedNumInputs); + }); + + it("should return the expected information of the inputs", () => { + const document = createNativeWorkflowDocument(TestWorkflowProvider.workflows.validation.withThreeSteps); + const result = document.getWorkflowInputs(); + expect(result.inputs).toHaveLength(2); + expect(result.inputs).toEqual([ + { + name: "WorkflowInput1", + doc: "input1 description", + type: "data", + }, + { + name: "WorkflowInput2", + doc: "", + type: "data", + }, + ]); + }); + }); }); From 7fb7528d7ee8118d0c960209bc9c9d99b8551346 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Wed, 15 May 2024 11:29:12 +0200 Subject: [PATCH 76/98] Fix getWorkflowInputs for .ga workflows --- .../src/nativeWorkflowDocument.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts b/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts index 3107895..a2c8d05 100644 --- a/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts +++ b/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts @@ -40,12 +40,23 @@ export class NativeWorkflowDocument extends WorkflowDocument { const stepTypeValue = String(stepTypeNode?.valueNode?.value); if (isWorkflowInputType(stepTypeValue)) { const labelNode = step.properties.find((property) => property.keyNode.value === "label"); - const labelValue = String(labelNode?.valueNode?.value); + let labelValue = labelNode?.valueNode?.value; const annotationNode = step.properties.find((property) => property.keyNode.value === "annotation"); - const annotationValue = String(annotationNode?.valueNode?.value); + let annotationValue = annotationNode?.valueNode?.value; + if (!labelNode) { + const inputs = step.properties.find((property) => property.keyNode.value === "inputs"); + if (inputs?.valueNode && inputs.valueNode.type === "array") { + const input = inputs?.valueNode.items.at(0); + if (input && input.type === "object") { + labelValue = input.properties.find((p) => p.keyNode.value === "name")?.valueNode?.value ?? labelValue; + annotationValue = + input.properties.find((p) => p.keyNode.value === "description")?.valueNode?.value ?? annotationValue; + } + } + } result.inputs.push({ - name: labelValue ?? "UNKNOWN", - doc: annotationValue, + name: String(labelValue), + doc: String(annotationValue ?? ""), type: this.getInputType(stepTypeValue), }); } From 01995fe6a67cb949488bfadeecab2a2d05c0c2fd Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Fri, 24 May 2024 18:17:07 +0200 Subject: [PATCH 77/98] Add tests for getting workflow outputs --- .../tests/unit/nativeWorkflowDocument.test.ts | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/server/gx-workflow-ls-native/tests/unit/nativeWorkflowDocument.test.ts b/server/gx-workflow-ls-native/tests/unit/nativeWorkflowDocument.test.ts index 8d5eced..bed7057 100644 --- a/server/gx-workflow-ls-native/tests/unit/nativeWorkflowDocument.test.ts +++ b/server/gx-workflow-ls-native/tests/unit/nativeWorkflowDocument.test.ts @@ -14,6 +14,7 @@ describe("NativeWorkflowDocument", () => { expect(stepNodes).toHaveLength(expectedNumSteps); }); }); + describe("getWorkflowInputs", () => { it.each([ ["", 0], @@ -46,4 +47,52 @@ describe("NativeWorkflowDocument", () => { ]); }); }); + + describe("getWorkflowOutputs", () => { + it.each([ + ["", 0], + [TestWorkflowProvider.workflows.validation.withoutSteps, 0], + [TestWorkflowProvider.workflows.validation.withOneStep, 0], + [TestWorkflowProvider.workflows.validation.withThreeSteps, 0], + [TestWorkflowProvider.workflows.validation.withWorkflowOutputLabels, 2], + [TestWorkflowProvider.workflows.validation.withoutWorkflowOutputLabels, 2], + ])("returns the expected number of outputs", (wf_content: string, expectedNumInputs: number) => { + const document = createNativeWorkflowDocument(wf_content); + const result = document.getWorkflowOutputs(); + expect(result.outputs).toHaveLength(expectedNumInputs); + }); + + it("should return the expected information of the outputs with labels", () => { + const document = createNativeWorkflowDocument(TestWorkflowProvider.workflows.validation.withWorkflowOutputLabels); + const result = document.getWorkflowOutputs(); + expect(result.outputs).toHaveLength(2); + expect(result.outputs).toEqual([ + { + name: "The first output", + uuid: "7f08baab-5426-427e-9640-85815d809261", + }, + { + name: "The second output", + uuid: "b58fce9c-e507-4714-abfc-739607e02eed", + }, + ]); + }); + it("should return the expected information of the outputs without labels", () => { + const document = createNativeWorkflowDocument( + TestWorkflowProvider.workflows.validation.withoutWorkflowOutputLabels + ); + const result = document.getWorkflowOutputs(); + expect(result.outputs).toHaveLength(2); + expect(result.outputs).toEqual([ + { + name: "output1", + uuid: "7f08baab-5426-427e-9640-85815d809261", + }, + { + name: "output2", + uuid: "b58fce9c-e507-4714-abfc-739607e02eed", + }, + ]); + }); + }); }); From 9f86288731d2e6d61cfbf6493a506d013f50510b Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Fri, 24 May 2024 18:18:08 +0200 Subject: [PATCH 78/98] Fix getWorkflowOutputs for .ga workflows --- .../gx-workflow-ls-native/src/nativeWorkflowDocument.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts b/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts index a2c8d05..3c40b3b 100644 --- a/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts +++ b/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts @@ -79,11 +79,15 @@ export class NativeWorkflowDocument extends WorkflowDocument { return; } const labelNode = workflowOutput.properties.find((property) => property.keyNode.value === "label"); - const labelValue = String(labelNode?.valueNode?.value); + let labelValue = labelNode?.valueNode?.value; + if (!labelValue) { + labelValue = workflowOutput.properties.find((property) => property.keyNode.value === "output_name") + ?.valueNode?.value; + } const uuidNode = workflowOutput.properties.find((property) => property.keyNode.value === "uuid"); const uuidValue = String(uuidNode?.valueNode?.value); result.outputs.push({ - name: labelValue, + name: String(labelValue), uuid: uuidValue, }); }); From 7f13bb9809d4cc83793cf892aa4de3489cfe5f61 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sat, 25 May 2024 15:19:01 +0200 Subject: [PATCH 79/98] Add wf test language server support to browser extension --- client/src/browser/extension.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/client/src/browser/extension.ts b/client/src/browser/extension.ts index 6e00e5b..1698493 100644 --- a/client/src/browser/extension.ts +++ b/client/src/browser/extension.ts @@ -9,11 +9,11 @@ let gxFormat2LanguageClient: LanguageClient; export function activate(context: ExtensionContext): void { nativeLanguageClient = createWebWorkerLanguageClient( - Constants.NATIVE_WORKFLOW_LANGUAGE_ID, + [Constants.NATIVE_WORKFLOW_LANGUAGE_ID], Uri.joinPath(context.extensionUri, "server/gx-workflow-ls-native/dist/web/nativeServer.js") ); gxFormat2LanguageClient = createWebWorkerLanguageClient( - Constants.GXFORMAT2_WORKFLOW_LANGUAGE_ID, + [Constants.GXFORMAT2_WORKFLOW_LANGUAGE_ID, Constants.GXFORMAT2_WORKFLOW_TESTS_LANGUAGE_ID], Uri.joinPath(context.extensionUri, "server/gx-workflow-ls-format2/dist/web/gxFormat2Server.js") ); @@ -25,9 +25,14 @@ export async function deactivate(): Promise { await gxFormat2LanguageClient?.stop(); } -function createWebWorkerLanguageClient(languageId: string, serverUri: Uri): LanguageClient { - const documentSelector = [{ language: languageId }]; +function createWebWorkerLanguageClient(languageIds: string[], serverUri: Uri): LanguageClient { + const documentSelector = languageIds.map((languageId) => ({ language: languageId })); const clientOptions: LanguageClientOptions = buildBasicLanguageClientOptions(documentSelector); const worker = new Worker(serverUri.toString()); - return new LanguageClient(`${languageId}-language-client`, `Galaxy Workflows (${languageId})`, clientOptions, worker); + return new LanguageClient( + `${languageIds}-language-client`, + `Galaxy Workflows (${languageIds})`, + clientOptions, + worker + ); } From f07436481d52e309582fd43465f58bd14928856a Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sat, 25 May 2024 15:25:49 +0200 Subject: [PATCH 80/98] Add script to run extension in browser --- package-lock.json | 1902 ++++++++++++++++++++++++++++++++++++++++++++- package.json | 6 +- 2 files changed, 1887 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index b32f4ce..fe78658 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,8 @@ "@types/webpack-env": "^1.18.1", "@typescript-eslint/eslint-plugin": "^6.5.0", "@typescript-eslint/parser": "^6.5.0", - "@vscode/test-electron": "^2.3.10", + "@vscode/test-electron": "^2.3.4", + "@vscode/test-web": "^0.0.54", "assert": "^2.0.0", "concurrently": "^8.2.1", "eslint": "^8.48.0", @@ -1436,6 +1437,34 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@koa/cors": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@koa/cors/-/cors-5.0.0.tgz", + "integrity": "sha512-x/iUDjcS90W69PryLDIMgFyV21YLTnG9zOpPXS7Bkt2b8AsY3zZsIpOLBkYr9fBcF3HbkKaER5hOBZLfpLgYNw==", + "dev": true, + "dependencies": { + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@koa/router": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@koa/router/-/router-12.0.1.tgz", + "integrity": "sha512-ribfPYfHb+Uw3b27Eiw6NPqjhIhTpVFzEWLwyc/1Xp+DCdwRRyIlAUODX+9bPARF6aQtUu1+/PHzdNvRzcs/+Q==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "http-errors": "^2.0.0", + "koa-compose": "^4.1.0", + "methods": "^1.1.2", + "path-to-regexp": "^6.2.1" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1501,6 +1530,19 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@playwright/browser-chromium": { + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/@playwright/browser-chromium/-/browser-chromium-1.44.1.tgz", + "integrity": "sha512-ixlllsDKMzeCfU0GvlneqUtPu2jqak5r8BD0kRyJO5gNkOMa9UxippK1Ubgi7yqWn4RXvxFRoLSOeh/w6PHFrQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "playwright-core": "1.44.1" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -1905,6 +1947,72 @@ "node": ">=16" } }, + "node_modules/@vscode/test-web": { + "version": "0.0.54", + "resolved": "https://registry.npmjs.org/@vscode/test-web/-/test-web-0.0.54.tgz", + "integrity": "sha512-LDdFFEzmnKQ96QmuRxK9kRhddQmwqyI/VOb6YhyPqOsbKBIM7uzTqpckoueOu9EEnvPSNQNoxqb426AakI8e5w==", + "dev": true, + "dependencies": { + "@koa/cors": "^5.0.0", + "@koa/router": "^12.0.1", + "@playwright/browser-chromium": "^1.43.1", + "gunzip-maybe": "^1.4.2", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.4", + "koa": "^2.15.3", + "koa-morgan": "^1.0.1", + "koa-mount": "^4.0.0", + "koa-static": "^5.0.0", + "minimist": "^1.2.8", + "playwright": "^1.43.1", + "tar-fs": "^3.0.5", + "vscode-uri": "^3.0.8" + }, + "bin": { + "vscode-test-web": "out/index.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@vscode/test-web/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@vscode/test-web/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@vscode/test-web/node_modules/https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", @@ -2107,6 +2215,19 @@ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", @@ -2286,6 +2407,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/b4a": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", + "dev": true + }, "node_modules/babel-jest": { "version": "29.6.4", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.6.4.tgz", @@ -2408,6 +2535,70 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/bare-events": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.2.2.tgz", + "integrity": "sha512-h7z00dWdG0PYOQEvChhOSWvOfkIKsdZGkWr083FgN/HyoQuebSew/cgirYqh9SCuy/hRvxc5Vy6Fw8xAmYHLkQ==", + "dev": true, + "optional": true + }, + "node_modules/bare-fs": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.0.tgz", + "integrity": "sha512-TNFqa1B4N99pds2a5NYHR15o0ZpdNKbAeKTE/+G6ED/UeOavv8RY3dr/Fu99HW3zU3pXpo2kDNO8Sjsm2esfOw==", + "dev": true, + "optional": true, + "dependencies": { + "bare-events": "^2.0.0", + "bare-path": "^2.0.0", + "bare-stream": "^1.0.0" + } + }, + "node_modules/bare-os": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.3.0.tgz", + "integrity": "sha512-oPb8oMM1xZbhRQBngTgpcQ5gXw6kjOaRsSWsIeNyRxGed2w/ARyP7ScBYpWR1qfX2E5rS3gBw6OWcSQo+s+kUg==", + "dev": true, + "optional": true + }, + "node_modules/bare-path": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz", + "integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==", + "dev": true, + "optional": true, + "dependencies": { + "bare-os": "^2.1.0" + } + }, + "node_modules/bare-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-1.0.0.tgz", + "integrity": "sha512-KhNUoDL40iP4gFaLSsoGE479t0jHijfYdIcxRn/XtezA2BaUD0NRf/JGRpsMq6dMNM+SrCrB0YSSo/5wBY4rOQ==", + "dev": true, + "optional": true, + "dependencies": { + "streamx": "^2.16.1" + } + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dev": true, + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, "node_modules/big-integer": { "version": "1.6.51", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", @@ -2475,6 +2666,21 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "node_modules/browserify-zlib": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", + "integrity": "sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ==", + "dev": true, + "dependencies": { + "pako": "~0.2.0" + } + }, + "node_modules/browserify-zlib/node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "dev": true + }, "node_modules/browserslist": { "version": "4.20.3", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.3.tgz", @@ -2546,6 +2752,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cache-content-type": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", + "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", + "dev": true, + "dependencies": { + "mime-types": "^2.1.18", + "ylru": "^1.2.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -2850,6 +3069,27 @@ "node": ">=12" } }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", @@ -2865,6 +3105,19 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true }, + "node_modules/cookies": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz", + "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==", + "dev": true, + "dependencies": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -2944,6 +3197,12 @@ } } }, + "node_modules/deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==", + "dev": true + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3127,6 +3386,31 @@ "node": ">= 0.4" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -3178,12 +3462,30 @@ "node": ">=6.0.0" } }, + "node_modules/duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, "node_modules/electron-to-chromium": { "version": "1.4.135", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.135.tgz", @@ -3217,6 +3519,24 @@ "node": ">= 4" } }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.15.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", @@ -3323,6 +3643,12 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -3639,6 +3965,12 @@ "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", "dev": true }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true + }, "node_modules/fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -3820,6 +4152,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -4000,6 +4341,23 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/gunzip-maybe": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz", + "integrity": "sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==", + "dev": true, + "dependencies": { + "browserify-zlib": "^0.1.4", + "is-deflate": "^1.0.0", + "is-gzip": "^1.0.0", + "peek-stream": "^1.1.0", + "pumpify": "^1.3.3", + "through2": "^2.0.3" + }, + "bin": { + "gunzip-maybe": "bin.js" + } + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -4072,6 +4430,69 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/http-assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz", + "integrity": "sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==", + "dev": true, + "dependencies": { + "deep-equal": "~1.0.1", + "http-errors": "~1.8.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-assert/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-assert/node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-assert/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/http-proxy-agent": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", @@ -4307,6 +4728,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-deflate": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-deflate/-/is-deflate-1.0.0.tgz", + "integrity": "sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ==", + "dev": true + }, "node_modules/is-docker": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", @@ -4376,6 +4803,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-gzip": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-gzip/-/is-gzip-1.0.0.tgz", + "integrity": "sha512-rcfALRIb1YewtnksfRIHGcIY93QnK8BIQ/2c9yDYcG/Y6+vRoJuTWBmmSEbyLLYtXm7q35pHOHbZFQBaLrhlWQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-inside-container": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", @@ -5462,6 +5898,18 @@ "setimmediate": "^1.0.5" } }, + "node_modules/keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "dev": true, + "dependencies": { + "tsscmp": "1.0.6" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -5480,29 +5928,208 @@ "node": ">=6" } }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "node_modules/koa": { + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.15.3.tgz", + "integrity": "sha512-j/8tY9j5t+GVMLeioLaxweJiKUayFhlGqNTzf2ZGwL0ZCQijd2RLHK0SLW5Tsko8YyyqCZC2cojIb0/s62qTAg==", "dev": true, "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" + "accepts": "^1.3.5", + "cache-content-type": "^1.0.0", + "content-disposition": "~0.5.2", + "content-type": "^1.0.4", + "cookies": "~0.9.0", + "debug": "^4.3.2", + "delegates": "^1.0.0", + "depd": "^2.0.0", + "destroy": "^1.0.4", + "encodeurl": "^1.0.2", + "escape-html": "^1.0.3", + "fresh": "~0.5.2", + "http-assert": "^1.3.0", + "http-errors": "^1.6.3", + "is-generator-function": "^1.0.7", + "koa-compose": "^4.1.0", + "koa-convert": "^2.0.0", + "on-finished": "^2.3.0", + "only": "~0.0.2", + "parseurl": "^1.3.2", + "statuses": "^1.5.0", + "type-is": "^1.6.16", + "vary": "^1.1.2" }, "engines": { - "node": ">= 0.8.0" + "node": "^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4" } }, - "node_modules/lie": { + "node_modules/koa-compose": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", + "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==", + "dev": true + }, + "node_modules/koa-convert": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-2.0.0.tgz", + "integrity": "sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==", + "dev": true, + "dependencies": { + "co": "^4.6.0", + "koa-compose": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/koa-morgan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/koa-morgan/-/koa-morgan-1.0.1.tgz", + "integrity": "sha512-JOUdCNlc21G50afBXfErUrr1RKymbgzlrO5KURY+wmDG1Uvd2jmxUJcHgylb/mYXy2SjiNZyYim/ptUBGsIi3A==", + "dev": true, + "dependencies": { + "morgan": "^1.6.1" + } + }, + "node_modules/koa-mount": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/koa-mount/-/koa-mount-4.0.0.tgz", + "integrity": "sha512-rm71jaA/P+6HeCpoRhmCv8KVBIi0tfGuO/dMKicbQnQW/YJntJ6MnnspkodoA4QstMVEZArsCphmd0bJEtoMjQ==", + "dev": true, + "dependencies": { + "debug": "^4.0.1", + "koa-compose": "^4.1.0" + }, + "engines": { + "node": ">= 7.6.0" + } + }, + "node_modules/koa-send": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/koa-send/-/koa-send-5.0.1.tgz", + "integrity": "sha512-tmcyQ/wXXuxpDxyNXv5yNNkdAMdFRqwtegBXUaowiQzUKqJehttS0x2j0eOZDQAyloAth5w6wwBImnFzkUz3pQ==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "http-errors": "^1.7.3", + "resolve-path": "^1.4.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/koa-send/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa-send/node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa-send/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa-static": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/koa-static/-/koa-static-5.0.0.tgz", + "integrity": "sha512-UqyYyH5YEXaJrf9S8E23GoJFQZXkBVJ9zYYMPGz919MSX1KuvAcycIuS0ci150HCoPf4XQVhQ84Qf8xRPWxFaQ==", + "dev": true, + "dependencies": { + "debug": "^3.1.0", + "koa-send": "^5.0.0" + }, + "engines": { + "node": ">= 7.6.0" + } + }, + "node_modules/koa-static/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/koa/node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa/node_modules/http-errors/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lie": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", @@ -5631,6 +6258,15 @@ "tmpl": "1.0.5" } }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/merge-options": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz", @@ -5658,6 +6294,15 @@ "node": ">= 8" } }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/micromatch": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", @@ -5713,6 +6358,15 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.3.tgz", @@ -5804,6 +6458,49 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "dev": true, + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -5828,6 +6525,15 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -5919,6 +6625,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -5943,6 +6670,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/only": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", + "integrity": "sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==", + "dev": true + }, "node_modules/open": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz", @@ -6053,6 +6786,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -6117,6 +6859,12 @@ "node": "14 || >=16.14" } }, + "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 + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -6126,6 +6874,17 @@ "node": ">=8" } }, + "node_modules/peek-stream": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/peek-stream/-/peek-stream-1.1.3.tgz", + "integrity": "sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "duplexify": "^3.5.0", + "through2": "^2.0.3" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -6217,6 +6976,36 @@ "node": ">=8" } }, + "node_modules/playwright": { + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz", + "integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==", + "dev": true, + "dependencies": { + "playwright-core": "1.44.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz", + "integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -6307,6 +7096,27 @@ "node": ">= 6" } }, + "node_modules/pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, + "dependencies": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + } + }, "node_modules/punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -6352,6 +7162,12 @@ } ] }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -6474,6 +7290,64 @@ "node": ">=4" } }, + "node_modules/resolve-path": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/resolve-path/-/resolve-path-1.4.0.tgz", + "integrity": "sha512-i1xevIst/Qa+nA9olDxLWnLk8YZbi8R/7JPbCMcgyWaFR6bKWaexgJgEB5oc2PKMjYdrHynyz0NY+if+H98t1w==", + "dev": true, + "dependencies": { + "http-errors": "~1.6.2", + "path-is-absolute": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/resolve-path/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/resolve-path/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/resolve-path/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "node_modules/resolve-path/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "node_modules/resolve-path/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/resolve.exports": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", @@ -6672,6 +7546,12 @@ "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "dev": true }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, "node_modules/shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", @@ -6801,6 +7681,34 @@ "node": ">=8" } }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "dev": true + }, + "node_modules/streamx": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.16.1.tgz", + "integrity": "sha512-m9QYj6WygWyWa3H1YY69amr4nVgy61xfjys7xO7kviL5rfIEc2naf+ewFiOA+aEJD7y0JO3h2GoiUv4TDwEGzQ==", + "dev": true, + "dependencies": { + "fast-fifo": "^1.1.0", + "queue-tick": "^1.0.1" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -6988,6 +7896,41 @@ "node": ">=6" } }, + "node_modules/tar-fs": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", + "dev": true, + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0" + } + }, + "node_modules/tar-fs/node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, "node_modules/terser": { "version": "5.19.3", "resolved": "https://registry.npmjs.org/terser/-/terser-5.19.3.tgz", @@ -7069,6 +8012,16 @@ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, "node_modules/titleize": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", @@ -7108,6 +8061,15 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -7206,6 +8168,15 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "dev": true }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "dev": true, + "engines": { + "node": ">=0.6.x" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -7239,6 +8210,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typescript": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", @@ -7319,6 +8303,21 @@ "node": ">=10.12.0" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "dev": true + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -7581,6 +8580,15 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -7661,6 +8669,15 @@ "node": ">=10" } }, + "node_modules/ylru": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.4.0.tgz", + "integrity": "sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -8741,6 +9758,28 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "@koa/cors": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@koa/cors/-/cors-5.0.0.tgz", + "integrity": "sha512-x/iUDjcS90W69PryLDIMgFyV21YLTnG9zOpPXS7Bkt2b8AsY3zZsIpOLBkYr9fBcF3HbkKaER5hOBZLfpLgYNw==", + "dev": true, + "requires": { + "vary": "^1.1.2" + } + }, + "@koa/router": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@koa/router/-/router-12.0.1.tgz", + "integrity": "sha512-ribfPYfHb+Uw3b27Eiw6NPqjhIhTpVFzEWLwyc/1Xp+DCdwRRyIlAUODX+9bPARF6aQtUu1+/PHzdNvRzcs/+Q==", + "dev": true, + "requires": { + "debug": "^4.3.4", + "http-errors": "^2.0.0", + "koa-compose": "^4.1.0", + "methods": "^1.1.2", + "path-to-regexp": "^6.2.1" + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -8788,6 +9827,15 @@ "tslib": "^2.6.0" } }, + "@playwright/browser-chromium": { + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/@playwright/browser-chromium/-/browser-chromium-1.44.1.tgz", + "integrity": "sha512-ixlllsDKMzeCfU0GvlneqUtPu2jqak5r8BD0kRyJO5gNkOMa9UxippK1Ubgi7yqWn4RXvxFRoLSOeh/w6PHFrQ==", + "dev": true, + "requires": { + "playwright-core": "1.44.1" + } + }, "@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -9097,6 +10145,59 @@ "semver": "^7.5.2" } }, + "@vscode/test-web": { + "version": "0.0.54", + "resolved": "https://registry.npmjs.org/@vscode/test-web/-/test-web-0.0.54.tgz", + "integrity": "sha512-LDdFFEzmnKQ96QmuRxK9kRhddQmwqyI/VOb6YhyPqOsbKBIM7uzTqpckoueOu9EEnvPSNQNoxqb426AakI8e5w==", + "dev": true, + "requires": { + "@koa/cors": "^5.0.0", + "@koa/router": "^12.0.1", + "@playwright/browser-chromium": "^1.43.1", + "gunzip-maybe": "^1.4.2", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.4", + "koa": "^2.15.3", + "koa-morgan": "^1.0.1", + "koa-mount": "^4.0.0", + "koa-static": "^5.0.0", + "minimist": "^1.2.8", + "playwright": "^1.43.1", + "tar-fs": "^3.0.5", + "vscode-uri": "^3.0.8" + }, + "dependencies": { + "agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "requires": { + "debug": "^4.3.4" + } + }, + "http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "requires": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + } + }, + "https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "dev": true, + "requires": { + "agent-base": "^7.0.2", + "debug": "4" + } + } + } + }, "@webassemblyjs/ast": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", @@ -9276,6 +10377,16 @@ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, "acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", @@ -9402,6 +10513,12 @@ "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", "dev": true }, + "b4a": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", + "dev": true + }, "babel-jest": { "version": "29.6.4", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.6.4.tgz", @@ -9499,6 +10616,69 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "bare-events": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.2.2.tgz", + "integrity": "sha512-h7z00dWdG0PYOQEvChhOSWvOfkIKsdZGkWr083FgN/HyoQuebSew/cgirYqh9SCuy/hRvxc5Vy6Fw8xAmYHLkQ==", + "dev": true, + "optional": true + }, + "bare-fs": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.0.tgz", + "integrity": "sha512-TNFqa1B4N99pds2a5NYHR15o0ZpdNKbAeKTE/+G6ED/UeOavv8RY3dr/Fu99HW3zU3pXpo2kDNO8Sjsm2esfOw==", + "dev": true, + "optional": true, + "requires": { + "bare-events": "^2.0.0", + "bare-path": "^2.0.0", + "bare-stream": "^1.0.0" + } + }, + "bare-os": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.3.0.tgz", + "integrity": "sha512-oPb8oMM1xZbhRQBngTgpcQ5gXw6kjOaRsSWsIeNyRxGed2w/ARyP7ScBYpWR1qfX2E5rS3gBw6OWcSQo+s+kUg==", + "dev": true, + "optional": true + }, + "bare-path": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz", + "integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==", + "dev": true, + "optional": true, + "requires": { + "bare-os": "^2.1.0" + } + }, + "bare-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-1.0.0.tgz", + "integrity": "sha512-KhNUoDL40iP4gFaLSsoGE479t0jHijfYdIcxRn/XtezA2BaUD0NRf/JGRpsMq6dMNM+SrCrB0YSSo/5wBY4rOQ==", + "dev": true, + "optional": true, + "requires": { + "streamx": "^2.16.1" + } + }, + "basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + }, "big-integer": { "version": "1.6.51", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", @@ -9551,6 +10731,23 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "browserify-zlib": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", + "integrity": "sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ==", + "dev": true, + "requires": { + "pako": "~0.2.0" + }, + "dependencies": { + "pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "dev": true + } + } + }, "browserslist": { "version": "4.20.3", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.3.tgz", @@ -9597,6 +10794,16 @@ "run-applescript": "^5.0.0" } }, + "cache-content-type": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", + "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", + "dev": true, + "requires": { + "mime-types": "^2.1.18", + "ylru": "^1.2.0" + } + }, "call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -9813,6 +11020,21 @@ } } }, + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true + }, "convert-source-map": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", @@ -9830,6 +11052,16 @@ } } }, + "cookies": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz", + "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==", + "dev": true, + "requires": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + } + }, "core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -9878,6 +11110,12 @@ "dev": true, "requires": {} }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==", + "dev": true + }, "deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -9994,6 +11232,24 @@ "object-keys": "^1.0.12" } }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true + }, "detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -10027,7 +11283,19 @@ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, "requires": { - "esutils": "^2.0.2" + "esutils": "^2.0.2" + } + }, + "duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" } }, "eastasianwidth": { @@ -10036,6 +11304,12 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, "electron-to-chromium": { "version": "1.4.135", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.135.tgz", @@ -10060,6 +11334,21 @@ "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", "dev": true }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, "enhanced-resolve": { "version": "5.15.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", @@ -10142,6 +11431,12 @@ "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", "dev": true }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, "escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -10361,6 +11656,12 @@ "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", "dev": true }, + "fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true + }, "fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -10506,6 +11807,12 @@ } } }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -10634,6 +11941,20 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "gunzip-maybe": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz", + "integrity": "sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==", + "dev": true, + "requires": { + "browserify-zlib": "^0.1.4", + "is-deflate": "^1.0.0", + "is-gzip": "^1.0.0", + "peek-stream": "^1.1.0", + "pumpify": "^1.3.3", + "through2": "^2.0.3" + } + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -10682,6 +12003,56 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "http-assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz", + "integrity": "sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==", + "dev": true, + "requires": { + "deep-equal": "~1.0.1", + "http-errors": "~1.8.0" + }, + "dependencies": { + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true + }, + "http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true + } + } + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, "http-proxy-agent": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", @@ -10848,6 +12219,12 @@ "has-tostringtag": "^1.0.0" } }, + "is-deflate": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-deflate/-/is-deflate-1.0.0.tgz", + "integrity": "sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ==", + "dev": true + }, "is-docker": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", @@ -10890,6 +12267,12 @@ "is-extglob": "^2.1.1" } }, + "is-gzip": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-gzip/-/is-gzip-1.0.0.tgz", + "integrity": "sha512-rcfALRIb1YewtnksfRIHGcIY93QnK8BIQ/2c9yDYcG/Y6+vRoJuTWBmmSEbyLLYtXm7q35pHOHbZFQBaLrhlWQ==", + "dev": true + }, "is-inside-container": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", @@ -11695,6 +13078,15 @@ "setimmediate": "^1.0.5" } }, + "keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "dev": true, + "requires": { + "tsscmp": "1.0.6" + } + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -11707,6 +13099,160 @@ "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", "dev": true }, + "koa": { + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.15.3.tgz", + "integrity": "sha512-j/8tY9j5t+GVMLeioLaxweJiKUayFhlGqNTzf2ZGwL0ZCQijd2RLHK0SLW5Tsko8YyyqCZC2cojIb0/s62qTAg==", + "dev": true, + "requires": { + "accepts": "^1.3.5", + "cache-content-type": "^1.0.0", + "content-disposition": "~0.5.2", + "content-type": "^1.0.4", + "cookies": "~0.9.0", + "debug": "^4.3.2", + "delegates": "^1.0.0", + "depd": "^2.0.0", + "destroy": "^1.0.4", + "encodeurl": "^1.0.2", + "escape-html": "^1.0.3", + "fresh": "~0.5.2", + "http-assert": "^1.3.0", + "http-errors": "^1.6.3", + "is-generator-function": "^1.0.7", + "koa-compose": "^4.1.0", + "koa-convert": "^2.0.0", + "on-finished": "^2.3.0", + "only": "~0.0.2", + "parseurl": "^1.3.2", + "statuses": "^1.5.0", + "type-is": "^1.6.16", + "vary": "^1.1.2" + }, + "dependencies": { + "http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "dependencies": { + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true + } + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true + } + } + }, + "koa-compose": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", + "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==", + "dev": true + }, + "koa-convert": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-2.0.0.tgz", + "integrity": "sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==", + "dev": true, + "requires": { + "co": "^4.6.0", + "koa-compose": "^4.1.0" + } + }, + "koa-morgan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/koa-morgan/-/koa-morgan-1.0.1.tgz", + "integrity": "sha512-JOUdCNlc21G50afBXfErUrr1RKymbgzlrO5KURY+wmDG1Uvd2jmxUJcHgylb/mYXy2SjiNZyYim/ptUBGsIi3A==", + "dev": true, + "requires": { + "morgan": "^1.6.1" + } + }, + "koa-mount": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/koa-mount/-/koa-mount-4.0.0.tgz", + "integrity": "sha512-rm71jaA/P+6HeCpoRhmCv8KVBIi0tfGuO/dMKicbQnQW/YJntJ6MnnspkodoA4QstMVEZArsCphmd0bJEtoMjQ==", + "dev": true, + "requires": { + "debug": "^4.0.1", + "koa-compose": "^4.1.0" + } + }, + "koa-send": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/koa-send/-/koa-send-5.0.1.tgz", + "integrity": "sha512-tmcyQ/wXXuxpDxyNXv5yNNkdAMdFRqwtegBXUaowiQzUKqJehttS0x2j0eOZDQAyloAth5w6wwBImnFzkUz3pQ==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "http-errors": "^1.7.3", + "resolve-path": "^1.4.0" + }, + "dependencies": { + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true + }, + "http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true + } + } + }, + "koa-static": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/koa-static/-/koa-static-5.0.0.tgz", + "integrity": "sha512-UqyYyH5YEXaJrf9S8E23GoJFQZXkBVJ9zYYMPGz919MSX1KuvAcycIuS0ci150HCoPf4XQVhQ84Qf8xRPWxFaQ==", + "dev": true, + "requires": { + "debug": "^3.1.0", + "koa-send": "^5.0.0" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, "leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -11825,6 +13371,12 @@ "tmpl": "1.0.5" } }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true + }, "merge-options": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz", @@ -11846,6 +13398,12 @@ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true + }, "micromatch": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", @@ -11886,6 +13444,12 @@ "brace-expansion": "^1.1.7" } }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true + }, "minipass": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.3.tgz", @@ -11956,6 +13520,45 @@ } } }, + "morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "dev": true, + "requires": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + } + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -11974,6 +13577,12 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true + }, "neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -12041,6 +13650,21 @@ "object-keys": "^1.1.1" } }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -12059,6 +13683,12 @@ "mimic-fn": "^2.1.0" } }, + "only": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", + "integrity": "sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==", + "dev": true + }, "open": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz", @@ -12136,6 +13766,12 @@ "lines-and-columns": "^1.1.6" } }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true + }, "path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -12184,12 +13820,29 @@ } } }, + "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 + }, "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true }, + "peek-stream": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/peek-stream/-/peek-stream-1.1.3.tgz", + "integrity": "sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "duplexify": "^3.5.0", + "through2": "^2.0.3" + } + }, "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -12256,6 +13909,22 @@ } } }, + "playwright": { + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz", + "integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==", + "dev": true, + "requires": { + "fsevents": "2.3.2", + "playwright-core": "1.44.1" + } + }, + "playwright-core": { + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz", + "integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==", + "dev": true + }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -12318,6 +13987,27 @@ "sisteransi": "^1.0.5" } }, + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, + "requires": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + } + }, "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -12336,6 +14026,12 @@ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, + "queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -12438,6 +14134,54 @@ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, + "resolve-path": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/resolve-path/-/resolve-path-1.4.0.tgz", + "integrity": "sha512-i1xevIst/Qa+nA9olDxLWnLk8YZbi8R/7JPbCMcgyWaFR6bKWaexgJgEB5oc2PKMjYdrHynyz0NY+if+H98t1w==", + "dev": true, + "requires": { + "http-errors": "~1.6.2", + "path-is-absolute": "1.0.1" + }, + "dependencies": { + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true + } + } + }, "resolve.exports": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", @@ -12560,6 +14304,12 @@ "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "dev": true }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, "shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", @@ -12664,6 +14414,29 @@ } } }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true + }, + "stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "dev": true + }, + "streamx": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.16.1.tgz", + "integrity": "sha512-m9QYj6WygWyWa3H1YY69amr4nVgy61xfjys7xO7kviL5rfIEc2naf+ewFiOA+aEJD7y0JO3h2GoiUv4TDwEGzQ==", + "dev": true, + "requires": { + "bare-events": "^2.2.0", + "fast-fifo": "^1.1.0", + "queue-tick": "^1.0.1" + } + }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -12800,6 +14573,41 @@ "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", "dev": true }, + "tar-fs": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", + "dev": true, + "requires": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0", + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "dependencies": { + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "requires": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, "terser": { "version": "5.19.3", "resolved": "https://registry.npmjs.org/terser/-/terser-5.19.3.tgz", @@ -12853,6 +14661,16 @@ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, "titleize": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", @@ -12880,6 +14698,12 @@ "is-number": "^7.0.0" } }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true + }, "tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -12935,6 +14759,12 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "dev": true }, + "tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "dev": true + }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -12956,6 +14786,16 @@ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, "typescript": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", @@ -13020,6 +14860,18 @@ "convert-source-map": "^1.6.0" } }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true + }, + "vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "dev": true + }, "walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -13202,6 +15054,12 @@ "signal-exit": "^3.0.7" } }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true + }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -13264,6 +15122,12 @@ "is-plain-obj": "^2.1.0" } }, + "ylru": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.4.0.tgz", + "integrity": "sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA==", + "dev": true + }, "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index cf3ade4..5e674c2 100644 --- a/package.json +++ b/package.json @@ -227,7 +227,8 @@ "test-server": "cd server && npm test", "test-compile": "tsc --project ./client --outDir client/out", "pretest:e2e": "npm run compile && npm run test-compile", - "test:e2e": "node ./client/out/e2e/runTests.js" + "test:e2e": "node ./client/out/e2e/runTests.js", + "test-browser": "vscode-test-web --extensionDevelopmentPath=. ./test-data" }, "devDependencies": { "@types/jest": "^29.5.8", @@ -236,7 +237,8 @@ "@types/webpack-env": "^1.18.1", "@typescript-eslint/eslint-plugin": "^6.5.0", "@typescript-eslint/parser": "^6.5.0", - "@vscode/test-electron": "^2.3.10", + "@vscode/test-electron": "^2.3.4", + "@vscode/test-web": "^0.0.54", "assert": "^2.0.0", "concurrently": "^8.2.1", "eslint": "^8.48.0", From 7b3146c9828b1b17dbd8413a728bd556087f4b2a Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sat, 25 May 2024 15:36:54 +0200 Subject: [PATCH 81/98] Set pre-launch task to watch in Webworker launcher --- .vscode/launch.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 3c804f4..848517e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -31,7 +31,11 @@ "sourceMaps": true, "preLaunchTask": { "type": "npm", - "script": "compile" + "script": "watch" + }, + "resolveSourceMapLocations": ["${workspaceFolder}/client/dist/**/*.js"], + "sourceMapPathOverrides": { + "webpack://?:*/*": "${workspaceFolder}/client/*" } }, { From 26dbbc183040bede6f01d98a776b1c883b6b5d24 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sat, 25 May 2024 15:54:42 +0200 Subject: [PATCH 82/98] Cleanup some console logs --- .../workflow-tests-language-service/src/schema/adapter.ts | 4 ++-- .../src/services/completion/helper.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/packages/workflow-tests-language-service/src/schema/adapter.ts b/server/packages/workflow-tests-language-service/src/schema/adapter.ts index d357efb..9a512c7 100644 --- a/server/packages/workflow-tests-language-service/src/schema/adapter.ts +++ b/server/packages/workflow-tests-language-service/src/schema/adapter.ts @@ -18,9 +18,9 @@ import { StringASTNode, } from "@gxwf/server-common/src/ast/types"; import { Diagnostic, DiagnosticSeverity, DocumentContext, Range } from "@gxwf/server-common/src/languageTypes"; +import { injectable } from "inversify"; import { URI } from "vscode-uri"; import { JSONSchema, JSONSchemaRef } from "./jsonSchema"; -import { injectable } from "inversify"; const YAML_SCHEMA_PREFIX = "yaml-schema: "; export const YAML_SOURCE = "YAML"; @@ -437,7 +437,7 @@ export function asSchema(schema?: JSONSchemaRef): JSONSchema | undefined { }; } if (schema.$ref) { - console.log(`DEF ${schema.$ref}`); + console.debug(`DEF ${schema.$ref}`); } return schema; } diff --git a/server/packages/workflow-tests-language-service/src/services/completion/helper.ts b/server/packages/workflow-tests-language-service/src/services/completion/helper.ts index 43031b8..75b55bd 100644 --- a/server/packages/workflow-tests-language-service/src/services/completion/helper.ts +++ b/server/packages/workflow-tests-language-service/src/services/completion/helper.ts @@ -515,7 +515,7 @@ ${this.indentation}${this.indentation}$0 result.items = this.mergeCompletionItems(result.items); - console.debug("COMPLETION RESULT:", result); + // console.debug("COMPLETION RESULT:", result); return result; } From b6293b355cec43073d5bdb21543b1cf02412269c Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sun, 26 May 2024 12:32:33 +0200 Subject: [PATCH 83/98] Tidy up some imports --- client/src/commands/cleanWorkflow.ts | 6 +---- client/src/languageTypes.ts | 21 ++++++++++++++++ client/src/providers/cleanWorkflowProvider.ts | 6 +---- client/src/requests/gxworkflows.ts | 12 ++++----- .../src/gxFormat2WorkflowDocument.ts | 7 +++--- .../src/nativeWorkflowDocument.ts | 9 ++++--- .../server-common/src/languageTypes.ts | 25 ++++++++++++++++++- .../src/models/workflowDocument.ts | 2 +- .../src/models/workflowTestsDocument.ts | 3 +-- .../src/providers/workflowDataProvider.ts | 7 ++++-- .../src/services/cleanWorkflow.ts | 8 +++--- .../tests/unit/completion.test.ts | 9 +++++-- 12 files changed, 81 insertions(+), 34 deletions(-) create mode 100644 client/src/languageTypes.ts diff --git a/client/src/commands/cleanWorkflow.ts b/client/src/commands/cleanWorkflow.ts index 8fd7b77..7ae6040 100644 --- a/client/src/commands/cleanWorkflow.ts +++ b/client/src/commands/cleanWorkflow.ts @@ -1,10 +1,6 @@ import { window } from "vscode"; import { CustomCommand, getCommandFullIdentifier } from "."; -import { - CleanWorkflowDocumentParams, - CleanWorkflowDocumentResult, - LSRequestIdentifiers, -} from "../../../shared/src/requestsDefinitions"; +import { CleanWorkflowDocumentParams, CleanWorkflowDocumentResult, LSRequestIdentifiers } from "../languageTypes"; /** * Command to 'clean' the selected workflow document. diff --git a/client/src/languageTypes.ts b/client/src/languageTypes.ts new file mode 100644 index 0000000..d50923f --- /dev/null +++ b/client/src/languageTypes.ts @@ -0,0 +1,21 @@ +import { + CleanWorkflowContentsParams, + CleanWorkflowContentsResult, + CleanWorkflowDocumentParams, + CleanWorkflowDocumentResult, + GetWorkflowInputsResult, + GetWorkflowOutputsResult, + LSRequestIdentifiers, + TargetWorkflowDocumentParams, +} from "../../shared/src/requestsDefinitions"; + +export { + CleanWorkflowContentsParams, + CleanWorkflowContentsResult, + CleanWorkflowDocumentParams, + CleanWorkflowDocumentResult, + GetWorkflowInputsResult, + GetWorkflowOutputsResult, + LSRequestIdentifiers, + TargetWorkflowDocumentParams, +}; diff --git a/client/src/providers/cleanWorkflowProvider.ts b/client/src/providers/cleanWorkflowProvider.ts index d2c25be..4efcae7 100644 --- a/client/src/providers/cleanWorkflowProvider.ts +++ b/client/src/providers/cleanWorkflowProvider.ts @@ -1,11 +1,7 @@ import { Uri, window, workspace } from "vscode"; import { BaseLanguageClient } from "vscode-languageclient"; -import { - CleanWorkflowContentsParams, - CleanWorkflowContentsResult, - LSRequestIdentifiers, -} from "../../../shared/src/requestsDefinitions"; import { getWorkspaceScheme, replaceUriScheme } from "../common/utils"; +import { CleanWorkflowContentsParams, CleanWorkflowContentsResult, LSRequestIdentifiers } from "../languageTypes"; import { GitProvider } from "./git"; /** diff --git a/client/src/requests/gxworkflows.ts b/client/src/requests/gxworkflows.ts index 161c127..baad2f4 100644 --- a/client/src/requests/gxworkflows.ts +++ b/client/src/requests/gxworkflows.ts @@ -1,16 +1,16 @@ import { ExtensionContext, Uri, workspace } from "vscode"; import { BaseLanguageClient } from "vscode-languageclient"; -import { - GetWorkflowInputsResult, - GetWorkflowOutputsResult, - LSRequestIdentifiers, - TargetWorkflowDocumentParams, -} from "../../../shared/src/requestsDefinitions"; import { getAssociatedWorkflowUriFromTestsUri, isNativeWorkflowDocument, isWorkflowTestsDocument, } from "../common/utils"; +import { + GetWorkflowInputsResult, + GetWorkflowOutputsResult, + LSRequestIdentifiers, + TargetWorkflowDocumentParams, +} from "../languageTypes"; export function setupRequests( context: ExtensionContext, diff --git a/server/gx-workflow-ls-format2/src/gxFormat2WorkflowDocument.ts b/server/gx-workflow-ls-format2/src/gxFormat2WorkflowDocument.ts index bf4d4c4..72168e5 100644 --- a/server/gx-workflow-ls-format2/src/gxFormat2WorkflowDocument.ts +++ b/server/gx-workflow-ls-format2/src/gxFormat2WorkflowDocument.ts @@ -1,11 +1,12 @@ import { PropertyASTNode } from "@gxwf/server-common/src/ast/types"; -import { TextDocument, WorkflowDocument } from "@gxwf/server-common/src/languageTypes"; -import { YAMLDocument } from "@gxwf/yaml-language-service/src"; import { GetWorkflowInputsResult, GetWorkflowOutputsResult, + TextDocument, WorkflowDataType, -} from "../../../shared/src/requestsDefinitions"; + WorkflowDocument, +} from "@gxwf/server-common/src/languageTypes"; +import { YAMLDocument } from "@gxwf/yaml-language-service/src"; /** * This class provides information about a gxformat2 workflow document structure. diff --git a/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts b/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts index 3c40b3b..4dd7cf9 100644 --- a/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts +++ b/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts @@ -1,12 +1,13 @@ import { ASTNode, ParsedDocument } from "@gxwf/server-common/src/ast/types"; -import { TextDocument, WorkflowDocument } from "@gxwf/server-common/src/languageTypes"; -import { isWorkflowInputType } from "@gxwf/server-common/src/utils"; -import { JSONDocument } from "vscode-json-languageservice"; import { GetWorkflowInputsResult, GetWorkflowOutputsResult, + TextDocument, WorkflowDataType, -} from "../../../shared/src/requestsDefinitions"; + WorkflowDocument, +} from "@gxwf/server-common/src/languageTypes"; +import { JSONDocument } from "vscode-json-languageservice"; +import { ToolState, isWorkflowInputType, type ParameterInputToolState } from "./utils"; /** * This class provides information about a Native workflow document structure. diff --git a/server/packages/server-common/src/languageTypes.ts b/server/packages/server-common/src/languageTypes.ts index fc316ce..fd3b3cd 100644 --- a/server/packages/server-common/src/languageTypes.ts +++ b/server/packages/server-common/src/languageTypes.ts @@ -50,13 +50,29 @@ import { HoverParams, } from "vscode-languageserver/browser"; import { URI } from "vscode-uri"; -import { GetWorkflowInputsResult, GetWorkflowOutputsResult } from "../../../../shared/src/requestsDefinitions"; +import { + CleanWorkflowContentsParams, + CleanWorkflowContentsResult, + CleanWorkflowDocumentParams, + CleanWorkflowDocumentResult, + GetWorkflowInputsResult, + GetWorkflowOutputsResult, + LSRequestIdentifiers, + TargetWorkflowDocumentParams, + WorkflowDataType, + WorkflowInput, + WorkflowOutput, +} from "../../../../shared/src/requestsDefinitions"; import { ASTNodeManager } from "./ast/nodeManager"; import { ConfigService } from "./configService"; import { WorkflowDocument } from "./models/workflowDocument"; import { WorkflowTestsDocument } from "./models/workflowTestsDocument"; export { + CleanWorkflowContentsParams, + CleanWorkflowContentsResult, + CleanWorkflowDocumentParams, + CleanWorkflowDocumentResult, CodeAction, CodeActionContext, CodeActionKind, @@ -81,9 +97,12 @@ export { DocumentUri, FoldingRange, FoldingRangeKind, + GetWorkflowInputsResult, + GetWorkflowOutputsResult, Hover, HoverParams, InsertTextFormat, + LSRequestIdentifiers, Location, MarkedString, MarkupContent, @@ -93,11 +112,15 @@ export { SelectionRange, SymbolInformation, SymbolKind, + TargetWorkflowDocumentParams, TextDocument, TextDocumentEdit, TextEdit, VersionedTextDocumentIdentifier, + WorkflowDataType, WorkflowDocument, + WorkflowInput, + WorkflowOutput, WorkflowTestsDocument, WorkspaceEdit, }; diff --git a/server/packages/server-common/src/models/workflowDocument.ts b/server/packages/server-common/src/models/workflowDocument.ts index 8c6fd1c..3060676 100644 --- a/server/packages/server-common/src/models/workflowDocument.ts +++ b/server/packages/server-common/src/models/workflowDocument.ts @@ -1,4 +1,4 @@ -import { GetWorkflowInputsResult, GetWorkflowOutputsResult } from "../../../../../shared/src/requestsDefinitions"; +import { GetWorkflowInputsResult, GetWorkflowOutputsResult } from "../languageTypes"; import { DocumentBase } from "./document"; /** diff --git a/server/packages/server-common/src/models/workflowTestsDocument.ts b/server/packages/server-common/src/models/workflowTestsDocument.ts index b8fa1f1..aad76c1 100644 --- a/server/packages/server-common/src/models/workflowTestsDocument.ts +++ b/server/packages/server-common/src/models/workflowTestsDocument.ts @@ -1,5 +1,4 @@ -import { WorkflowInput, WorkflowOutput } from "../../../../../shared/src/requestsDefinitions"; -import { WorkflowDataProvider } from "../languageTypes"; +import { WorkflowDataProvider, WorkflowInput, WorkflowOutput } from "../languageTypes"; import { DocumentBase } from "./document"; /** diff --git a/server/packages/server-common/src/providers/workflowDataProvider.ts b/server/packages/server-common/src/providers/workflowDataProvider.ts index 5a89fa9..0205b9a 100644 --- a/server/packages/server-common/src/providers/workflowDataProvider.ts +++ b/server/packages/server-common/src/providers/workflowDataProvider.ts @@ -1,12 +1,15 @@ import { inject, injectable } from "inversify"; import { Connection } from "vscode-languageserver"; import { + DocumentsCache, GetWorkflowInputsResult, GetWorkflowOutputsResult, LSRequestIdentifiers, + TYPES, TargetWorkflowDocumentParams, -} from "../../../../../shared/src/requestsDefinitions"; -import { DocumentsCache, TYPES, WorkflowDataProvider, WorkflowDocument } from "../languageTypes"; + WorkflowDataProvider, + WorkflowDocument, +} from "../languageTypes"; @injectable() export class WorkflowDataProviderImpl implements WorkflowDataProvider { diff --git a/server/packages/server-common/src/services/cleanWorkflow.ts b/server/packages/server-common/src/services/cleanWorkflow.ts index 49a6706..1f94cb8 100644 --- a/server/packages/server-common/src/services/cleanWorkflow.ts +++ b/server/packages/server-common/src/services/cleanWorkflow.ts @@ -1,15 +1,17 @@ import { ApplyWorkspaceEditParams, Range, TextDocumentEdit, TextEdit } from "vscode-languageserver"; import { TextDocument } from "vscode-languageserver-textdocument"; import { ServiceBase } from "."; + +import { ASTNode, PropertyASTNode } from "../ast/types"; import { CleanWorkflowContentsParams, CleanWorkflowContentsResult, CleanWorkflowDocumentParams, CleanWorkflowDocumentResult, + GalaxyWorkflowLanguageServer, LSRequestIdentifiers, -} from "../../../../../shared/src/requestsDefinitions"; -import { ASTNode, PropertyASTNode } from "../ast/types"; -import { GalaxyWorkflowLanguageServer, WorkflowDocument } from "../languageTypes"; + WorkflowDocument, +} from "../languageTypes"; /** * Service for handling workflow `cleaning` requests. diff --git a/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts b/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts index cdedcc4..bc4e544 100644 --- a/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts +++ b/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts @@ -1,8 +1,13 @@ import { container } from "@gxwf/server-common/src/inversify.config"; -import { CompletionItem, CompletionList, WorkflowDataProvider } from "@gxwf/server-common/src/languageTypes"; +import { + CompletionItem, + CompletionList, + WorkflowDataProvider, + WorkflowInput, + WorkflowOutput, +} from "@gxwf/server-common/src/languageTypes"; import { WorkflowTestsLanguageServiceContainerModule } from "@gxwf/workflow-tests-language-service/src/inversify.config"; import "reflect-metadata"; -import { WorkflowInput, WorkflowOutput } from "../../../../../shared/src/requestsDefinitions"; import { WorkflowTestsSchemaService } from "../../src/schema/service"; import { YAMLCompletionHelper } from "../../src/services/completion/helper"; import { TYPES } from "../../src/types"; From 5220456a0f41ce4ab16b563b2994ec702501a064 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sun, 26 May 2024 12:34:35 +0200 Subject: [PATCH 84/98] Include missing parameter_input in workflow inputs --- .../src/nativeWorkflowDocument.ts | 10 ++++++++-- server/gx-workflow-ls-native/src/utils.ts | 12 ++++++++++++ server/packages/server-common/src/utils.ts | 5 ----- 3 files changed, 20 insertions(+), 7 deletions(-) create mode 100644 server/gx-workflow-ls-native/src/utils.ts delete mode 100644 server/packages/server-common/src/utils.ts diff --git a/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts b/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts index 4dd7cf9..f42ce85 100644 --- a/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts +++ b/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts @@ -55,10 +55,14 @@ export class NativeWorkflowDocument extends WorkflowDocument { } } } + const toolStateNode = step.properties.find((property) => property.keyNode.value === "tool_state"); + const toolStateValue = JSON.parse( + toolStateNode?.valueNode?.value ? String(toolStateNode?.valueNode?.value) : "{}" + ); result.inputs.push({ name: String(labelValue), doc: String(annotationValue ?? ""), - type: this.getInputType(stepTypeValue), + type: this.getInputType(stepTypeValue, toolStateValue), }); } }); @@ -97,12 +101,14 @@ export class NativeWorkflowDocument extends WorkflowDocument { return result; } - private getInputType(typeName: string): WorkflowDataType { + private getInputType(typeName: string, toolStateValue: ToolState): WorkflowDataType { switch (typeName) { case "data_input": return "data"; case "data_collection_input": return "collection"; + case "parameter_input": + return (toolStateValue as ParameterInputToolState).parameter_type as WorkflowDataType; default: return "data"; } diff --git a/server/gx-workflow-ls-native/src/utils.ts b/server/gx-workflow-ls-native/src/utils.ts new file mode 100644 index 0000000..d27bfb6 --- /dev/null +++ b/server/gx-workflow-ls-native/src/utils.ts @@ -0,0 +1,12 @@ +import { WorkflowDataType } from "@gxwf/server-common/src/languageTypes"; + +export function isWorkflowInputType(input: string): input is WorkflowDataType { + return ["data_input", "data_collection_input", "parameter_input"].includes(input); +} + +export interface ParameterInputToolState { + parameter_type: string; + optional: boolean; +} + +export type ToolState = ParameterInputToolState | unknown; diff --git a/server/packages/server-common/src/utils.ts b/server/packages/server-common/src/utils.ts deleted file mode 100644 index 1cd11a6..0000000 --- a/server/packages/server-common/src/utils.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { WorkflowDataType } from "../../../../shared/src/requestsDefinitions"; - -export function isWorkflowInputType(input: string): input is WorkflowDataType { - return input === "data_input" || input === "data_collection_input"; -} From 6ea4a52973d339936732e4f2d73ebc86a12ea45c Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sun, 26 May 2024 12:44:14 +0200 Subject: [PATCH 85/98] Refactor completion helper To gather workflow inputs and outputs just once per request --- .../src/services/completion/helper.ts | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/server/packages/workflow-tests-language-service/src/services/completion/helper.ts b/server/packages/workflow-tests-language-service/src/services/completion/helper.ts index 75b55bd..ed3d51f 100644 --- a/server/packages/workflow-tests-language-service/src/services/completion/helper.ts +++ b/server/packages/workflow-tests-language-service/src/services/completion/helper.ts @@ -22,6 +22,8 @@ import { Position, Range, TextEdit, + WorkflowInput, + WorkflowOutput, WorkflowTestsDocument, } from "@gxwf/server-common/src/languageTypes"; import { YamlNode } from "@gxwf/yaml-language-service/src/parser/astTypes"; @@ -65,6 +67,8 @@ interface InsertText { export class YAMLCompletionHelper { private indentation: string = " "; private arrayPrefixIndentation: string = ""; + private workflowInputs: WorkflowInput[] = []; + private workflowOutputs: WorkflowOutput[] = []; constructor(protected schemaService: WorkflowTestsSchemaService) {} @@ -132,6 +136,11 @@ ${this.indentation}${this.indentation}$0 return Promise.resolve(result); } + // Gather all the workflow information needed to provide completions + const testDocument = documentContext as WorkflowTestsDocument; + this.workflowInputs = await testDocument.getWorkflowInputs(); + this.workflowOutputs = await testDocument.getWorkflowOutputs(); + let overwriteRange: Range | null = null; if (areOnlySpacesAfterPosition) { overwriteRange = Range.create(position, Position.create(position.line, lineContent.length)); @@ -978,12 +987,9 @@ ${this.indentation}${this.indentation}$0 return it.node.internalNode === originalNode && it.schema.properties; }); - // if the parent is the `job` key, then we need to add the `job` properties from the document context + // if the parent is the `job` key, then we need to add the workflow inputs if (nodeParent && isPair(nodeParent) && isScalar(nodeParent.key) && nodeParent.key.value === "job") { - const testDocument = documentContext as WorkflowTestsDocument; - const workflowInputs = await testDocument.getWorkflowInputs(); - // create a completion item for the inputs excluding the ones already defined - workflowInputs.forEach((input) => { + this.workflowInputs.forEach((input) => { collector.add({ kind: CompletionItemKind.Property, label: input.name, @@ -995,10 +1001,9 @@ ${this.indentation}${this.indentation}$0 return; } + // if the parent is the `outputs` key, then we need to add the workflow outputs if (nodeParent && isPair(nodeParent) && isScalar(nodeParent.key) && nodeParent.key.value === "outputs") { - const testDocument = documentContext as WorkflowTestsDocument; - const workflowOutputs = await testDocument.getWorkflowOutputs(); - workflowOutputs.forEach((output) => { + this.workflowOutputs.forEach((output) => { collector.add({ kind: CompletionItemKind.Property, label: output.name, @@ -1011,10 +1016,8 @@ ${this.indentation}${this.indentation}$0 //If the parent is a workflow input, then we need to add the properties from the document context if (nodeParent && isPair(nodeParent) && isScalar(nodeParent.key)) { - const testDocument = documentContext as WorkflowTestsDocument; - const workflowInputs = await testDocument.getWorkflowInputs(); const nodeParentKey = nodeParent.key.value; - const matchingWorkflowInput = workflowInputs.find((input) => input.name === nodeParentKey); + const matchingWorkflowInput = this.workflowInputs.find((input) => input.name === nodeParentKey); if (matchingWorkflowInput) { const type = matchingWorkflowInput.type; const DATA_INPUT_TYPE_OPTIONS = ["PathFile", "LocationFile", "CompositeDataFile"]; @@ -1235,6 +1238,7 @@ ${this.indentation}${this.indentation}$0 if (node && (parentKey !== null || isSeq(node))) { const separatorAfter = ""; const didCallFromAutoComplete = true; + //TODO: check if parentKey is as input or output const matchingSchemas = this.schemaService.getMatchingSchemas(documentContext, offset, didCallFromAutoComplete); for (const s of matchingSchemas) { const internalNode = s.node.internalNode as HasRange; From 89d84b08fefd1354ddda9931c2df806c8866ec2f Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sun, 26 May 2024 13:51:35 +0200 Subject: [PATCH 86/98] Add completion for input values --- .../src/services/completion/helper.ts | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/server/packages/workflow-tests-language-service/src/services/completion/helper.ts b/server/packages/workflow-tests-language-service/src/services/completion/helper.ts index ed3d51f..a6aae91 100644 --- a/server/packages/workflow-tests-language-service/src/services/completion/helper.ts +++ b/server/packages/workflow-tests-language-service/src/services/completion/helper.ts @@ -1238,7 +1238,30 @@ ${this.indentation}${this.indentation}$0 if (node && (parentKey !== null || isSeq(node))) { const separatorAfter = ""; const didCallFromAutoComplete = true; - //TODO: check if parentKey is as input or output + // Check if the parent is a workflow input + const matchingInput = this.workflowInputs.find((input) => input.name === parentKey); + if (matchingInput) { + const type = matchingInput.type; + let typeSchema: JSONSchema = { type: "string" }; + switch (type) { + case "boolean": + this.addBooleanValueCompletion(true, separatorAfter, collector); + this.addBooleanValueCompletion(false, separatorAfter, collector); + return; + case "null": + this.addNullValueCompletion(separatorAfter, collector); + return; + case "double": + case "float": + case "long": + case "int": + case "integer": + typeSchema = { type: "number" }; + break; + } + this.addSchemaValueCompletions(typeSchema, separatorAfter, collector, types, "value"); + return; + } const matchingSchemas = this.schemaService.getMatchingSchemas(documentContext, offset, didCallFromAutoComplete); for (const s of matchingSchemas) { const internalNode = s.node.internalNode as HasRange; From f217074f758406da9d8cb9d882ce80c38e55ce8d Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sun, 26 May 2024 13:52:20 +0200 Subject: [PATCH 87/98] Refactor getWorkflowInputs test for .ga To check also for the contents of the inputs --- .../tests/unit/nativeWorkflowDocument.test.ts | 63 ++++++++++--------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/server/gx-workflow-ls-native/tests/unit/nativeWorkflowDocument.test.ts b/server/gx-workflow-ls-native/tests/unit/nativeWorkflowDocument.test.ts index bed7057..37ca7a7 100644 --- a/server/gx-workflow-ls-native/tests/unit/nativeWorkflowDocument.test.ts +++ b/server/gx-workflow-ls-native/tests/unit/nativeWorkflowDocument.test.ts @@ -1,3 +1,4 @@ +import { WorkflowInput } from "@gxwf/server-common/src/languageTypes"; import { createNativeWorkflowDocument } from "../testHelpers"; import { TestWorkflowProvider } from "../testWorkflowProvider"; @@ -16,35 +17,41 @@ describe("NativeWorkflowDocument", () => { }); describe("getWorkflowInputs", () => { - it.each([ - ["", 0], - [TestWorkflowProvider.workflows.validation.withoutSteps, 0], - [TestWorkflowProvider.workflows.validation.withOneStep, 1], - [TestWorkflowProvider.workflows.validation.withThreeSteps, 2], - [TestWorkflowProvider.workflows.validation.withWorkflowOutputLabels, 1], - [TestWorkflowProvider.workflows.validation.withoutWorkflowOutputLabels, 1], - ])("returns the expected number of inputs", (wf_content: string, expectedNumInputs: number) => { - const document = createNativeWorkflowDocument(wf_content); + it.each<[string, WorkflowInput[]]>([ + ["", []], + [TestWorkflowProvider.workflows.validation.withoutSteps, []], + [ + TestWorkflowProvider.workflows.validation.withOneStep, + [{ doc: "Step description", name: "Test Step", type: "data" }], + ], + [ + TestWorkflowProvider.workflows.validation.withThreeSteps, + [ + { + name: "WorkflowInput1", + doc: "input1 description", + type: "data", + }, + { + name: "WorkflowInput2", + doc: "", + type: "data", + }, + ], + ], + [ + TestWorkflowProvider.workflows.validation.withWorkflowOutputLabels, + [{ doc: "Step description", name: "Test Step", type: "data" }], + ], + [ + TestWorkflowProvider.workflows.validation.withoutWorkflowOutputLabels, + [{ doc: "Step description", name: "Test Step", type: "data" }], + ], + ])("returns the expected inputs", (wfContent: string, expectedInputs: WorkflowInput[]) => { + const document = createNativeWorkflowDocument(wfContent); const result = document.getWorkflowInputs(); - expect(result.inputs).toHaveLength(expectedNumInputs); - }); - - it("should return the expected information of the inputs", () => { - const document = createNativeWorkflowDocument(TestWorkflowProvider.workflows.validation.withThreeSteps); - const result = document.getWorkflowInputs(); - expect(result.inputs).toHaveLength(2); - expect(result.inputs).toEqual([ - { - name: "WorkflowInput1", - doc: "input1 description", - type: "data", - }, - { - name: "WorkflowInput2", - doc: "", - type: "data", - }, - ]); + expect(result.inputs).toHaveLength(expectedInputs.length); + expect(result.inputs).toEqual(expectedInputs); }); }); From a8251cdc1b6acdba87ca3aec2c3f03df5845f4c0 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sun, 26 May 2024 14:21:56 +0200 Subject: [PATCH 88/98] Fix input name retrieval for .ga --- server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts b/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts index f42ce85..287b059 100644 --- a/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts +++ b/server/gx-workflow-ls-native/src/nativeWorkflowDocument.ts @@ -55,6 +55,10 @@ export class NativeWorkflowDocument extends WorkflowDocument { } } } + if (!labelValue) { + const nameNode = step.properties.find((property) => property.keyNode.value === "name"); + labelValue = nameNode?.valueNode?.value; + } const toolStateNode = step.properties.find((property) => property.keyNode.value === "tool_state"); const toolStateValue = JSON.parse( toolStateNode?.valueNode?.value ? String(toolStateNode?.valueNode?.value) : "{}" From e8c52f9de99242e4cc5979ab34cb7aba76db159a Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sun, 26 May 2024 14:35:29 +0200 Subject: [PATCH 89/98] Add `color` as possible input type This seems to be valid when creating a wf parameter in the UI but the gxformat2 schema doesn't seem to support it. --- shared/src/requestsDefinitions.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/shared/src/requestsDefinitions.ts b/shared/src/requestsDefinitions.ts index 39c2754..41ff0be 100644 --- a/shared/src/requestsDefinitions.ts +++ b/shared/src/requestsDefinitions.ts @@ -29,6 +29,7 @@ export interface TargetWorkflowDocumentParams { } export type WorkflowDataType = + | "color" //TODO: this type seems to be missing in format2 schema | "null" | "boolean" | "int" From 5ded943a269faa8419d4aa46afd146183971f9b2 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sun, 26 May 2024 14:36:37 +0200 Subject: [PATCH 90/98] Increase test coverage for getWorkflowInputs in .ga workflows --- .../tests/testWorkflowProvider.ts | 3 + .../tests/unit/nativeWorkflowDocument.test.ts | 12 ++ test-data/json/validation/test_wf_05.ga | 201 ++++++++++++++++++ 3 files changed, 216 insertions(+) create mode 100644 test-data/json/validation/test_wf_05.ga diff --git a/server/gx-workflow-ls-native/tests/testWorkflowProvider.ts b/server/gx-workflow-ls-native/tests/testWorkflowProvider.ts index 8486b42..c480994 100644 --- a/server/gx-workflow-ls-native/tests/testWorkflowProvider.ts +++ b/server/gx-workflow-ls-native/tests/testWorkflowProvider.ts @@ -16,6 +16,8 @@ interface TestJsonWorkflows { withoutWorkflowOutputLabels: string; /** Workflow with 1 step. The step has 2 workflow_outputs with labels. */ withWorkflowOutputLabels: string; + /** Workflow with 6 steps. All steps are inputs with different types. */ + withOnlyInputs: string; }; } @@ -33,6 +35,7 @@ export class TestWorkflowProvider { path.join(TEST_DATA_PATH, "json", "validation", "test_wf_04.ga"), "utf-8" ), + withOnlyInputs: fs.readFileSync(path.join(TEST_DATA_PATH, "json", "validation", "test_wf_05.ga"), "utf-8"), }, }; diff --git a/server/gx-workflow-ls-native/tests/unit/nativeWorkflowDocument.test.ts b/server/gx-workflow-ls-native/tests/unit/nativeWorkflowDocument.test.ts index 37ca7a7..96c30bf 100644 --- a/server/gx-workflow-ls-native/tests/unit/nativeWorkflowDocument.test.ts +++ b/server/gx-workflow-ls-native/tests/unit/nativeWorkflowDocument.test.ts @@ -47,6 +47,18 @@ describe("NativeWorkflowDocument", () => { TestWorkflowProvider.workflows.validation.withoutWorkflowOutputLabels, [{ doc: "Step description", name: "Test Step", type: "data" }], ], + [ + TestWorkflowProvider.workflows.validation.withOnlyInputs, + [ + { doc: "", name: "Dataset Input", type: "data" }, + { doc: "", name: "Collection Input", type: "collection" }, + { doc: "", name: "Text Param", type: "text" }, + { doc: "", name: "Integer Param", type: "integer" }, + { doc: "", name: "Float Param", type: "float" }, + { doc: "", name: "Boolean Param", type: "boolean" }, + { doc: "", name: "Color Param", type: "color" }, + ], + ], ])("returns the expected inputs", (wfContent: string, expectedInputs: WorkflowInput[]) => { const document = createNativeWorkflowDocument(wfContent); const result = document.getWorkflowInputs(); diff --git a/test-data/json/validation/test_wf_05.ga b/test-data/json/validation/test_wf_05.ga new file mode 100644 index 0000000..3bf04dd --- /dev/null +++ b/test-data/json/validation/test_wf_05.ga @@ -0,0 +1,201 @@ +{ + "a_galaxy_workflow": "true", + "annotation": "", + "comments": [], + "format-version": "0.1", + "name": "Workflow with inputs only", + "steps": { + "0": { + "annotation": "", + "content_id": null, + "errors": null, + "id": 0, + "input_connections": {}, + "inputs": [ + { + "description": "", + "name": "Dataset Input" + } + ], + "label": "Dataset Input", + "name": "Input dataset", + "outputs": [], + "position": { + "left": 21, + "top": 0 + }, + "tool_id": null, + "tool_state": "{\"optional\": false, \"tag\": null}", + "tool_version": null, + "type": "data_input", + "uuid": "f38a0899-4d6a-42ef-a25e-fb860d624230", + "when": null, + "workflow_outputs": [] + }, + "1": { + "annotation": "", + "content_id": null, + "errors": null, + "id": 1, + "input_connections": {}, + "inputs": [ + { + "description": "", + "name": "Collection Input" + } + ], + "label": "Collection Input", + "name": "Input dataset collection", + "outputs": [], + "position": { + "left": 20, + "top": 123 + }, + "tool_id": null, + "tool_state": "{\"optional\": false, \"tag\": null, \"collection_type\": \"list\"}", + "tool_version": null, + "type": "data_collection_input", + "uuid": "ee32048f-8e39-49cd-a2ff-cd24582a6bac", + "when": null, + "workflow_outputs": [] + }, + "2": { + "annotation": "", + "content_id": null, + "errors": null, + "id": 2, + "input_connections": {}, + "inputs": [ + { + "description": "", + "name": "Text Param" + } + ], + "label": "Text Param", + "name": "Input parameter", + "outputs": [], + "position": { + "left": 20, + "top": 260 + }, + "tool_id": null, + "tool_state": "{\"parameter_type\": \"text\", \"optional\": false}", + "tool_version": null, + "type": "parameter_input", + "uuid": "f15e91fb-b5f1-4a3d-910a-436e617afc53", + "when": null, + "workflow_outputs": [] + }, + "3": { + "annotation": "", + "content_id": null, + "errors": null, + "id": 3, + "input_connections": {}, + "inputs": [ + { + "description": "", + "name": "Integer Param" + } + ], + "label": "Integer Param", + "name": "Input parameter", + "outputs": [], + "position": { + "left": 24, + "top": 374 + }, + "tool_id": null, + "tool_state": "{\"parameter_type\": \"integer\", \"optional\": false}", + "tool_version": null, + "type": "parameter_input", + "uuid": "847d6938-c7aa-41fa-a139-611f4aaa900d", + "when": null, + "workflow_outputs": [] + }, + "4": { + "annotation": "", + "content_id": null, + "errors": null, + "id": 4, + "input_connections": {}, + "inputs": [ + { + "description": "", + "name": "Float Param" + } + ], + "label": "Float Param", + "name": "Input parameter", + "outputs": [], + "position": { + "left": 22, + "top": 489 + }, + "tool_id": null, + "tool_state": "{\"parameter_type\": \"float\", \"optional\": false}", + "tool_version": null, + "type": "parameter_input", + "uuid": "dd9fdf0e-76af-4de8-a1d8-e6a2f6265013", + "when": null, + "workflow_outputs": [] + }, + "5": { + "annotation": "", + "content_id": null, + "errors": null, + "id": 5, + "input_connections": {}, + "inputs": [ + { + "description": "", + "name": "Boolean Param" + } + ], + "label": "Boolean Param", + "name": "Input parameter", + "outputs": [], + "position": { + "left": 12, + "top": 596 + }, + "tool_id": null, + "tool_state": "{\"parameter_type\": \"boolean\", \"optional\": false}", + "tool_version": null, + "type": "parameter_input", + "uuid": "e600a8b4-3255-4f22-9452-71bde2b2a687", + "when": null, + "workflow_outputs": [] + }, + "6": { + "annotation": "", + "content_id": null, + "errors": null, + "id": 6, + "input_connections": {}, + "inputs": [ + { + "description": "", + "name": "Color Param" + } + ], + "label": "Color Param", + "name": "Input parameter", + "outputs": [], + "position": { + "left": 0, + "top": 712 + }, + "tool_id": null, + "tool_state": "{\"parameter_type\": \"color\", \"optional\": false}", + "tool_version": null, + "type": "parameter_input", + "uuid": "28d79f2e-a8d0-49e1-aab3-a032b2d3d327", + "when": null, + "workflow_outputs": [] + } + }, + "tags": [], + "uuid": "a1d51848-2a7b-4ec1-b1a7-624e25d9c47a", + "version": 1 +} \ No newline at end of file From f902f182ea4e755b9d083f26769a2cb08cae98f1 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Tue, 28 May 2024 23:56:22 +0200 Subject: [PATCH 91/98] Fix typescript config for shared module --- server/packages/server-common/tsconfig.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/packages/server-common/tsconfig.json b/server/packages/server-common/tsconfig.json index 1a5daca..019c99f 100644 --- a/server/packages/server-common/tsconfig.json +++ b/server/packages/server-common/tsconfig.json @@ -14,8 +14,10 @@ "composite": true, "declaration": true, "baseUrl": ".", + "rootDir": "../../../", "paths": { "@schemas/*": ["../../../workflow-languages/schemas/*"] } - } + }, + "include": ["src/**/*", "../../../shared/**/*", "../../../workflow-languages/**/*"] } From 6789e3fefc84df6072735c19369135199cf90b27 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Wed, 29 May 2024 17:06:26 +0200 Subject: [PATCH 92/98] Add basic validation for inputs/outputs in test document --- .../src/services/validation.ts | 68 +++++++++++++++++-- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/server/packages/workflow-tests-language-service/src/services/validation.ts b/server/packages/workflow-tests-language-service/src/services/validation.ts index a839127..5e24874 100644 --- a/server/packages/workflow-tests-language-service/src/services/validation.ts +++ b/server/packages/workflow-tests-language-service/src/services/validation.ts @@ -1,9 +1,15 @@ -import { Diagnostic, DiagnosticSeverity, DocumentContext, Range } from "@gxwf/server-common/src/languageTypes"; +import { + Diagnostic, + DiagnosticSeverity, + DocumentContext, + Range, + WorkflowTestsDocument, +} from "@gxwf/server-common/src/languageTypes"; import { inject, injectable } from "inversify"; -import { WorkflowTestsSchemaProvider } from "../schema/provider"; -import { TYPES } from "../types"; import { ResolvedSchema } from "../schema/jsonSchema"; +import { WorkflowTestsSchemaProvider } from "../schema/provider"; import { WorkflowTestsSchemaService } from "../schema/service"; +import { TYPES } from "../types"; export interface WorkflowTestsValidationService { doValidation(documentContext: DocumentContext): Promise; @@ -66,6 +72,60 @@ export class WorkflowTestsValidationServiceImpl implements WorkflowTestsValidati }; const schema = this.schemaProvider.getResolvedSchema(); - return getDiagnostics(schema); + const schemaValidation = getDiagnostics(schema); + const semanticValidation = await this.doSemanticValidation(documentContext); + return schemaValidation.concat(semanticValidation); + } + + async doSemanticValidation(documentContext: DocumentContext): Promise { + const testDocument = documentContext as WorkflowTestsDocument; + const inputDiagnostics = await this.validateWorkflowInputs(testDocument); + const outputDiagnostics = await this.validateWorkflowOutputs(testDocument); + return inputDiagnostics.concat(outputDiagnostics); + } + + private async validateWorkflowInputs(testDocument: WorkflowTestsDocument): Promise { + const diagnostics: Diagnostic[] = []; + const workflowInputs = await testDocument.getWorkflowInputs(); + const documentInputNodes = testDocument.nodeManager.getAllPropertyNodesByName("job")[0]?.valueNode?.children ?? []; + documentInputNodes.forEach((inputNode) => { + if (inputNode.type !== "property") { + return; + } + const inputName = inputNode.keyNode.value as string; + const input = workflowInputs.find((i) => i.name === inputName); + if (!input) { + const range = Range.create( + testDocument.textDocument.positionAt(inputNode.offset), + testDocument.textDocument.positionAt(inputNode.offset + inputNode.length) + ); + const message = `Input "${inputName}" is not defined in the associated workflow.`; + diagnostics.push(Diagnostic.create(range, message, DiagnosticSeverity.Error)); + } + }); + return diagnostics; + } + + private async validateWorkflowOutputs(testDocument: WorkflowTestsDocument): Promise { + const diagnostics: Diagnostic[] = []; + const workflowOutputs = await testDocument.getWorkflowOutputs(); + const documentOutputNodes = + testDocument.nodeManager.getAllPropertyNodesByName("outputs")[0]?.valueNode?.children ?? []; + documentOutputNodes.forEach((outputNode) => { + if (outputNode.type !== "property") { + return; + } + const outputName = outputNode.keyNode.value as string; + const output = workflowOutputs.find((o) => o.name === outputName); + if (!output) { + const range = Range.create( + testDocument.textDocument.positionAt(outputNode.offset), + testDocument.textDocument.positionAt(outputNode.offset + outputNode.length) + ); + const message = `Output "${outputName}" is not defined in the associated workflow.`; + diagnostics.push(Diagnostic.create(range, message, DiagnosticSeverity.Error)); + } + }); + return diagnostics; } } From f181f5278b558e37453d4ad26c3f62af9e62c1c9 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Wed, 29 May 2024 17:07:20 +0200 Subject: [PATCH 93/98] Add tests for test document validation --- .../tests/unit/validation.test.ts | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 server/packages/workflow-tests-language-service/tests/unit/validation.test.ts diff --git a/server/packages/workflow-tests-language-service/tests/unit/validation.test.ts b/server/packages/workflow-tests-language-service/tests/unit/validation.test.ts new file mode 100644 index 0000000..e90ec22 --- /dev/null +++ b/server/packages/workflow-tests-language-service/tests/unit/validation.test.ts @@ -0,0 +1,136 @@ +import { container } from "@gxwf/server-common/src/inversify.config"; +import { + Diagnostic, + DiagnosticSeverity, + WorkflowDataProvider, + WorkflowInput, + WorkflowOutput, +} from "@gxwf/server-common/src/languageTypes"; +import { WorkflowTestsLanguageServiceContainerModule } from "@gxwf/workflow-tests-language-service/src/inversify.config"; +import { WorkflowTestsValidationService } from "@gxwf/workflow-tests-language-service/src/services/validation"; +import "reflect-metadata"; +import { TYPES } from "../../src/types"; +import { createGxWorkflowTestsDocument } from "../testHelpers"; + +describe("Workflow Tests Validation Service", () => { + let service: WorkflowTestsValidationService; + beforeAll(() => { + container.load(WorkflowTestsLanguageServiceContainerModule); + service = container.get(TYPES.WorkflowTestsValidationService); + }); + + async function validate(contents: string, workflowDataProvider?: WorkflowDataProvider): Promise { + const documentContext = createGxWorkflowTestsDocument(contents, workflowDataProvider); + return await service.doValidation(documentContext); + } + + it("should warn about missing job and outputs properties", async () => { + const testDocumentContents = ` +- doc: The docs + `; + + const diagnostics = await validate(testDocumentContents); + + expect(diagnostics.length).toBe(2); + expect(diagnostics[0].message).toBe('Missing property "job".'); + expect(diagnostics[0].severity).toBe(DiagnosticSeverity.Warning); + expect(diagnostics[1].message).toBe('Missing property "outputs".'); + expect(diagnostics[1].severity).toBe(DiagnosticSeverity.Warning); + }); + + describe("Workflow Inputs/Outputs Validation", () => { + let workflowDataProviderMock: WorkflowDataProvider; + const FAKE_DATASET_INPUT: WorkflowInput = { + name: "My fake dataset", + doc: "This is a simple dataset", + type: "data", + }; + const FAKE_DATASET_INPUT_COLON: WorkflowInput = { + name: "Input dataset: fake", + doc: "This is a simple dataset with a colon in the name", + type: "File", + }; + const FAKE_DATASET_COLLECTION_INPUT: WorkflowInput = { + name: "My fake collection", + doc: "This is a collection", + type: "collection", + }; + const FAKE_WORKFLOW_INPUTS: WorkflowInput[] = [ + FAKE_DATASET_INPUT, + FAKE_DATASET_COLLECTION_INPUT, + FAKE_DATASET_INPUT_COLON, + ]; + const FAKE_WORKFLOW_OUTPUTS: WorkflowOutput[] = [ + { + name: "My output", + uuid: "1234-5678-91011-1213", + }, + { + name: "My second output", + uuid: "1234-5678-91011-1214", + }, + { + name: "My third output: with colon", + uuid: "1234-5678-91011-1215", + }, + ]; + + beforeAll(() => { + workflowDataProviderMock = { + async getWorkflowInputs(_workflowDocumentUri: string) { + return { + inputs: FAKE_WORKFLOW_INPUTS, + }; + }, + async getWorkflowOutputs(_workflowDocumentUri: string) { + return { + outputs: FAKE_WORKFLOW_OUTPUTS, + }; + }, + }; + }); + + it("should pass validation when the inputs and outputs are defined in the workflow", async () => { + const testDocumentContents = ` +- doc: The docs + job: + My fake dataset: data/input.txt + outputs: + My output: out/output.txt`; + + const diagnostics = await validate(testDocumentContents, workflowDataProviderMock); + + expect(diagnostics).not.toBeNull(); + }); + + it("should error when an input is not defined in the workflow", async () => { + const testDocumentContents = ` +- doc: The docs + job: + Missing input: data/input.txt + outputs: + My output: out/output.txt`; + + const diagnostics = await validate(testDocumentContents, workflowDataProviderMock); + + expect(diagnostics.length).toBe(1); + expect(diagnostics[0].message).toBe('Input "Missing input" is not defined in the associated workflow.'); + expect(diagnostics[0].severity).toBe(DiagnosticSeverity.Error); + }); + + it("should error when an output is not defined in the workflow", async () => { + const testDocumentContents = ` +- doc: The docs + job: + My fake dataset: data/input.txt + outputs: + Missing output: out/output.txt`; + + const diagnostics = await validate(testDocumentContents, workflowDataProviderMock); + + expect(diagnostics.length).toBe(1); + expect(diagnostics[0].message).toBe('Output "Missing output" is not defined in the associated workflow.'); + expect(diagnostics[0].severity).toBe(DiagnosticSeverity.Error); + }); + }); +}); From 8a7a63bbd9c9686547a8bb3583106123c4decfde Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Wed, 29 May 2024 17:20:07 +0200 Subject: [PATCH 94/98] Cleanup comments --- .../src/services/completion/helper.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/packages/workflow-tests-language-service/src/services/completion/helper.ts b/server/packages/workflow-tests-language-service/src/services/completion/helper.ts index a6aae91..c0ec3b6 100644 --- a/server/packages/workflow-tests-language-service/src/services/completion/helper.ts +++ b/server/packages/workflow-tests-language-service/src/services/completion/helper.ts @@ -1048,8 +1048,6 @@ ${this.indentation}${this.indentation}$0 } break; case "collection": - // The valid schema is "Collection" - // TODO add class: Collection matchingSchemas = matchingSchemas.filter((schema) => schema.schema.title === "Collection"); break; } From b65a4c9c8873d7befe2b37a8864ccacafc64d088 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Thu, 30 May 2024 18:41:03 +0200 Subject: [PATCH 95/98] Move parseTemplate function to common testHelpers module --- .../server-common/tests/testHelpers.ts | 28 ++++++++++++++++++ .../tests/unit/completion.test.ts | 29 +++---------------- 2 files changed, 32 insertions(+), 25 deletions(-) diff --git a/server/packages/server-common/tests/testHelpers.ts b/server/packages/server-common/tests/testHelpers.ts index 0e56c61..c1e74fe 100644 --- a/server/packages/server-common/tests/testHelpers.ts +++ b/server/packages/server-common/tests/testHelpers.ts @@ -4,3 +4,31 @@ export function expectPropertyNodeToHaveKey(propertyNode: ASTNode | null, expect expect(propertyNode?.type).toBe("property"); expect((propertyNode as PropertyASTNode).keyNode.value).toBe(expectedPropertyKey); } + +/** + * Simulates the position of the cursor in the contents of a text document. + * @param template Represents the contents of a text document with a single character to be replaced. + * @param char Defaults to "$". The character to be replaced in the template. Its position will be used to simulate the position of the cursor. + * @returns The contents of the template string with the character removed and the position of the character. + */ +export function parseTemplate( + template: string, + char?: string +): { contents: string; position: { line: number; character: number } } { + if (!char) { + char = "$"; + } + let position = { line: 0, character: 0 }; + const contents = template.replace(char, ""); + + const lines = template.split("\n"); + for (let i = 0; i < lines.length; i++) { + const character = lines[i].indexOf(char); + if (character !== -1) { + position = { line: i, character }; + return { contents, position }; + } + } + + return { contents, position }; +} diff --git a/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts b/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts index bc4e544..c4f1753 100644 --- a/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts +++ b/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts @@ -6,11 +6,12 @@ import { WorkflowInput, WorkflowOutput, } from "@gxwf/server-common/src/languageTypes"; +import { parseTemplate } from "@gxwf/server-common/tests/testHelpers"; import { WorkflowTestsLanguageServiceContainerModule } from "@gxwf/workflow-tests-language-service/src/inversify.config"; +import { WorkflowTestsSchemaService } from "@gxwf/workflow-tests-language-service/src/schema/service"; +import { YAMLCompletionHelper } from "@gxwf/workflow-tests-language-service/src/services/completion/helper"; +import { TYPES } from "@gxwf/workflow-tests-language-service/src/types"; import "reflect-metadata"; -import { WorkflowTestsSchemaService } from "../../src/schema/service"; -import { YAMLCompletionHelper } from "../../src/services/completion/helper"; -import { TYPES } from "../../src/types"; import { createGxWorkflowTestsDocument } from "../testHelpers"; describe("Workflow Tests Completion Service", () => { @@ -264,28 +265,6 @@ describe("Workflow Tests Completion Service", () => { }); }); -function parseTemplate( - template: string, - char?: string -): { contents: string; position: { line: number; character: number } } { - if (!char) { - char = "$"; - } - let position = { line: 0, character: 0 }; - const contents = template.replace(char, ""); - - const lines = template.split("\n"); - for (let i = 0; i < lines.length; i++) { - const character = lines[i].indexOf(char); - if (character !== -1) { - position = { line: i, character }; - return { contents, position }; - } - } - - return { contents, position }; -} - function expectCompletionItemDocumentationToContain(completionItem: CompletionItem, value: string): void { expect(completionItem.documentation).toBeDefined(); if (typeof completionItem.documentation === "string") { From dbdb074687176875e55331f0d2645e91b426749d Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Thu, 30 May 2024 19:15:16 +0200 Subject: [PATCH 96/98] Refactor tests to use the same fake WorkflowDataProvider --- .../server-common/tests/testHelpers.ts | 53 +++++++++++ .../tests/unit/completion.test.ts | 88 +++++-------------- .../tests/unit/validation.test.ts | 71 ++------------- 3 files changed, 83 insertions(+), 129 deletions(-) diff --git a/server/packages/server-common/tests/testHelpers.ts b/server/packages/server-common/tests/testHelpers.ts index c1e74fe..b8239dc 100644 --- a/server/packages/server-common/tests/testHelpers.ts +++ b/server/packages/server-common/tests/testHelpers.ts @@ -1,4 +1,5 @@ import { ASTNode, PropertyASTNode } from "../src/ast/types"; +import { WorkflowDataProvider, WorkflowInput, WorkflowOutput } from "../src/languageTypes"; export function expectPropertyNodeToHaveKey(propertyNode: ASTNode | null, expectedPropertyKey: string): void { expect(propertyNode?.type).toBe("property"); @@ -32,3 +33,55 @@ export function parseTemplate( return { contents, position }; } + +export const FAKE_DATASET_INPUT: WorkflowInput = { + name: "My fake dataset", + doc: "This is a simple dataset", + type: "data", +}; + +export const EXPECTED_WORKFLOW_INPUTS: WorkflowInput[] = [ + FAKE_DATASET_INPUT, + { + name: "Input dataset: fake", + doc: "This is a simple dataset with a colon in the name", + type: "File", + }, + { + name: "My fake collection", + doc: "This is a collection", + type: "collection", + }, +]; + +export const EXPECTED_WORKFLOW_OUTPUTS: WorkflowOutput[] = [ + { + name: "My output", + uuid: "1234-5678-91011-1213", + }, + { + name: "My second output", + uuid: "1234-5678-91011-1214", + }, + { + name: "My third output: with colon", + uuid: "1234-5678-91011-1215", + }, +]; + +/** + * A fake implementation of the WorkflowDataProvider interface. + * Simulates a workflow with the expected inputs and outputs. + */ +export const FAKE_WORKFLOW_DATA_PROVIDER: WorkflowDataProvider = { + async getWorkflowInputs(_workflowDocumentUri: string) { + return { + inputs: EXPECTED_WORKFLOW_INPUTS, + }; + }, + async getWorkflowOutputs(_workflowDocumentUri: string) { + return { + outputs: EXPECTED_WORKFLOW_OUTPUTS, + }; + }, +}; diff --git a/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts b/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts index c4f1753..08266ca 100644 --- a/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts +++ b/server/packages/workflow-tests-language-service/tests/unit/completion.test.ts @@ -4,9 +4,14 @@ import { CompletionList, WorkflowDataProvider, WorkflowInput, - WorkflowOutput, } from "@gxwf/server-common/src/languageTypes"; -import { parseTemplate } from "@gxwf/server-common/tests/testHelpers"; +import { + EXPECTED_WORKFLOW_INPUTS, + EXPECTED_WORKFLOW_OUTPUTS, + FAKE_DATASET_INPUT, + FAKE_WORKFLOW_DATA_PROVIDER, + parseTemplate, +} from "@gxwf/server-common/tests/testHelpers"; import { WorkflowTestsLanguageServiceContainerModule } from "@gxwf/workflow-tests-language-service/src/inversify.config"; import { WorkflowTestsSchemaService } from "@gxwf/workflow-tests-language-service/src/schema/service"; import { YAMLCompletionHelper } from "@gxwf/workflow-tests-language-service/src/services/completion/helper"; @@ -25,7 +30,7 @@ describe("Workflow Tests Completion Service", () => { async function getCompletions( contents: string, position: { line: number; character: number }, - workflowDataProvider?: WorkflowDataProvider + workflowDataProvider: WorkflowDataProvider = FAKE_WORKFLOW_DATA_PROVIDER ): Promise { const documentContext = createGxWorkflowTestsDocument(contents, workflowDataProvider); @@ -101,57 +106,6 @@ describe("Workflow Tests Completion Service", () => { }); describe("Workflow Inputs Completion", () => { - let workflowDataProviderMock: WorkflowDataProvider; - const FAKE_DATASET_INPUT: WorkflowInput = { - name: "My fake dataset", - doc: "This is a simple dataset", - type: "data", - }; - const FAKE_DATASET_INPUT_COLON: WorkflowInput = { - name: "Input dataset: fake", - doc: "This is a simple dataset with a colon in the name", - type: "File", - }; - const FAKE_DATASET_COLLECTION_INPUT: WorkflowInput = { - name: "My fake collection", - doc: "This is a collection", - type: "collection", - }; - const FAKE_WORKFLOW_INPUTS: WorkflowInput[] = [ - FAKE_DATASET_INPUT, - FAKE_DATASET_COLLECTION_INPUT, - FAKE_DATASET_INPUT_COLON, - ]; - const FAKE_WORKFLOW_OUTPUTS: WorkflowOutput[] = [ - { - name: "My output", - uuid: "1234-5678-91011-1213", - }, - { - name: "My second output", - uuid: "1234-5678-91011-1214", - }, - { - name: "My third output: with colon", - uuid: "1234-5678-91011-1215", - }, - ]; - - beforeAll(() => { - workflowDataProviderMock = { - async getWorkflowInputs(_workflowDocumentUri: string) { - return { - inputs: FAKE_WORKFLOW_INPUTS, - }; - }, - async getWorkflowOutputs(_workflowDocumentUri: string) { - return { - outputs: FAKE_WORKFLOW_OUTPUTS, - }; - }, - }; - }); - it("should suggest all the defined inputs of the workflow when no inputs are defined in the test", async () => { const template = ` - doc: The docs @@ -159,12 +113,12 @@ describe("Workflow Tests Completion Service", () => { $`; const { contents, position } = parseTemplate(template); - const completions = await getCompletions(contents, position, workflowDataProviderMock); + const completions = await getCompletions(contents, position); expect(completions).not.toBeNull(); - expect(completions?.items.length).toBe(FAKE_WORKFLOW_INPUTS.length); - for (let index = 0; index < FAKE_WORKFLOW_INPUTS.length; index++) { - const workflowInput = FAKE_WORKFLOW_INPUTS[index]; + expect(completions?.items.length).toBe(EXPECTED_WORKFLOW_INPUTS.length); + for (let index = 0; index < EXPECTED_WORKFLOW_INPUTS.length; index++) { + const workflowInput = EXPECTED_WORKFLOW_INPUTS[index]; const completionItem = completions!.items[index]; expectCompletionItemToMatchWorkflowInput(completionItem, workflowInput); } @@ -177,14 +131,14 @@ describe("Workflow Tests Completion Service", () => { Input$`; const { contents, position } = parseTemplate(template); - const completions = await getCompletions(contents, position, workflowDataProviderMock); + const completions = await getCompletions(contents, position); expect(completions).not.toBeNull(); }); it("should not suggest an existing input when suggesting inputs", async () => { const existingInput = FAKE_DATASET_INPUT; - const expectedNumOfRemainingInputs = FAKE_WORKFLOW_INPUTS.length - 1; + const expectedNumOfRemainingInputs = EXPECTED_WORKFLOW_INPUTS.length - 1; const template = ` - doc: The docs job: @@ -192,7 +146,7 @@ describe("Workflow Tests Completion Service", () => { $`; const { contents, position } = parseTemplate(template); - const completions = await getCompletions(contents, position, workflowDataProviderMock); + const completions = await getCompletions(contents, position); expect(completions).not.toBeNull(); expect(completions?.items.length).toBe(expectedNumOfRemainingInputs); @@ -211,7 +165,7 @@ describe("Workflow Tests Completion Service", () => { $`; const { contents, position } = parseTemplate(template); - const completions = await getCompletions(contents, position, workflowDataProviderMock); + const completions = await getCompletions(contents, position); expect(completions).not.toBeNull(); expect(completions?.items.length).toBe(3); @@ -234,7 +188,7 @@ describe("Workflow Tests Completion Service", () => { $`; const { contents, position } = parseTemplate(template); - const completions = await getCompletions(contents, position, workflowDataProviderMock); + const completions = await getCompletions(contents, position); expect(completions).not.toBeNull(); for (const expectedAttribute of expectedAttributes) { @@ -251,12 +205,12 @@ describe("Workflow Tests Completion Service", () => { $`; const { contents, position } = parseTemplate(template); - const completions = await getCompletions(contents, position, workflowDataProviderMock); + const completions = await getCompletions(contents, position); expect(completions).not.toBeNull(); - expect(completions?.items.length).toBe(FAKE_WORKFLOW_OUTPUTS.length); - for (let index = 0; index < FAKE_WORKFLOW_OUTPUTS.length; index++) { - const workflowOutput = FAKE_WORKFLOW_OUTPUTS[index]; + expect(completions?.items.length).toBe(EXPECTED_WORKFLOW_OUTPUTS.length); + for (let index = 0; index < EXPECTED_WORKFLOW_OUTPUTS.length; index++) { + const workflowOutput = EXPECTED_WORKFLOW_OUTPUTS[index]; const completionItem = completions!.items[index]; expect(completionItem.label).toEqual(workflowOutput.name); } diff --git a/server/packages/workflow-tests-language-service/tests/unit/validation.test.ts b/server/packages/workflow-tests-language-service/tests/unit/validation.test.ts index e90ec22..c7f0692 100644 --- a/server/packages/workflow-tests-language-service/tests/unit/validation.test.ts +++ b/server/packages/workflow-tests-language-service/tests/unit/validation.test.ts @@ -1,11 +1,6 @@ import { container } from "@gxwf/server-common/src/inversify.config"; -import { - Diagnostic, - DiagnosticSeverity, - WorkflowDataProvider, - WorkflowInput, - WorkflowOutput, -} from "@gxwf/server-common/src/languageTypes"; +import { Diagnostic, DiagnosticSeverity, WorkflowDataProvider } from "@gxwf/server-common/src/languageTypes"; +import { FAKE_WORKFLOW_DATA_PROVIDER } from "@gxwf/server-common/tests/testHelpers"; import { WorkflowTestsLanguageServiceContainerModule } from "@gxwf/workflow-tests-language-service/src/inversify.config"; import { WorkflowTestsValidationService } from "@gxwf/workflow-tests-language-service/src/services/validation"; import "reflect-metadata"; @@ -19,7 +14,10 @@ describe("Workflow Tests Validation Service", () => { service = container.get(TYPES.WorkflowTestsValidationService); }); - async function validate(contents: string, workflowDataProvider?: WorkflowDataProvider): Promise { + async function validate( + contents: string, + workflowDataProvider: WorkflowDataProvider = FAKE_WORKFLOW_DATA_PROVIDER + ): Promise { const documentContext = createGxWorkflowTestsDocument(contents, workflowDataProvider); return await service.doValidation(documentContext); } @@ -39,57 +37,6 @@ describe("Workflow Tests Validation Service", () => { }); describe("Workflow Inputs/Outputs Validation", () => { - let workflowDataProviderMock: WorkflowDataProvider; - const FAKE_DATASET_INPUT: WorkflowInput = { - name: "My fake dataset", - doc: "This is a simple dataset", - type: "data", - }; - const FAKE_DATASET_INPUT_COLON: WorkflowInput = { - name: "Input dataset: fake", - doc: "This is a simple dataset with a colon in the name", - type: "File", - }; - const FAKE_DATASET_COLLECTION_INPUT: WorkflowInput = { - name: "My fake collection", - doc: "This is a collection", - type: "collection", - }; - const FAKE_WORKFLOW_INPUTS: WorkflowInput[] = [ - FAKE_DATASET_INPUT, - FAKE_DATASET_COLLECTION_INPUT, - FAKE_DATASET_INPUT_COLON, - ]; - const FAKE_WORKFLOW_OUTPUTS: WorkflowOutput[] = [ - { - name: "My output", - uuid: "1234-5678-91011-1213", - }, - { - name: "My second output", - uuid: "1234-5678-91011-1214", - }, - { - name: "My third output: with colon", - uuid: "1234-5678-91011-1215", - }, - ]; - - beforeAll(() => { - workflowDataProviderMock = { - async getWorkflowInputs(_workflowDocumentUri: string) { - return { - inputs: FAKE_WORKFLOW_INPUTS, - }; - }, - async getWorkflowOutputs(_workflowDocumentUri: string) { - return { - outputs: FAKE_WORKFLOW_OUTPUTS, - }; - }, - }; - }); - it("should pass validation when the inputs and outputs are defined in the workflow", async () => { const testDocumentContents = ` - doc: The docs @@ -98,7 +45,7 @@ describe("Workflow Tests Validation Service", () => { outputs: My output: out/output.txt`; - const diagnostics = await validate(testDocumentContents, workflowDataProviderMock); + const diagnostics = await validate(testDocumentContents); expect(diagnostics).not.toBeNull(); }); @@ -111,7 +58,7 @@ describe("Workflow Tests Validation Service", () => { outputs: My output: out/output.txt`; - const diagnostics = await validate(testDocumentContents, workflowDataProviderMock); + const diagnostics = await validate(testDocumentContents); expect(diagnostics.length).toBe(1); expect(diagnostics[0].message).toBe('Input "Missing input" is not defined in the associated workflow.'); @@ -126,7 +73,7 @@ describe("Workflow Tests Validation Service", () => { outputs: Missing output: out/output.txt`; - const diagnostics = await validate(testDocumentContents, workflowDataProviderMock); + const diagnostics = await validate(testDocumentContents); expect(diagnostics.length).toBe(1); expect(diagnostics[0].message).toBe('Output "Missing output" is not defined in the associated workflow.'); From 0368f78774af5bba6d8cc14509af559bfcd4feb2 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Thu, 30 May 2024 19:43:25 +0200 Subject: [PATCH 97/98] Implement Hover for inputs and outputs in tests documents --- .../src/services/hover.ts | 84 ++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/server/packages/workflow-tests-language-service/src/services/hover.ts b/server/packages/workflow-tests-language-service/src/services/hover.ts index 845cb04..9e9205a 100644 --- a/server/packages/workflow-tests-language-service/src/services/hover.ts +++ b/server/packages/workflow-tests-language-service/src/services/hover.ts @@ -1,3 +1,4 @@ +import { ASTNode } from "@gxwf/server-common/src/ast/types"; import { DocumentContext, Hover, @@ -5,12 +6,13 @@ import { MarkupKind, Position, Range, + WorkflowTestsDocument, } from "@gxwf/server-common/src/languageTypes"; import { inject, injectable } from "inversify"; -import { TYPES } from "../types"; import { isAllSchemasMatched, isBoolean } from "../schema/adapter"; import { JSONSchemaRef } from "../schema/jsonSchema"; import { WorkflowTestsSchemaService } from "../schema/service"; +import { TYPES } from "../types"; export interface WorkflowTestsHoverService { doHover(documentContext: DocumentContext, position: Position): Promise; @@ -50,6 +52,20 @@ export class WorkflowTestsHoverServiceImpl implements WorkflowTestsHoverService documentContext.textDocument.positionAt(hoverRangeNode.offset + hoverRangeNode.length) ); + if (this.parentPropertyMatchesKey(node, "job")) { + const inputHover = await this.getHoverForWorkflowInput(documentContext, node, hoverRange); + if (inputHover) { + return inputHover; + } + } + + if (this.parentPropertyMatchesKey(node, "outputs")) { + const outputHover = await this.getHoverForWorkflowOutput(documentContext, node, hoverRange); + if (outputHover) { + return outputHover; + } + } + const matchingSchemas = this.schemaService.getMatchingSchemas(documentContext, node.offset); const removePipe = (value: string): string => { @@ -114,4 +130,70 @@ export class WorkflowTestsHoverServiceImpl implements WorkflowTestsHoverService }; return result; } + + private parentPropertyMatchesKey(node: ASTNode, key: string): boolean { + // The first parent is the value node (object), the second parent is the property node + // we are looking for. + // ParentNode (property) <- Target node + // |- ValueNode (object) + // |- Node (property) <- Initial node + const parent = node.parent?.parent; + if (!parent || parent.type !== "property") { + return false; + } + return parent.keyNode.value === key; + } + + private async getHoverForWorkflowInput( + documentContext: DocumentContext, + node: ASTNode, + hoverRange: Range + ): Promise { + if (node.type !== "property") { + return null; + } + const key = node.keyNode.value; + const testDocument = documentContext as WorkflowTestsDocument; + const inputs = await testDocument.getWorkflowInputs(); + const matchingInput = inputs.find((input) => input.name === key); + if (matchingInput) { + const hoverContents = [`**${matchingInput.name}** (Input)`]; + if (matchingInput.doc) { + hoverContents.push(matchingInput.doc); + } + if (matchingInput.type) { + hoverContents.push(`Type: ${matchingInput.type}`); + } + return this.createHover(hoverContents.join("\n\n"), hoverRange); + } + return this.createHover("Input not found", hoverRange); + } + + private async getHoverForWorkflowOutput( + documentContext: DocumentContext, + node: ASTNode, + hoverRange: Range + ): Promise { + if (node.type !== "property") { + return null; + } + const key = node.keyNode.value; + const testDocument = documentContext as WorkflowTestsDocument; + const outputs = await testDocument.getWorkflowOutputs(); + const matchingOutput = outputs.find((output) => output.name === key); + if (matchingOutput) { + const hoverContents = [`**${matchingOutput.name}** (Output)`]; + if (matchingOutput.doc) { + hoverContents.push(matchingOutput.doc); + } + if (matchingOutput.type) { + hoverContents.push(`Type: ${matchingOutput.type}`); + } + if (matchingOutput.uuid) { + hoverContents.push(matchingOutput.uuid); + } + return this.createHover(hoverContents.join("\n\n"), hoverRange); + } + return this.createHover("Output not found", hoverRange); + } } From 94de9737908604386064bcd0e34b95ea8e33b5f8 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Thu, 30 May 2024 19:43:42 +0200 Subject: [PATCH 98/98] Add tests for hover in test documents --- .../tests/unit/hover.test.ts | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 server/packages/workflow-tests-language-service/tests/unit/hover.test.ts diff --git a/server/packages/workflow-tests-language-service/tests/unit/hover.test.ts b/server/packages/workflow-tests-language-service/tests/unit/hover.test.ts new file mode 100644 index 0000000..845756d --- /dev/null +++ b/server/packages/workflow-tests-language-service/tests/unit/hover.test.ts @@ -0,0 +1,132 @@ +import { container } from "@gxwf/server-common/src/inversify.config"; +import { Hover, MarkupContent, WorkflowDataProvider } from "@gxwf/server-common/src/languageTypes"; +import { FAKE_WORKFLOW_DATA_PROVIDER, parseTemplate } from "@gxwf/server-common/tests/testHelpers"; +import { WorkflowTestsLanguageServiceContainerModule } from "@gxwf/workflow-tests-language-service/src/inversify.config"; +import { WorkflowTestsHoverService } from "@gxwf/workflow-tests-language-service/src/services/hover"; +import { TYPES } from "@gxwf/workflow-tests-language-service/src/types"; +import "reflect-metadata"; +import { createGxWorkflowTestsDocument } from "../testHelpers"; + +describe("Workflow Tests Hover Service", () => { + let service: WorkflowTestsHoverService; + beforeAll(() => { + container.load(WorkflowTestsLanguageServiceContainerModule); + service = container.get(TYPES.WorkflowTestsHoverService); + }); + + async function getHover( + contents: string, + position: { line: number; character: number }, + workflowDataProvider: WorkflowDataProvider = FAKE_WORKFLOW_DATA_PROVIDER + ): Promise { + const documentContext = createGxWorkflowTestsDocument(contents, workflowDataProvider); + + return await service.doHover(documentContext, position); + } + + it("should return the documentation of the `doc` property when hovering over it", async () => { + const template = ` +- do$c: The docs + `; + const { contents, position } = parseTemplate(template); + const hover = await getHover(contents, position); + + expect(hover).not.toBeNull(); + + expectHoverToContainContents(hover!, "Doc"); + expectHoverToContainContents(hover!, "Describes the purpose of the test"); + }); + + it("should return the documentation of the `job` property when hovering over it", async () => { + const template = ` +- doc: The docs + jo$b: + `; + const { contents, position } = parseTemplate(template); + const hover = await getHover(contents, position); + + expect(hover).not.toBeNull(); + + expectHoverToContainContents(hover!, "Job"); + expectHoverToContainContents(hover!, "Defines job to execute"); + }); + + it("should return the documentation of the `outputs` property when hovering over it", async () => { + const template = ` +- doc: The docs + outp$uts: + `; + const { contents, position } = parseTemplate(template); + const hover = await getHover(contents, position); + + expect(hover).not.toBeNull(); + + expectHoverToContainContents(hover!, "Outputs"); + expectHoverToContainContents(hover!, "Defines assertions about outputs"); + }); + + describe("Workflow Inputs/Outputs Hover", () => { + it.each<[string, string[]]>([ + [ + ` +- job: + My fake$ dataset: + `, + ["My fake dataset", "This is a simple dataset", "Type: data"], + ], + [ + ` +- job: + 'Input$ dataset: fake': + `, + ["Input dataset: fake", "This is a simple dataset with a colon in the name", "Type: File"], + ], + [ + ` +- job: + My fake$ collection: + `, + ["My fake collection", "This is a collection", "Type: collection"], + ], + [ + ` +- outputs: + My out$put: + `, + ["My output", "1234-5678-91011-1213"], + ], + [ + ` +- outputs: + My second out$put: + `, + ["My second output", "1234-5678-91011-1214"], + ], + [ + ` +- outputs: + 'My third out$put: with colon': + `, + ["My third output: with colon", "1234-5678-91011-1215"], + ], + ])( + "should return the documentation of the workflow inputs when hovering over them", + async (template: string, expectedHoverContents: string[]) => { + const { contents, position } = parseTemplate(template); + const hover = await getHover(contents, position); + + expect(hover).not.toBeNull(); + + for (const expectedContent of expectedHoverContents) { + expectHoverToContainContents(hover!, expectedContent); + } + } + ); + }); +}); + +function expectHoverToContainContents(hover: Hover, expectedContents: string): void { + expect(hover.contents).toBeDefined(); + const contents = hover.contents as MarkupContent; + expect(contents.value).toContain(expectedContents); +}