Skip to content

Commit

Permalink
Merge pull request #67 from davelopez/improve_format2_support
Browse files Browse the repository at this point in the history
Improve gxFormat2 auto-completion support
  • Loading branch information
davelopez authored Jun 2, 2024
2 parents 12fccd9 + 0ff7b52 commit e9308ef
Show file tree
Hide file tree
Showing 18 changed files with 664 additions and 117 deletions.
25 changes: 12 additions & 13 deletions server/gx-workflow-ls-format2/src/languageService.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import {
TextDocument,
Range,
CompletionList,
Diagnostic,
FormattingOptions,
TextEdit,
Hover,
LanguageService,
LanguageServiceBase,
Position,
Hover,
CompletionList,
Diagnostic,
Range,
TextDocument,
TextEdit,
WorkflowValidator,
LanguageService,
} 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 { inject, injectable } from "inversify";
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";

const LANGUAGE_ID = "gxformat2";

Expand Down Expand Up @@ -61,19 +61,18 @@ export class GxFormat2WorkflowLanguageServiceImpl
}

public override doHover(documentContext: GxFormat2WorkflowDocument, position: Position): Promise<Hover | null> {
return this._hoverService.doHover(documentContext.textDocument, position, documentContext.nodeManager);
return this._hoverService.doHover(documentContext, position);
}

public override async doComplete(
documentContext: GxFormat2WorkflowDocument,
position: Position
): Promise<CompletionList | null> {
return this._completionService.doComplete(documentContext.textDocument, position, documentContext.nodeManager);
return this._completionService.doComplete(documentContext, position);
}

