Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic JSON schema validation #18

Merged
merged 7 commits into from
Apr 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 55 additions & 5 deletions server/src/languageService.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,42 @@
import { ASTNode, getLanguageService, LanguageService } from "vscode-json-languageservice";
import {
getLanguageService,
LanguageService,
LanguageServiceParams,
DocumentLanguageSettings,
Diagnostic,
JSONSchema,
LanguageSettings,
SchemaConfiguration,
} from "vscode-json-languageservice";
import {
TextDocument,
Range,
FormattingOptions,
TextEdit,
WorkflowDocument,
WorkflowLanguageService,
Position,
Hover,
} from "./languageTypes";
import NativeWorkflowSchema from "../../workflow-languages/schemas/native.schema.json";

/**
* A wrapper around the JSON Language Service to support language features
* for native Galaxy workflow files AKA '.ga' workflows.
*/
export class NativeWorkflowLanguageService implements WorkflowLanguageService {
private _jsonLanguageService: LanguageService;
private _documentSettings: DocumentLanguageSettings = { schemaValidation: "error" };

constructor() {
this._jsonLanguageService = getLanguageService({});
const params: LanguageServiceParams = {};
const settings = this.getLanguageSettings();
this._jsonLanguageService = getLanguageService(params);
this._jsonLanguageService.configure(settings);
}

public get schema(): JSONSchema {
return NativeWorkflowSchema;
}

public parseWorkflowDocument(document: TextDocument): WorkflowDocument {
Expand All @@ -27,8 +47,38 @@ export class NativeWorkflowLanguageService implements WorkflowLanguageService {
public format(document: TextDocument, range: Range, options: FormattingOptions): TextEdit[] {
return this._jsonLanguageService.format(document, range, options);
}
}

export function getRange(document: TextDocument, node: ASTNode) {
return Range.create(document.positionAt(node.offset), document.positionAt(node.offset + node.length));
public async doValidation(workflowDocument: WorkflowDocument): Promise<Diagnostic[]> {
const schemaValidationResults = await this._jsonLanguageService.doValidation(
workflowDocument.textDocument,
workflowDocument.jsonDocument,
this._documentSettings,
this.schema
);
return schemaValidationResults;
}

public async doHover(workflowDocument: WorkflowDocument, position: Position): Promise<Hover | null> {
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,
};
}
}
1 change: 1 addition & 0 deletions server/src/languageTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Diagnostic[]>;
}

export abstract class ServerContext {
Expand Down
24 changes: 23 additions & 1 deletion server/src/models/workflowDocument.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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));
}
}
14 changes: 3 additions & 11 deletions server/src/providers/hoverProvider.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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"),
Expand All @@ -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") {
Expand Down
14 changes: 7 additions & 7 deletions server/src/providers/symbolsProvider.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 [];
}
Expand All @@ -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);
Expand All @@ -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,
Expand Down
27 changes: 20 additions & 7 deletions server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -68,21 +68,34 @@ 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);
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() {
this.workflowDocuments.dispose();
}

private validate(workflowDocument: WorkflowDocument) {
this.languageService.doValidation(workflowDocument).then((diagnostics) => {
this.connection.sendDiagnostics({ uri: workflowDocument.textDocument.uri, diagnostics });
});
}

private clearValidation(textDocument: TextDocument) {
this.connection.sendDiagnostics({ uri: textDocument.uri, diagnostics: [] });
}
}
2 changes: 2 additions & 0 deletions server/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
],
"module": "commonjs",
"moduleResolution": "node",
"resolveJsonModule": true,
"esModuleInterop": true,
"sourceMap": true,
"strict": true
},
Expand Down
Loading