From eed491da459423e161e8e683f776398a8c8e75c4 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sun, 3 Apr 2022 14:42:14 +0200 Subject: [PATCH 1/7] Add basic JSON schema for native workflows This schema is incomplete and inaccurate --- workflow-languages/schemas/native.schema.json | 199 ++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 workflow-languages/schemas/native.schema.json diff --git a/workflow-languages/schemas/native.schema.json b/workflow-languages/schemas/native.schema.json new file mode 100644 index 0000000..0ec1c2e --- /dev/null +++ b/workflow-languages/schemas/native.schema.json @@ -0,0 +1,199 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "a_galaxy_workflow": { + "type": "string" + }, + "annotation": { + "type": "string" + }, + "creator": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "class": { + "type": "string" + }, + "identifier": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "class", + "identifier", + "name" + ] + } + ] + }, + "format-version": { + "type": "string" + }, + "license": { + "type": "string" + }, + "name": { + "type": "string" + }, + "release": { + "type": "string" + }, + "steps": { + "type": "object", + "$ref": "#/definitions/stepDef" + }, + "tags": { + "type": "array", + "items": [ + { + "type": "string" + }, + { + "type": "string" + } + ] + }, + "uuid": { + "type": "string" + } + }, + "required": [ + "a_galaxy_workflow", + "annotation", + "creator", + "format-version", + "license", + "name", + "release", + "steps", + "tags", + "uuid" + ], + "definitions": { + "stepDef": { + "type": "object", + "patternProperties": { + "[0-9]*": { + "type": "object", + "properties": { + "annotation": { + "type": "string" + }, + "content_id": { + "type": "string" + }, + "errors": { + "type": "null" + }, + "id": { + "type": "integer" + }, + "input_connections": { + "type": "object" + }, + "inputs": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "description", + "name" + ] + } + ] + }, + "label": { + "type": "string" + }, + "name": { + "type": "string" + }, + "outputs": { + "type": "array", + "items": {} + }, + "position": { + "type": "object", + "properties": { + "bottom": { + "type": "number" + }, + "height": { + "type": "number" + }, + "left": { + "type": "number" + }, + "right": { + "type": "number" + }, + "top": { + "type": "number" + }, + "width": { + "type": "integer" + }, + "x": { + "type": "number" + }, + "y": { + "type": "number" + } + } + }, + "tool_id": { + "type": "null" + }, + "tool_state": { + "type": "string" + }, + "tool_version": { + "type": "null" + }, + "type": { + "type": "string" + }, + "uuid": { + "type": "string" + }, + "workflow_outputs": { + "type": "array", + "items": {} + } + }, + "required": [ + "annotation", + "content_id", + "id", + "input_connections", + "inputs", + "name", + "outputs", + "position", + "tool_id", + "tool_state", + "tool_version", + "type", + "uuid", + "workflow_outputs" + ] + } + } + } + } +} From 977645cd92a849394e3a7651dfe98dd6e918e9ea Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sun, 3 Apr 2022 14:48:03 +0200 Subject: [PATCH 2/7] Add basic schema validation for native workflows --- server/src/languageService.ts | 29 +++++++++++++++++++++++++++-- server/src/languageTypes.ts | 1 + server/src/server.ts | 14 +++++++++++--- server/tsconfig.json | 2 ++ 4 files changed, 41 insertions(+), 5 deletions(-) diff --git a/server/src/languageService.ts b/server/src/languageService.ts index 1929825..0c03289 100644 --- a/server/src/languageService.ts +++ b/server/src/languageService.ts @@ -1,4 +1,12 @@ -import { ASTNode, getLanguageService, LanguageService } from "vscode-json-languageservice"; +import { + ASTNode, + getLanguageService, + LanguageService, + LanguageServiceParams, + DocumentLanguageSettings, + Diagnostic, + JSONSchema, +} from "vscode-json-languageservice"; import { TextDocument, Range, @@ -7,6 +15,7 @@ import { WorkflowDocument, WorkflowLanguageService, } from "./languageTypes"; +import NativeSchema from "../../workflow-languages/schemas/native.schema.json"; /** * A wrapper around the JSON Language Service to support language features @@ -14,9 +23,15 @@ import { */ export class NativeWorkflowLanguageService implements WorkflowLanguageService { private _jsonLanguageService: LanguageService; + private _galaxyNativeWorkflowSchema: JSONSchema = NativeSchema; + private _documentSettings: DocumentLanguageSettings; constructor() { - this._jsonLanguageService = getLanguageService({}); + const params: LanguageServiceParams = {}; + this._jsonLanguageService = getLanguageService(params); + this._documentSettings = { + schemaValidation: "error", + }; } public parseWorkflowDocument(document: TextDocument): WorkflowDocument { @@ -27,6 +42,16 @@ export class NativeWorkflowLanguageService implements WorkflowLanguageService { public format(document: TextDocument, range: Range, options: FormattingOptions): TextEdit[] { return this._jsonLanguageService.format(document, range, options); } + + public async doValidation(workflowDocument: WorkflowDocument): Promise { + const schemaValidationResults = await this._jsonLanguageService.doValidation( + workflowDocument.textDocument, + workflowDocument.jsonDocument, + this._documentSettings, + this._galaxyNativeWorkflowSchema + ); + return schemaValidationResults; + } } export function getRange(document: TextDocument, node: ASTNode) { diff --git a/server/src/languageTypes.ts b/server/src/languageTypes.ts index ebdaec6..9bae35f 100644 --- a/server/src/languageTypes.ts +++ b/server/src/languageTypes.ts @@ -121,6 +121,7 @@ export interface FormattingOptions extends LSPFormattingOptions { export interface WorkflowLanguageService { format(document: TextDocument, range: Range, options: FormattingOptions): TextEdit[]; parseWorkflowDocument(document: TextDocument): WorkflowDocument; + doValidation(workflowDocument: WorkflowDocument): Promise; } export abstract class ServerContext { diff --git a/server/src/server.ts b/server/src/server.ts index ca43043..a749368 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -7,7 +7,7 @@ import { WorkspaceFolder, } from "vscode-languageserver"; import { CleanWorkflowCommand } from "./commands/cleanWorkflow"; -import { WorkflowLanguageService, TextDocument } from "./languageTypes"; +import { WorkflowLanguageService, TextDocument, WorkflowDocument } from "./languageTypes"; import { WorkflowDocuments } from "./models/workflowDocuments"; import { SymbolsProvider } from "./providers/symbolsProvider"; import { FormattingProvider } from "./providers/formattingProvider"; @@ -68,14 +68,16 @@ export class GalaxyWorkflowLanguageServer { this.documents.onDidClose((event) => this.onDidClose(event.document)); } - private onDocumentOpen(document: TextDocument) { - const workflowDocument = this.languageService.parseWorkflowDocument(document); + private onDocumentOpen(textDocument: TextDocument) { + const workflowDocument = this.languageService.parseWorkflowDocument(textDocument); this.workflowDocuments.addOrReplaceWorkflowDocument(workflowDocument); + this.validate(workflowDocument); } private onDidChangeContent(document: TextDocument) { const workflowDocument = this.languageService.parseWorkflowDocument(document); this.workflowDocuments.addOrReplaceWorkflowDocument(workflowDocument); + this.validate(workflowDocument); } private onDidClose(document: TextDocument) { @@ -85,4 +87,10 @@ export class GalaxyWorkflowLanguageServer { private cleanup() { this.workflowDocuments.dispose(); } + + private validate(workflowDocument: WorkflowDocument) { + this.languageService.doValidation(workflowDocument).then((diagnostics) => { + this.connection.sendDiagnostics({ uri: workflowDocument.textDocument.uri, diagnostics }); + }); + } } diff --git a/server/tsconfig.json b/server/tsconfig.json index 746d2bc..e869053 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -7,6 +7,8 @@ ], "module": "commonjs", "moduleResolution": "node", + "resolveJsonModule": true, + "esModuleInterop": true, "sourceMap": true, "strict": true }, From 03372d679c38bbf099ccefb27b41c8696d9a35f3 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sun, 3 Apr 2022 14:59:52 +0200 Subject: [PATCH 3/7] Clear diganostics on document close --- server/src/server.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/server/src/server.ts b/server/src/server.ts index a749368..566f9b1 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -74,14 +74,15 @@ export class GalaxyWorkflowLanguageServer { this.validate(workflowDocument); } - private onDidChangeContent(document: TextDocument) { - const workflowDocument = this.languageService.parseWorkflowDocument(document); + private onDidChangeContent(textDocument: TextDocument) { + const workflowDocument = this.languageService.parseWorkflowDocument(textDocument); this.workflowDocuments.addOrReplaceWorkflowDocument(workflowDocument); this.validate(workflowDocument); } - private onDidClose(document: TextDocument) { - this.workflowDocuments.removeWorkflowDocument(document.uri); + private onDidClose(textDocument: TextDocument) { + this.workflowDocuments.removeWorkflowDocument(textDocument.uri); + this.clearValidation(textDocument); } private cleanup() { @@ -93,4 +94,8 @@ export class GalaxyWorkflowLanguageServer { this.connection.sendDiagnostics({ uri: workflowDocument.textDocument.uri, diagnostics }); }); } + + private clearValidation(textDocument: TextDocument) { + this.connection.sendDiagnostics({ uri: textDocument.uri, diagnostics: [] }); + } } From 9d418832e605b779a7a13b5d0f995939f23ec998 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sun, 3 Apr 2022 18:54:37 +0200 Subject: [PATCH 4/7] Add helper funtions to `WorkflowDocument` --- server/src/models/workflowDocument.ts | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/server/src/models/workflowDocument.ts b/server/src/models/workflowDocument.ts index 7a97078..1b77175 100644 --- a/server/src/models/workflowDocument.ts +++ b/server/src/models/workflowDocument.ts @@ -1,5 +1,5 @@ import { JSONDocument } from "vscode-json-languageservice"; -import { TextDocument } from "../languageTypes"; +import { TextDocument, Range, Position, ASTNode } from "../languageTypes"; /** * This class contains information about workflow semantics. @@ -25,4 +25,26 @@ export class WorkflowDocument { public get jsonDocument(): JSONDocument { return this._jsonDocument; } + + public getNodeAtPosition(position: Position): ASTNode | undefined { + const offset = this.textDocument.offsetAt(position); + return this.jsonDocument.getNodeFromOffset(offset); + } + + public getNodeRange(node: ASTNode): Range { + return Range.create( + this.textDocument.positionAt(node.offset), + this.textDocument.positionAt(node.offset + node.length) + ); + } + + public getNodeRangeAtPosition(position: Position): Range { + const node = this.getNodeAtPosition(position); + return node ? this.getNodeRange(node) : this.getDefaultRangeAtPosition(position); + } + + private getDefaultRangeAtPosition(position: Position): Range { + const offset = this.textDocument.offsetAt(position); + return Range.create(this.textDocument.positionAt(offset), this.textDocument.positionAt(offset + 1)); + } } From ee3b355b8ef3fabdff2d3ea34e99d786d8a0f7b8 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sun, 3 Apr 2022 18:57:13 +0200 Subject: [PATCH 5/7] Refactor language service and providers --- server/src/languageService.ts | 47 +++++++++++++++++++------ server/src/providers/hoverProvider.ts | 14 ++------ server/src/providers/symbolsProvider.ts | 14 ++++---- 3 files changed, 46 insertions(+), 29 deletions(-) diff --git a/server/src/languageService.ts b/server/src/languageService.ts index 0c03289..93fcbcc 100644 --- a/server/src/languageService.ts +++ b/server/src/languageService.ts @@ -1,11 +1,12 @@ import { - ASTNode, getLanguageService, LanguageService, LanguageServiceParams, DocumentLanguageSettings, Diagnostic, JSONSchema, + LanguageSettings, + SchemaConfiguration, } from "vscode-json-languageservice"; import { TextDocument, @@ -14,8 +15,10 @@ import { TextEdit, WorkflowDocument, WorkflowLanguageService, + Position, + Hover, } from "./languageTypes"; -import NativeSchema from "../../workflow-languages/schemas/native.schema.json"; +import NativeWorkflowSchema from "../../workflow-languages/schemas/native.schema.json"; /** * A wrapper around the JSON Language Service to support language features @@ -23,15 +26,17 @@ import NativeSchema from "../../workflow-languages/schemas/native.schema.json"; */ export class NativeWorkflowLanguageService implements WorkflowLanguageService { private _jsonLanguageService: LanguageService; - private _galaxyNativeWorkflowSchema: JSONSchema = NativeSchema; - private _documentSettings: DocumentLanguageSettings; + private _documentSettings: DocumentLanguageSettings = { schemaValidation: "error" }; constructor() { const params: LanguageServiceParams = {}; + const settings = this.getLanguageSettings(); this._jsonLanguageService = getLanguageService(params); - this._documentSettings = { - schemaValidation: "error", - }; + this._jsonLanguageService.configure(settings); + } + + public get schema(): JSONSchema { + return NativeWorkflowSchema; } public parseWorkflowDocument(document: TextDocument): WorkflowDocument { @@ -48,12 +53,32 @@ export class NativeWorkflowLanguageService implements WorkflowLanguageService { workflowDocument.textDocument, workflowDocument.jsonDocument, this._documentSettings, - this._galaxyNativeWorkflowSchema + this.schema ); return schemaValidationResults; } -} -export function getRange(document: TextDocument, node: ASTNode) { - return Range.create(document.positionAt(node.offset), document.positionAt(node.offset + node.length)); + public async doHover(workflowDocument: WorkflowDocument, position: Position): Promise { + const hover = await this._jsonLanguageService.doHover( + workflowDocument.textDocument, + position, + workflowDocument.jsonDocument + ); + return hover; + } + + private getLanguageSettings(): LanguageSettings { + const settings: LanguageSettings = { + schemas: [this.getWorkflowSchemaConfig()], + }; + return settings; + } + + private getWorkflowSchemaConfig(): SchemaConfiguration { + return { + uri: this.schema.$id ?? "", + fileMatch: ["**.ga"], + schema: this.schema, + }; + } } diff --git a/server/src/providers/hoverProvider.ts b/server/src/providers/hoverProvider.ts index 6b61af6..6031d31 100644 --- a/server/src/providers/hoverProvider.ts +++ b/server/src/providers/hoverProvider.ts @@ -1,7 +1,6 @@ -import { Hover, HoverParams, MarkupKind, Position, ASTNode, WorkflowDocument, PropertyASTNode } from "../languageTypes"; +import { Hover, HoverParams, MarkupKind, ASTNode, PropertyASTNode } from "../languageTypes"; import { GalaxyWorkflowLanguageServer } from "../server"; import { Provider } from "./provider"; -import { getRange } from "../languageService"; import { ArrayASTNode, BooleanASTNode, NullASTNode, NumberASTNode, StringASTNode } from "vscode-json-languageservice"; export class HoverProvider extends Provider { @@ -23,12 +22,12 @@ export class HoverProvider extends Provider { if (!workflowDocument) { return undefined; } - const node = this.getNodeAtDocumentPosition(workflowDocument, params.position); + const node = workflowDocument.getNodeAtPosition(params.position); if (!node) { return undefined; } const contentLines = this.printNode(node); - const hoverRange = getRange(workflowDocument.textDocument, node); + const hoverRange = workflowDocument.getNodeRange(node); const markdown = { kind: MarkupKind.Markdown, value: contentLines.join("\n\n"), @@ -40,13 +39,6 @@ export class HoverProvider extends Provider { return result; } - private getNodeAtDocumentPosition(workflowDocument: WorkflowDocument, position: Position): ASTNode | undefined { - const document = workflowDocument.textDocument; - const offset = document.offsetAt(position); - const node = workflowDocument.jsonDocument.getNodeFromOffset(offset); - return node; - } - private printNode(node: ASTNode): string[] { const contentLines = [`## ${node.type}`]; if (node.type === "object") { diff --git a/server/src/providers/symbolsProvider.ts b/server/src/providers/symbolsProvider.ts index 17d72f7..beaff7f 100644 --- a/server/src/providers/symbolsProvider.ts +++ b/server/src/providers/symbolsProvider.ts @@ -1,12 +1,11 @@ -import { getRange } from "../languageService"; import { - TextDocument, DocumentSymbolParams, DocumentSymbol, SymbolKind, ASTNode, PropertyASTNode, ObjectASTNode, + WorkflowDocument, } from "../languageTypes"; import { GalaxyWorkflowLanguageServer } from "../server"; import { Provider } from "./provider"; @@ -26,13 +25,14 @@ export class SymbolsProvider extends Provider { public onDocumentSymbol(params: DocumentSymbolParams): DocumentSymbol[] { const workflowDocument = this.workflowDocuments.get(params.textDocument.uri); if (workflowDocument) { - const symbols = this.getSymbols(workflowDocument.textDocument, workflowDocument.jsonDocument.root); + const symbols = this.getSymbols(workflowDocument); return symbols; } return []; } - private getSymbols(document: TextDocument, root: ASTNode | undefined): DocumentSymbol[] { + private getSymbols(workflowDocument: WorkflowDocument): DocumentSymbol[] { + const root = workflowDocument.jsonDocument.root; if (!root) { return []; } @@ -48,7 +48,7 @@ export class SymbolsProvider extends Provider { if (IGNORE_SYMBOL_NAMES.has(name)) { return; } - const range = getRange(document, node); + const range = workflowDocument.getNodeRange(node); const selectionRange = range; const symbol = { name, kind: this.getSymbolKind(node.type), range, selectionRange, children: [] }; result.push(symbol); @@ -70,8 +70,8 @@ export class SymbolsProvider extends Provider { if (IGNORE_SYMBOL_NAMES.has(name)) { return; } - const range = getRange(document, property); - const selectionRange = getRange(document, property.keyNode); + const range = workflowDocument.getNodeRange(property); + const selectionRange = workflowDocument.getNodeRange(property.keyNode); const children: DocumentSymbol[] = []; const symbol: DocumentSymbol = { name: name, From 8abe4f9560ecbb77985c520d6adc3bb635869014 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sun, 3 Apr 2022 19:00:04 +0200 Subject: [PATCH 6/7] Add schema identifier for native workflows This ID is completely made up... but should be unique --- server/src/languageService.ts | 2 +- workflow-languages/schemas/native.schema.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/languageService.ts b/server/src/languageService.ts index 93fcbcc..6251f08 100644 --- a/server/src/languageService.ts +++ b/server/src/languageService.ts @@ -76,7 +76,7 @@ export class NativeWorkflowLanguageService implements WorkflowLanguageService { private getWorkflowSchemaConfig(): SchemaConfiguration { return { - uri: this.schema.$id ?? "", + uri: this.schema.id ?? "", fileMatch: ["**.ga"], schema: this.schema, }; diff --git a/workflow-languages/schemas/native.schema.json b/workflow-languages/schemas/native.schema.json index 0ec1c2e..303ef0e 100644 --- a/workflow-languages/schemas/native.schema.json +++ b/workflow-languages/schemas/native.schema.json @@ -1,4 +1,5 @@ { + "id": "https://galaxyproject.org/schemas/workflow/native", "$schema": "http://json-schema.org/draft-04/schema#", "type": "object", "properties": { From 5ef6eceba80e1065521247766d7a5ec7edfff4af Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sun, 3 Apr 2022 19:45:25 +0200 Subject: [PATCH 7/7] Improve native workflow JSON schema Fix some types and add example documentation for `a_galaxy_workflow` property --- workflow-languages/schemas/native.schema.json | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/workflow-languages/schemas/native.schema.json b/workflow-languages/schemas/native.schema.json index 303ef0e..3687703 100644 --- a/workflow-languages/schemas/native.schema.json +++ b/workflow-languages/schemas/native.schema.json @@ -4,7 +4,13 @@ "type": "object", "properties": { "a_galaxy_workflow": { - "type": "string" + "type": "string", + "title": "## Galaxy Workflow indicator", + "markdownDescription": "Indicates that this JSON document is a **Galaxy Workflow**", + "default": "true", + "enum": [ + "true" + ] }, "annotation": { "type": "string" @@ -47,7 +53,7 @@ }, "steps": { "type": "object", - "$ref": "#/definitions/stepDef" + "$ref": "#/definitions/step" }, "tags": { "type": "array", @@ -77,17 +83,18 @@ "uuid" ], "definitions": { - "stepDef": { + "step": { "type": "object", + "additionalProperties": false, "patternProperties": { - "[0-9]*": { + "^[0-9]*$": { "type": "object", "properties": { "annotation": { "type": "string" }, "content_id": { - "type": "string" + "$ref": "#/definitions/optionalString" }, "errors": { "type": "null" @@ -119,7 +126,7 @@ ] }, "label": { - "type": "string" + "$ref": "#/definitions/optionalString" }, "name": { "type": "string" @@ -158,13 +165,13 @@ } }, "tool_id": { - "type": "null" + "$ref": "#/definitions/optionalString" }, "tool_state": { "type": "string" }, "tool_version": { - "type": "null" + "$ref": "#/definitions/optionalString" }, "type": { "type": "string" @@ -179,7 +186,6 @@ }, "required": [ "annotation", - "content_id", "id", "input_connections", "inputs", @@ -187,14 +193,18 @@ "outputs", "position", "tool_id", - "tool_state", - "tool_version", "type", "uuid", "workflow_outputs" ] } } + }, + "optionalString": { + "type": [ + "string", + "null" + ] } } }