protected override async doValidation(documentContext: GxFormat2WorkflowDocument): Promise<Diagnostic[]> {
const format2WorkflowDocument = documentContext as GxFormat2WorkflowDocument;
const diagnostics = await this._yamlLanguageService.doValidation(format2WorkflowDocument.yamlDocument);
const diagnostics = await this._yamlLanguageService.doValidation(documentContext.yamlDocument);
for (const validator of this._validationServices) {
const results = await validator.doValidation(documentContext);
diagnostics.push(...results);
Expand Down
53 changes: 51 additions & 2 deletions server/gx-workflow-ls-format2/src/schema/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,45 @@ export interface SchemaNode {
isRoot: boolean;
}

export class EnumSchemaNode implements SchemaNode {
public static definitions: SchemaDefinitions;

private readonly _schemaEnum: SchemaEnum;

constructor(schemaEnum: SchemaEnum) {
this._schemaEnum = schemaEnum;
}

public get name(): string {
return this._schemaEnum.name;
}

public get symbols(): string[] {
return this._schemaEnum.symbols;
}

public get documentation(): string | undefined {
return this._schemaEnum.doc;
}

public get isRoot(): boolean {
return !!this._schemaEnum.documentRoot;
}

public get canBeArray(): boolean {
return false;
}

public get typeRef(): string {
return this._schemaEnum.name;
}

//Override toString for debugging purposes
public toString(): string {
return `EnumSchemaNode: ${this.name} - ${this.symbols}`;
}
}

export class FieldSchemaNode implements SchemaNode, IdMapper {
public static definitions: SchemaDefinitions;

Expand Down Expand Up @@ -281,11 +320,16 @@ export class FieldSchemaNode implements SchemaNode, IdMapper {
private isObjectType(typeName: string): boolean {
return FieldSchemaNode.definitions.records.has(typeName);
}

//Override toString for debugging purposes
public toString(): string {
return `FieldSchemaNode: ${this.name} - ${this.typeRef}`;
}
}

export class RecordSchemaNode implements SchemaNode {
public static definitions: SchemaDefinitions;
public static readonly NULL: SchemaNode = new RecordSchemaNode({
public static readonly NULL = new RecordSchemaNode({
name: "null",
type: "null",
fields: [],
Expand Down Expand Up @@ -343,11 +387,16 @@ export class RecordSchemaNode implements SchemaNode {
public getFieldByName(name: string): FieldSchemaNode | undefined {
return this.fields.find((t) => t.name === name);
}

//Override toString for debugging purposes
public toString(): string {
return `RecordSchemaNode: ${this.name}`;
}
}

export interface SchemaDefinitions {
records: Map<string, RecordSchemaNode>;
fields: Map<string, FieldSchemaNode>;
enums: Map<string, EnumSchemaNode>;
specializations: Map<string, string>;
primitiveTypes: Set<string>;
}
19 changes: 9 additions & 10 deletions server/gx-workflow-ls-format2/src/schema/schemaLoader.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
EnumSchemaNode,
FieldSchemaNode,
isSchemaEntryBase,
isSchemaEnumType,
Expand Down Expand Up @@ -70,28 +71,26 @@ export class GalaxyWorkflowFormat2SchemaLoader implements GalaxyWorkflowSchemaLo
private loadSchemaDefinitions(schemaEntries: Map<string, SchemaEntry>): SchemaDefinitions {
const definitions: SchemaDefinitions = {
records: new Map<string, RecordSchemaNode>(),
fields: new Map<string, FieldSchemaNode>(),
enums: new Map<string, EnumSchemaNode>(),
specializations: new Map<string, string>(),
primitiveTypes: new Set<string>(),
};

this.expandEntries(schemaEntries.values());
schemaEntries.forEach((v, k) => {
if (isSchemaRecord(v)) {
definitions.records.set(k, new RecordSchemaNode(v));
const record = new RecordSchemaNode(v);
definitions.records.set(k, record);
if (v.specialize) {
v.specialize.forEach((sp) => {
definitions.specializations.set(sp.specializeFrom, sp.specializeTo);
});
}
v.fields.forEach((field) => {
if (definitions.fields.has(field.name)) {
if (this.enableDebugTrace) console.debug("****** DUPLICATED FIELD", field.name);
}
definitions.fields.set(field.name, new FieldSchemaNode(field));
});
} else if (isSchemaEnumType(v) && v.name === "GalaxyType") {
definitions.primitiveTypes = new Set(v.symbols);
} else if (isSchemaEnumType(v)) {
definitions.enums.set(k, new EnumSchemaNode(v));
if (v.name === "GalaxyType") {
definitions.primitiveTypes = new Set(v.symbols);
}
}
});
return definitions;
Expand Down
31 changes: 20 additions & 11 deletions server/gx-workflow-ls-format2/src/schema/schemaNodeResolver.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NodePath, Segment } from "@gxwf/server-common/src/ast/types";
import { RecordSchemaNode, SchemaDefinitions, SchemaNode, SchemaRecord } from "./definitions";
import { FieldSchemaNode, RecordSchemaNode, SchemaDefinitions, SchemaNode, SchemaRecord } from "./definitions";

export interface SchemaNodeResolver {
rootNode: SchemaNode;
Expand All @@ -9,26 +9,35 @@ export interface SchemaNodeResolver {
}

export class SchemaNodeResolverImpl implements SchemaNodeResolver {
public readonly rootNode: SchemaNode;
public readonly rootNode: RecordSchemaNode;
constructor(
public readonly definitions: SchemaDefinitions,
root?: SchemaRecord
) {
this.rootNode = root ? new RecordSchemaNode(root) : RecordSchemaNode.NULL;
}

/**
* Determines the matching schema node for the last segment in the path.
* @param path The path to resolve from root to leaf
* @returns The matching schema node for the last segment in the path or undefined
* if the path does not match any schema node.
*/
public resolveSchemaContext(path: NodePath): SchemaNode | undefined {
const toWalk = path.slice();
const lastSegment = toWalk.pop();
const schemaNodeFound = this.getSchemaNodeForSegment(lastSegment);
while (toWalk.length && !schemaNodeFound) {
const parentSegment = toWalk.pop();
const parentNode = this.getSchemaNodeForSegment(parentSegment);
if (parentNode) {
return this.getSchemaNodeForSegment(parentNode.typeRef);
let currentSegment = toWalk.shift();
let currentSchemaNode: SchemaNode | undefined = this.rootNode;

while (currentSegment !== undefined) {
if (currentSchemaNode instanceof RecordSchemaNode) {
currentSchemaNode = currentSchemaNode.fields.find((f) => f.name === currentSegment);
} else if (currentSchemaNode instanceof FieldSchemaNode) {
const typeNode = this.getSchemaNodeByTypeRef(currentSchemaNode.typeRef);
currentSchemaNode = typeNode;
}
currentSegment = toWalk.shift();
}
return schemaNodeFound;
return currentSchemaNode;
}

public getSchemaNodeByTypeRef(typeRef: string): SchemaNode | undefined {
Expand All @@ -41,7 +50,7 @@ export class SchemaNodeResolverImpl implements SchemaNodeResolver {
if (this.definitions.records.has(pathSegment)) {
return this.definitions.records.get(pathSegment);
}
return this.definitions.fields.get(pathSegment);
return this.definitions.enums.get(pathSegment);
}
return undefined;
}
Expand Down
110 changes: 74 additions & 36 deletions server/gx-workflow-ls-format2/src/services/completionService.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,55 @@
import { ASTNodeManager } from "@gxwf/server-common/src/ast/nodeManager";
import { ASTNode } from "@gxwf/server-common/src/ast/types";
import {
CompletionItem,
CompletionItemKind,
CompletionList,
Position,
TextDocument,
} from "@gxwf/server-common/src/languageTypes";
import { CompletionItem, CompletionItemKind, CompletionList, Position } from "@gxwf/server-common/src/languageTypes";
import { TextBuffer } from "@gxwf/yaml-language-service/src/utils/textBuffer";
import { RecordSchemaNode, SchemaNode, SchemaNodeResolver } from "../schema";
import { GxFormat2WorkflowDocument } from "../gxFormat2WorkflowDocument";
import { FieldSchemaNode, RecordSchemaNode, SchemaNode, SchemaNodeResolver } from "../schema";
import { EnumSchemaNode } from "../schema/definitions";

export class GxFormat2CompletionService {
constructor(protected readonly schemaNodeResolver: SchemaNodeResolver) {}

public doComplete(
textDocument: TextDocument,
position: Position,
nodeManager: ASTNodeManager
): Promise<CompletionList> {
public doComplete(documentContext: GxFormat2WorkflowDocument, position: Position): Promise<CompletionList> {
const textDocument = documentContext.textDocument;
const nodeManager = documentContext.nodeManager;
const result: CompletionList = {
items: [],
isIncomplete: false,
};
// TODO: Refactor most of this to an Context class with all the information around the cursor
const textBuffer = new TextBuffer(textDocument);
const text = textBuffer.getText();
const offset = textBuffer.getOffsetAt(position);
const node = nodeManager.getNodeFromOffset(offset);
if (!node) {
return Promise.resolve(result);
}
if (text.charAt(offset - 1) === ":") {
return Promise.resolve(result);
}

const currentWord = textBuffer.getCurrentWord(offset);

DEBUG_printNodeName(node);
let node = nodeManager.getNodeFromOffset(offset);

const existing = nodeManager.getDeclaredPropertyNames(node);
if (nodeManager.isRoot(node)) {
result.items = this.getProposedItems(this.schemaNodeResolver.rootNode, currentWord, existing);
return Promise.resolve(result);
}
const nodePath = nodeManager.getPathFromNode(node);
const schemaNode = this.schemaNodeResolver.resolveSchemaContext(nodePath);
let schemaNode = this.schemaNodeResolver.resolveSchemaContext(nodePath);
if (schemaNode === undefined) {
// Try parent node
node = node?.parent;
const parentPath = nodePath.slice(0, -1);
const parentNode = this.schemaNodeResolver.resolveSchemaContext(parentPath);
schemaNode = parentNode;
}
if (schemaNode) {
result.items = this.getProposedItems(schemaNode, currentWord, existing);
const existing = nodeManager.getDeclaredPropertyNames(node);
result.items = this.getProposedItems(schemaNode, textBuffer, existing, offset);
}
return Promise.resolve(result);
}

private getProposedItems(schemaNode: SchemaNode, currentWord: string, exclude: Set<string>): CompletionItem[] {
private getProposedItems(
schemaNode: SchemaNode,
textBuffer: TextBuffer,
exclude: Set<string>,
offset: number
): CompletionItem[] {
const result: CompletionItem[] = [];
const currentWord = textBuffer.getCurrentWord(offset);
const overwriteRange = textBuffer.getCurrentWordRange(offset);
const position = textBuffer.getPosition(offset);
const isPositionAfterColon = textBuffer.isPositionAfterToken(position, ":");
if (schemaNode instanceof RecordSchemaNode) {
if (isPositionAfterColon) {
return result; // Do not suggest fields inlined after colon
}
schemaNode.fields
.filter((f) => f.name.startsWith(currentWord))
.forEach((field) => {
Expand All @@ -64,15 +60,57 @@ export class GxFormat2CompletionService {
sortText: `_${field.name}`,
kind: CompletionItemKind.Field,
insertText: `${field.name}: `,
textEdit: {
range: overwriteRange,
newText: `${field.name}: `,
},
};
result.push(item);
});
} else if (schemaNode instanceof FieldSchemaNode) {
if (this.schemaNodeResolver.definitions.primitiveTypes.has(schemaNode.typeRef)) {
const defaultValue = String(schemaNode.default ?? "");
if (defaultValue) {
const item: CompletionItem = {
label: defaultValue,
kind: CompletionItemKind.Value,
documentation: schemaNode.documentation,
insertText: defaultValue,
textEdit: {
range: overwriteRange,
newText: defaultValue,
},
};
result.push(item);
return result;
}
}
const schemaRecord = this.schemaNodeResolver.getSchemaNodeByTypeRef(schemaNode.typeRef);
if (schemaRecord instanceof EnumSchemaNode) {
schemaRecord.symbols
.filter((v) => v.startsWith(currentWord))
.forEach((value) => {
if (exclude.has(value)) return;
const item: CompletionItem = {
label: value,
sortText: `_${value}`,
kind: CompletionItemKind.EnumMember,
documentation: schemaRecord.documentation,
insertText: value,
textEdit: {
range: overwriteRange,
newText: value,
},
};
result.push(item);
});
}
}
return result;
}
}

function DEBUG_printNodeName(node: ASTNode): void {
function _DEBUG_printNodeName(node: ASTNode): void {
let nodeName = "_root_";
if (node?.type === "property") {
nodeName = node.keyNode.value;
Expand Down
Loading

0 comments on commit e9308ef

Please sign in to comment.