diff --git a/server/src/assets.ts b/server/src/assets.ts index 64ea1aa..762ab29 100644 --- a/server/src/assets.ts +++ b/server/src/assets.ts @@ -1,4 +1,4 @@ -import { CompletionItemKind, CompletionItem } from "vscode-languageserver/node"; +import { CompletionItemKind, CompletionItem, InsertTextMode } from "vscode-languageserver/node"; import * as fs from "fs"; import * as log from "./log"; import { CompletionVisitor } from "./completions"; @@ -16,7 +16,14 @@ export async function resetAssetIndex(): Promise { ASSET_INDEX = await AssetIndex.new(); } -class Asset { +abstract class AssetInterface { + abstract id: string; + abstract location: string; +} + +type AssetInterfaceConstructor = new (id: string, location: string) => T; + +class Asset implements AssetInterface { id: string; location: string; @@ -27,35 +34,50 @@ class Asset { async visit(_provider: CompletionVisitor): Promise { throw new Error("Method 'visit' must be implemented."); } - static getCompletions(): CompletionItem[] { - throw new Error("Method 'getCompletions' must be implemented."); + static all(): Asset[] { + throw new Error("Static method 'all' must be implemented."); + } + static getCompletions(_condition: (asset: Asset) => boolean = () => true): CompletionItem[] { + throw new Error("Static method 'getCompletions' must be implemented."); } } export class Block extends Asset { async visit(provider: CompletionVisitor): Promise { await provider.onBlock(this); } - static getCompletions(): CompletionItem[] { + static all(): Block[] { + return ASSET_INDEX?.blocks ?? []; + } + static getCompletions(condition: (asset: Block) => boolean = () => true): CompletionItem[] { const completions: CompletionItem[] = []; ASSET_INDEX?.blocks.forEach((element) => { - completions.push({ - label: element.id, - kind: CompletionItemKind.Field, - data: completions.length, - }); + if (condition(element)) + completions.push({ + label: element.id, + kind: CompletionItemKind.Class, + data: completions.length, + detail: "block", + }); }); return completions; } } export class BlockTexture extends Asset { - static getCompletions(): CompletionItem[] { + static all(): BlockTexture[] { + return ASSET_INDEX?.blockTextures ?? []; + } + static getCompletions( + condition: (asset: BlockTexture) => boolean = () => true, + ): CompletionItem[] { const completions: CompletionItem[] = []; ASSET_INDEX?.blockTextures.forEach((element) => { - completions.push({ - label: element.id, - kind: CompletionItemKind.File, - data: completions.length, - }); + if (condition(element)) + completions.push({ + label: element.id, + kind: CompletionItemKind.Struct, + data: completions.length, + detail: "block texture", + }); }); return completions; } @@ -64,27 +86,41 @@ export class Item extends Asset { async visit(provider: CompletionVisitor): Promise { await provider.onItem(this); } - static getCompletions(): CompletionItem[] { + static all(): Item[] { + return ASSET_INDEX?.items ?? []; + } + static getCompletions(condition: (asset: Item) => boolean = () => true): CompletionItem[] { const completions: CompletionItem[] = []; ASSET_INDEX?.items.forEach((element) => { - completions.push({ - label: element.id, - kind: CompletionItemKind.Function, - data: completions.length, - }); + if (condition(element)) + completions.push({ + label: element.id, + kind: CompletionItemKind.Class, + data: completions.length, + detail: "item", + }); }); return completions; } } export class ItemTexture extends Asset { - static getCompletions(): CompletionItem[] { + static all(): ItemTexture[] { + return ASSET_INDEX?.itemTextures ?? []; + } + static getCompletions( + condition: (asset: ItemTexture) => boolean = () => true, + ): CompletionItem[] { const completions: CompletionItem[] = []; ASSET_INDEX?.itemTextures.forEach((element) => { - completions.push({ - label: element.id, - kind: CompletionItemKind.File, - data: completions.length, - }); + if (condition(element)) + completions.push({ + label: element.id, + insertText: '"' + element.id + '"', + insertTextMode: InsertTextMode.asIs, + kind: CompletionItemKind.Struct, + data: completions.length, + detail: "item texture", + }); }); return completions; } @@ -93,14 +129,19 @@ export class Tool extends Asset { async visit(provider: CompletionVisitor): Promise { await provider.onTool(this); } - static getCompletions(): CompletionItem[] { + static all(): Tool[] { + return ASSET_INDEX?.tools ?? []; + } + static getCompletions(condition: (asset: Tool) => boolean = () => true): CompletionItem[] { const completions: CompletionItem[] = []; ASSET_INDEX?.tools.forEach((element) => { - completions.push({ - label: element.id, - kind: CompletionItemKind.Method, - data: completions.length, - }); + if (condition(element)) + completions.push({ + label: element.id, + kind: CompletionItemKind.Class, + data: completions.length, + detail: "tool", + }); }); return completions; } @@ -109,27 +150,37 @@ export class Biome extends Asset { async visit(provider: CompletionVisitor): Promise { await provider.onBiome(this); } - static getCompletions(): CompletionItem[] { + static all(): Biome[] { + return ASSET_INDEX?.biomes ?? []; + } + static getCompletions(condition: (asset: Biome) => boolean = () => true): CompletionItem[] { const completions: CompletionItem[] = []; ASSET_INDEX?.biomes.forEach((element) => { - completions.push({ - label: element.id, - kind: CompletionItemKind.Interface, - data: completions.length, - }); + if (condition(element)) + completions.push({ + label: element.id, + kind: CompletionItemKind.Class, + data: completions.length, + detail: "biome", + }); }); return completions; } } export class Model extends Asset { - static getCompletions(): CompletionItem[] { + static all(): Model[] { + return ASSET_INDEX?.models ?? []; + } + static getCompletions(condition: (asset: Model) => boolean = () => true): CompletionItem[] { const completions: CompletionItem[] = []; ASSET_INDEX?.models.forEach((element) => { - completions.push({ - label: element.id, - kind: CompletionItemKind.Module, - data: completions.length, - }); + if (condition(element)) + completions.push({ + label: element.id, + kind: CompletionItemKind.Module, + data: completions.length, + detail: "model", + }); }); return completions; } @@ -138,27 +189,37 @@ export class SBB extends Asset { async visit(provider: CompletionVisitor): Promise { await provider.onSBB(this); } - static getCompletions(): CompletionItem[] { + static all(): SBB[] { + return ASSET_INDEX?.structureBuildingBlocks ?? []; + } + static getCompletions(condition: (asset: SBB) => boolean = () => true): CompletionItem[] { const completions: CompletionItem[] = []; ASSET_INDEX?.structureBuildingBlocks.forEach((element) => { - completions.push({ - label: element.id, - kind: CompletionItemKind.Class, - data: completions.length, - }); + if (condition(element)) + completions.push({ + label: element.id, + kind: CompletionItemKind.Class, + data: completions.length, + detail: "SBB", + }); }); return completions; } } export class Blueprint extends Asset { - static getCompletions(): CompletionItem[] { + static all(): Blueprint[] { + return ASSET_INDEX?.blueprints ?? []; + } + static getCompletions(condition: (asset: Blueprint) => boolean = () => true): CompletionItem[] { const completions: CompletionItem[] = []; ASSET_INDEX?.blueprints.forEach((element) => { - completions.push({ - label: element.id, - kind: CompletionItemKind.File, - data: completions.length, - }); + if (condition(element)) + completions.push({ + label: element.id, + kind: CompletionItemKind.Module, + data: completions.length, + detail: "blueprint", + }); }); return completions; } @@ -190,73 +251,73 @@ export class AssetIndex { index.blocks, "blocks", addon.name, - ".zig.zon" + ".zig.zon", ); await AssetIndex.registerAsset( BlockTexture, index.blockTextures, "blocks/textures", addon.name, - ".png" + ".png", ); await AssetIndex.registerAsset( Item, index.items, "items", addon.name, - ".zig.zon" + ".zig.zon", ); await AssetIndex.registerTextures( ItemTexture, index.itemTextures, "items/textures", - addon.name + addon.name, ); await AssetIndex.registerAsset( Tool, index.tools, "tools", addon.name, - ".zig.zon" + ".zig.zon", ); await AssetIndex.registerAsset( Biome, index.biomes, "biomes", addon.name, - ".zig.zon" + ".zig.zon", ); await AssetIndex.registerAsset( Model, index.models, "models", addon.name, - ".obj" + ".obj", ); await AssetIndex.registerAsset( SBB, index.structureBuildingBlocks, "sbb", addon.name, - ".zig.zon" + ".zig.zon", ); await AssetIndex.registerAsset( Blueprint, index.blueprints, "sbb", addon.name, - ".blp" + ".blp", ); } return index; } - static async registerAsset( - cls: new (id: string, location: string) => AssetT, - storage: AssetT[], + static async registerAsset( + cls: AssetInterfaceConstructor, + storage: T[], scope: string, addon: string, - extension: string + extension: string, ) { const basePath = `assets/${addon}/${scope}/`; if (!fs.existsSync(basePath) || !fs.statSync(basePath).isDirectory()) return; @@ -284,11 +345,11 @@ export class AssetIndex { log.log(`Registered ${scope} asset: '${id}' at ${location}`); } } - static async registerTextures( - cls: new (id: string, location: string) => AssetT, - storage: AssetT[], + static async registerTextures( + cls: AssetInterfaceConstructor, + storage: T[], scope: string, - addon: string + addon: string, ) { const basePath = `assets/${addon}/${scope}/`; if (!fs.existsSync(basePath) || !fs.statSync(basePath).isDirectory()) return; diff --git a/server/src/completions.ts b/server/src/completions.ts index b5c4776..10c448b 100644 --- a/server/src/completions.ts +++ b/server/src/completions.ts @@ -1,124 +1,565 @@ -import { CompletionItem, CompletionItemKind, CompletionParams } from "vscode-languageserver/node"; +import { + CompletionItem, + CompletionItemKind, + CompletionParams, + InsertTextFormat, + InsertTextMode, +} from "vscode-languageserver/node"; import { Block, Item, Tool, Biome, SBB, Model, BlockTexture, ItemTexture } from "./assets"; -import { ZonNode, Is, ZonSyntaxError, ZonIdentifier, ZonEntry, ZonObject } from "./zon"; +import { + ZonNode, + ZonSyntaxError, + ZonIdentifier, + ZonEntry, + ZonObject, + ZonEmpty, + ZonArray, + ZonNodePath, + ZonString, + ZonNumber, +} from "./zon"; + +interface Completion { + type: T; + keyKind?: CompletionItemKind; + keyDetail?: string; +} + +interface NumberCompletion extends Completion<"number"> { + completions?: () => void; +} +interface ColorCompletion extends Completion<"color"> { + completions?: () => void; +} +interface StringCompletion extends Completion<"string"> { + completions: (node: ZonNode) => void; +} +interface IdentifierCompletion extends Completion<"identifier"> { + completions: (node: ZonIdentifier) => void; +} +type BooleanCompletion = Completion<"boolean">; +interface ObjectCompletion extends Completion<"object"> { + completions: () => Record; +} +interface ArrayCompletion extends Completion<"array"> { + completions: () => AnyCompletion; +} + +type AnyCompletion = + | NumberCompletion + | StringCompletion + | IdentifierCompletion + | ObjectCompletion + | ArrayCompletion + | BooleanCompletion + | ColorCompletion; + +class ResolveCompletion { + visitor: CompletionVisitor; + + constructor(visitor: CompletionVisitor) { + this.visitor = visitor; + } + + /** + * Append all the relevant completions for a given object indicated by `path` parameter + * as described by schema from `completion` argument. Completion items are appended directly + * to visitor specified in a constructor of this class. + */ + any(path: ZonNode[], completion: AnyCompletion): void { + switch (completion.type) { + case "object": { + const [first, ...rest] = path; + + if (first instanceof ZonSyntaxError && [".", ""].includes(first.value)) { + this.visitor.completions.push({ + label: ".{}", + insertText: ".{$0}", + kind: CompletionItemKind.Method, + insertTextFormat: InsertTextFormat.Snippet, + }); + break; + } + + if ( + !( + first instanceof ZonObject || + first instanceof ZonEmpty || + first instanceof ZonArray + ) + ) + break; + + this.obj(first, rest, completion); + break; + } + case "array": { + const [first, ...rest] = path; + if ( + !( + first instanceof ZonArray || + first instanceof ZonEmpty || + first instanceof ZonSyntaxError + ) + ) + return; + this.array(first, rest, completion); + break; + } + case "number": { + const [first, ...rest] = path; + if (!(first instanceof ZonNumber || first instanceof ZonSyntaxError)) return; + if (rest.length > 0) return; + this.number(first, completion); + break; + } + case "string": { + const [first, ...rest] = path; + if (!(first instanceof ZonString || first instanceof ZonSyntaxError)) return; + if (rest.length > 0) return; + this.string(first, completion); + break; + } + case "identifier": { + const [first, ...rest] = path; + if (!(first instanceof ZonIdentifier || first instanceof ZonSyntaxError)) return; + if (rest.length > 0) return; + this.identifier(first, completion); + break; + } + case "boolean": { + const [first, ...rest] = path; + if (!(first instanceof ZonString || first instanceof ZonSyntaxError)) return; + if (rest.length > 0) return; + this.boolean(); + break; + } + case "color": { + if (path.length == 0) { + this.color(completion); + } + break; + } + } + } + + obj( + zonObject: ZonObject | ZonEmpty | ZonArray, + rest: ZonNode[], + completion: ObjectCompletion, + ): void { + const objectCompletions = completion.completions(); + + // Object can be misinterpreted as an array if there is a syntax error / it was not finished yet. + if (zonObject instanceof ZonArray) { + if (rest.length > 1) return; // Deep completion inside arrays is not supported form object perspective. + if (rest.length === 0) { + const target = rest[0] as ZonSyntaxError; + this.suggestObjectKeys(zonObject, objectCompletions, target.getValueString() ?? ""); + } else { + this.suggestObjectKeys(zonObject, objectCompletions); + } + return; + } + + // Completion was done directly inside an object, likely to add a new key. + if (rest.length === 0 || zonObject instanceof ZonEmpty) { + this.suggestObjectKeys(zonObject, objectCompletions); + return; + } + + // We have at least one `rest` node to inspect now. + const [newFirst, ...newRest] = rest; + if (newFirst instanceof ZonSyntaxError || newFirst instanceof ZonIdentifier) { + this.suggestObjectKeys(newFirst, objectCompletions, newFirst.getValueString() ?? ""); + return; + } + if (!(newFirst instanceof ZonEntry)) return; + const entryNode = newFirst; + + if (newRest.length === 0) { + this.visitor.completions.push({ + label: `=`, + kind: CompletionItemKind.Text, + }); + return; + } + + const childNode = newRest[0]; + if (childNode === entryNode.key) { + if (childNode instanceof ZonSyntaxError || childNode instanceof ZonIdentifier) { + this.suggestObjectKeys( + zonObject, + objectCompletions, + childNode.getValueString() ?? "", + ); + return; + } + // No other viable completions available. + return; + } + if (childNode === entryNode.value) { + const completion = objectCompletions[entryNode.key.getValueString() ?? ""]; + if (completion) return this.any(newRest, completion); + return; + } + } + /** + * Append suggestion entries for all relevant keys that can be present in object described by `completion`. + * `prefix` argument can be used to narrow down the list by startsWith check. + */ + suggestObjectKeys( + node: ZonNode, + objectCompletions: Record, + prefix = "", + ): void { + const existingKeys: string[] = []; + if (node instanceof ZonObject) { + existingKeys.push(...node.getKeyStrings()); + } + for (const key in objectCompletions) { + // Keys cannot be duplicated, so we shouldn't suggest duplicated keys. + if (existingKeys.includes(key)) continue; + // If we already have a prefix, we should skip everyting that starts differently. + if (!key.startsWith(prefix)) continue; + + this.visitor.completions.push({ + label: `.${key}`, + insertText: `.${key} = `, + kind: objectCompletions[key].keyKind ?? CompletionItemKind.Keyword, + detail: objectCompletions[key].keyDetail ?? objectCompletions[key].type, + }); + } + } + array( + zonArray: ZonArray | ZonEmpty | ZonSyntaxError, + rest: ZonNode[], + completion: ArrayCompletion, + ): void { + if (rest.length == 0 && zonArray instanceof ZonSyntaxError) { + this.visitor.completions.push({ + label: ".{}", + insertText: ".{$0},", + insertTextFormat: InsertTextFormat.Snippet, + kind: CompletionItemKind.Keyword, + detail: "array", + }); + return; + } + return this.any(rest, completion.completions()); + } + number(_first: ZonNumber | ZonSyntaxError, _completion: NumberCompletion): void {} + color(_completion: ColorCompletion): void { + this.visitor.completions.push({ + label: "0x${0}${1}${2}${3}", + insertText: "0x${1:ff}${2:ff}${3:ff}${4:ff}$0", + kind: CompletionItemKind.Method, + insertTextFormat: InsertTextFormat.Snippet, + detail: "ARGB color template", + }); + this.visitor.completions.push({ + label: `0xffffffff`, + kind: CompletionItemKind.Constant, + detail: "ARGB white color value", + }); + this.visitor.completions.push({ + label: `0xff000000`, + kind: CompletionItemKind.Constant, + detail: "ARGB black color value", + }); + } + string(first: ZonString | ZonSyntaxError, completion: StringCompletion): void { + completion.completions(first); + } + boolean(): void { + const completions = ["true", "false"].map((e: string): CompletionItem => { + return { + label: e, + kind: CompletionItemKind.Constant, + detail: "item texture", + }; + }); + this.visitor.completions.push(...completions); + } + identifier(first: ZonIdentifier | ZonSyntaxError, completion: IdentifierCompletion): void { + completion.completions(first); + } +} export class CompletionVisitor { params: CompletionParams; completions: CompletionItem[]; ast: ZonNode; node: ZonNode; + nodePath: ZonNodePath; - constructor(params: CompletionParams, ast: ZonNode, node: ZonNode) { + constructor(params: CompletionParams, ast: ZonNode, node: ZonNode, nodePath: ZonNodePath) { this.params = params; this.completions = []; this.ast = ast; this.node = node; + this.nodePath = nodePath; } async onBlock(_asset: Block): Promise { - const topLevelKeys = [ - ".rotation", - ".blockHealth", - ".blockResistance", - ".tags", - ".emittedLight", - ".absorbedLight", - ".degradable", - ".selectable", - ".replacable", - ".gui", - "transparent", - ".collide", - ".alwaysViewThrough", - ".viewThrough", - ".hasBackFace", - ".friction", - ".allowOres", - ".tickEvent", - ".touchFunction", - ".blockEntity", - ".ore", - ]; - if (Is.childOfTopLevelObject(this.node)) { - if (Is.entryKeyEqual(this.node, "model")) { - this.completions.push(...Model.getCompletions()); - return; - } - if (Is.entryKeyMatch(this.node, /texture.*/)) { - this.completions.push(...BlockTexture.getCompletions()); - return; - } - if (Is.childOfEntry(this.node)) { - if (this.node instanceof ZonSyntaxError || this.node instanceof ZonIdentifier) { - this.addCompletionsFromArray(topLevelKeys); - return; - } - } - } - if (Is.topLevelObject(this.node)) { - this.addCompletionsFromArray(topLevelKeys); + new ResolveCompletion(this).any(this.nodePath.path, { + type: "object", + completions: () => ({ + item: { + type: "object", + completions: () => ({ + material: { + type: "object", + completions: () => ({ + durability: { type: "number" }, + massDamage: { type: "number" }, + hardnessDamage: { type: "number" }, + swingSpeed: { type: "number" }, + textureRoughness: { type: "number" }, + colors: { + type: "array", + completions: () => ({ type: "color" }), + }, + modifiers: { + type: "array", + completions: () => ({ type: "number" }), + }, + }), + }, + texture: { + type: "string", + completions: this.getItemTextureCompletionCallback(), + }, + }), + }, + rotation: { + type: "string", + completions: this.getRotationCompletionCallback(), + }, + blockHealth: { + type: "string", + completions: () => {}, + }, + blockResistance: { + type: "string", + completions: () => {}, + }, + tags: { + type: "array", + completions: () => ({ + type: "string", + completions: this.getHardCodedIdentifierCompletionCallback([ + ".air", + ".fluid", + ".sbbChild", + ".fluidPlaceable", + ".chiselable", + ]), + }), + }, + emittedLight: { type: "number" }, + absorbedLight: { type: "number" }, + degradable: { type: "boolean" }, + selectable: { type: "boolean" }, + replacable: { type: "boolean" }, + transparent: { type: "boolean" }, + collide: { type: "boolean" }, + alwaysViewThrough: { type: "boolean" }, + viewThrough: { type: "boolean" }, + hasBackFace: { type: "boolean" }, + friction: { type: "number" }, + bounciness: { type: "number" }, + density: { type: "number" }, + terminalVelocity: { type: "number" }, + mobility: { type: "number" }, + allowOres: { + type: "boolean", + completions: () => {}, + }, + blockEntity: { + type: "string", + completions: () => {}, + }, + ore: { + type: "object", + completions: () => ({ + veins: { type: "number" }, + size: { type: "number" }, + height: { type: "number" }, + minHeight: { type: "number" }, + density: { type: "number" }, + }), + }, + model: { + type: "string", + completions: this.getModelCompletionCallback(), + }, + ...(() => { + const completions: Record = {}; + const completionCallback = this.getBlockTextureCompletionCallback(); + for (const suffix of [ + ...Array(16).keys(), + "", + "_front", + "_left", + "_right", + "_top", + "_bottom", + ]) { + completions[`texture${suffix}`] = { + type: "string", + completions: completionCallback, + }; + } + return completions; + })(), + }), + }); + + return; + } + getBlockTextureCompletionCallback(): (node: ZonNode) => void { + return (node: ZonNode) => { + const completions = BlockTexture.all().map((e: BlockTexture): CompletionItem => { + return { + label: e.id, + insertText: node instanceof ZonString ? e.id : '"' + e.id + '"', + insertTextMode: InsertTextMode.asIs, + kind: CompletionItemKind.Struct, + detail: "block texture", + }; + }); + this.completions.push(...completions); return; - } + }; + } + getItemTextureCompletionCallback(): (node: ZonNode) => void { + return (node: ZonNode) => { + const completions = ItemTexture.all().map((e: ItemTexture): CompletionItem => { + return { + label: e.id, + insertText: node instanceof ZonString ? e.id : '"' + e.id + '"', + insertTextMode: InsertTextMode.asIs, + kind: CompletionItemKind.Struct, + detail: "item texture", + }; + }); + this.completions.push(...completions); + }; + } + getModelCompletionCallback(): (node: ZonNode) => void { + return (node: ZonNode) => { + const completions = Model.all().map((e: Model): CompletionItem => { + return { + label: e.id, + insertText: node instanceof ZonString ? e.id : '"' + e.id + '"', + insertTextMode: InsertTextMode.asIs, + kind: CompletionItemKind.Module, + detail: "model", + }; + }); + this.completions.push(...completions); + }; + } + getHardCodedIdentifierCompletionCallback(array: string[]): (node: ZonNode) => void { + return () => { + const completions = array.map((s: string): CompletionItem => { + return { + label: s, + kind: CompletionItemKind.Constant, + detail: "model", + }; + }); + this.completions.push(...completions); + }; } - addCompletionsFromArray(symbols: string[]): void { + getRotationCompletionCallback(): (node: ZonNode) => void { + return (node: ZonNode) => { + const completions = [ + "cubyz:branch", + "cubyz:carpet", + "cubyz:direction", + "cubyz:fence", + "cubyz:hanging", + "cubyz:log", + "cubyz:no_rotation", + "cubyz:ore", + "cubyz:planar", + "cubyz:sign", + "cubyz:stairs", + "cubyz:texture_pile", + "cubyz:torch", + ].map((s: string): CompletionItem => { + return { + label: s, + insertText: node instanceof ZonString ? s : '"' + s + '"', + insertTextMode: InsertTextMode.asIs, + kind: CompletionItemKind.Module, + detail: "model", + }; + }); + this.completions.push(...completions); + }; + } + addCompletionsLike(symbols: string[], like: string, extra: object = {}): void { + return this.addCompletions( + symbols.filter((item) => item.startsWith(like) || item.substring(1).startsWith(like)), + extra, + ); + } + addCompletions(symbols: string[], extra: object = {}): void { symbols.forEach((symbol) => { this.completions.push({ label: symbol, kind: CompletionItemKind.Constant, + ...extra, }); }); } async onItem(_asset: Item): Promise { - const topLevelKeys = [ - ".name", - ".tags", - ".stackSize", - ".material", - ".block", - ".texture", - ".foodValue", - ]; - const materialKeys = [ - ".density", - ".elasticity", - ".hardness", - ".textureRoughness", - ".colors", - ]; - if (Is.childOfTopLevelObject(this.node)) { - if (Is.entryKeyEqual(this.node, "texture")) { - this.completions.push(...ItemTexture.getCompletions()); - return; - } - if (Is.childOfEntry(this.node)) { - if (this.node instanceof ZonSyntaxError || this.node instanceof ZonIdentifier) { - this.addCompletionsFromArray(topLevelKeys); - return; - } - } - } - if (Is.topLevelObject(this.node)) { - this.addCompletionsFromArray(topLevelKeys); - return; - } - if (this.node instanceof ZonSyntaxError || this.node instanceof ZonIdentifier) { - if (this.node.parent instanceof ZonEntry) { - const materialsEntry = this.node.parent; - if (materialsEntry.parent instanceof ZonObject) { - const materialsObject = materialsEntry.parent; - if (materialsObject.parent instanceof ZonEntry) { - const itemEntry = materialsObject.parent; - if ( - (itemEntry.key instanceof ZonIdentifier || - itemEntry.key instanceof ZonSyntaxError) && - itemEntry.key.value === "material" - ) { - this.addCompletionsFromArray(materialKeys); - return; - } - } - } - } - } + new ResolveCompletion(this).any(this.nodePath.path, { + type: "object", + completions: () => ({ + material: { + type: "object", + completions: () => ({ + durability: { + type: "number", + completions: () => {}, + }, + massDamage: { + type: "number", + completions: () => {}, + }, + hardnessDamage: { + type: "number", + completions: () => {}, + }, + swingSpeed: { + type: "number", + completions: () => {}, + }, + textureRoughness: { + type: "number", + completions: () => {}, + }, + colors: { + type: "array", + completions: () => ({ type: "color" }), + }, + modifiers: { + type: "array", + completions: () => ({ type: "number" }), + }, + }), + }, + texture: { + type: "string", + completions: this.getItemTextureCompletionCallback(), + }, + }), + }); + + return; } async onTool(_asset: Tool): Promise {} async onBiome(_asset: Biome): Promise {} diff --git a/server/src/server.ts b/server/src/server.ts index aa31aa4..0719ee6 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -111,32 +111,36 @@ connection.onCompletion(async (params: CompletionParams) => { console.log(ast); const position = new zon.Location(params.position.character, params.position.line); - const node = new zon.FindZonNode().find(ast, position); - console.log(node); + const nodeFinder = new zon.FindZonNode(); + const result = nodeFinder.find(ast, position); + if (result === null) return []; - if (node === null) return []; - if (!(node instanceof zon.ZonSyntaxError) && !(node instanceof zon.ZonString)) return []; + const [node, nodePath] = result; + console.log(node); + console.log(nodePath); - const visitor = new CompletionVisitor(params, ast, node); + const visitor = new CompletionVisitor(params, ast, node, nodePath); + console.log(`Collecting completions for scope ${scope}`); switch (scope) { case "blocks": - new Block("", relativePath).visit(visitor); + new Block("", "").visit(visitor); break; case "items": - new Item("", relativePath).visit(visitor); + new Item("", "").visit(visitor); break; case "tools": - new Tool("", relativePath).visit(visitor); + new Tool("", "").visit(visitor); break; case "biomes": - new Biome("", relativePath).visit(visitor); + new Biome("", "").visit(visitor); break; case "sbb": - new SBB("", relativePath).visit(visitor); + new SBB("", "").visit(visitor); break; } + console.log(`Finished collecting completions: ${visitor.completions.length} items found.`); return visitor.completions; }); diff --git a/server/src/utils.ts b/server/src/utils.ts new file mode 100644 index 0000000..5d37e19 --- /dev/null +++ b/server/src/utils.ts @@ -0,0 +1,19 @@ +export enum ZipMode { + truncate, +} +export class Pair { + a: T; + b: U; + constructor(a: T, b: U) { + this.a = a; + this.b = b; + } +} +export function zip(a: T[], b: U[], mode: ZipMode = ZipMode.truncate): Pair[] { + const length = mode === ZipMode.truncate ? Math.min(a.length, b.length) : a.length; + const result: Pair[] = []; + for (let i = 0; i < length; i++) { + result.push(new Pair(a[i], b[i])); + } + return result; +} diff --git a/server/src/zon.ts b/server/src/zon.ts index 39e2095..5c398c1 100644 --- a/server/src/zon.ts +++ b/server/src/zon.ts @@ -88,8 +88,45 @@ export class ZonNode { this.end.equals(other.end) ); } + isZonEmpty(): boolean { + return false; + } + isZonArray(): boolean { + return false; + } + isZonEntry(): boolean { + return false; + } + isZonObject(): boolean { + return false; + } + isZonString(): boolean { + return false; + } + isZonIdentifier(): boolean { + return false; + } + isZonNumber(): boolean { + return false; + } + isZonBoolean(): boolean { + return false; + } + isZonNull(): boolean { + return false; + } + isZonSyntaxError(): boolean { + return false; + } + isZonEnd(): boolean { + return false; + } + getValueString(): string | null { + return null; + } } +/// Represents an empty object / array where syntax doesn't allow for differentiating between them. export class ZonEmpty extends ZonNode { constructor(start: Location, end: Location) { super(start, end); @@ -100,6 +137,9 @@ export class ZonEmpty extends ZonNode { walk(walker: ZonWalker): void { walker.on_empty(this); } + isZonEmpty(): boolean { + return true; + } } export class ZonArray extends ZonNode { @@ -125,8 +165,11 @@ export class ZonArray extends ZonNode { walk(walker: ZonWalker): void { walker.on_array(this); } + isZonArray(): boolean { + return true; + } } - +/// Represents a key-value pair from an object. export class ZonEntry extends ZonNode { key: ZonNode; value: ZonNode; @@ -144,6 +187,9 @@ export class ZonEntry extends ZonNode { walk(walker: ZonWalker): void { walker.on_entry(this); } + isZonEntry(): boolean { + return true; + } } export class ZonObject extends ZonNode { @@ -170,6 +216,12 @@ export class ZonObject extends ZonNode { walk(walker: ZonWalker): void { walker.on_object(this); } + isZonObject(): boolean { + return true; + } + getKeyStrings(): string[] { + return this.items.map((x) => x.getValueString()).filter((x) => x != null); + } } export class ZonString extends ZonNode { @@ -185,6 +237,12 @@ export class ZonString extends ZonNode { walk(walker: ZonWalker): void { walker.on_string(this); } + isZonString(): boolean { + return true; + } + getValueString(): string | null { + return this.value; + } } export class ZonIdentifier extends ZonNode { @@ -204,6 +262,12 @@ export class ZonIdentifier extends ZonNode { walk(walker: ZonWalker): void { walker.on_identifier(this); } + isZonIdentifier(): boolean { + return true; + } + getValueString(): string | null { + return this.value; + } } export class ZonNumber extends ZonNode { @@ -219,6 +283,12 @@ export class ZonNumber extends ZonNode { walk(walker: ZonWalker): void { walker.on_number(this); } + isZonNumber(): boolean { + return true; + } + getValueString(): string | null { + return this.value; + } } export class ZonBoolean extends ZonNode { value: boolean; @@ -233,6 +303,9 @@ export class ZonBoolean extends ZonNode { walk(walker: ZonWalker): void { walker.on_boolean(this); } + isZonBoolean(): boolean { + return true; + } } export class ZonNull extends ZonNode { constructor(start: Location, end: Location) { @@ -244,6 +317,9 @@ export class ZonNull extends ZonNode { walk(walker: ZonWalker): void { walker.on_null(this); } + isZonNull(): boolean { + return true; + } } export class ZonSyntaxError extends ZonNode { value: string; @@ -260,6 +336,15 @@ export class ZonSyntaxError extends ZonNode { walk(walker: ZonWalker): void { walker.on_syntax_error(this); } + isZonSyntaxError(): boolean { + return true; + } + getValueString(): string | null { + let value = this.value.trim(); + if (value.startsWith(".")) return value.substring(1); + value = value.replace(/,$/, ""); + return value; + } } export class ZonEnd extends ZonNode { constructor(start: Location, end: Location) { @@ -271,14 +356,21 @@ export class ZonEnd extends ZonNode { walk(walker: ZonWalker): void { walker.on_end(this); } + isZonEnd(): boolean { + return true; + } } export class ZonWalker { + path: string[] = []; + on_empty(_node: ZonEmpty): void {} on_array(_node: ZonArray): void { - for (const item of _node.items) { - item.walk(this); - } + _node.items.map((value, index) => { + this.path.push(`${index}`); + value.walk(this); + this.path.pop(); + }); } on_object(node: ZonObject): void { for (const entry of node.items) { @@ -286,6 +378,7 @@ export class ZonWalker { } } on_entry(node: ZonEntry): void { + this.path.push((node.key as ZonIdentifier).value); node.key.walk(this); node.value.walk(this); } @@ -298,83 +391,137 @@ export class ZonWalker { on_end(_node: ZonEnd): void {} } +export enum LengthRule { + exact, + /// Path must be at least as long as the pattern, but can be longer. + atLeast, + /// Path must be at most as long as the pattern, but can be shorter. + atMost, +} + +export class ZonNodePath { + path: ZonNode[] = []; + + constructor(path: ZonNode[]) { + this.path = path; + } + + match( + conditions: ((x: ZonNode) => boolean)[], + length: LengthRule | undefined = LengthRule.exact, + ): boolean { + if (length === undefined) { + length = LengthRule.exact; + } + switch (length) { + case LengthRule.exact: + if (this.path.length !== conditions.length) return false; + break; + case LengthRule.atLeast: + if (this.path.length < conditions.length) return false; + break; + case LengthRule.atMost: + if (this.path.length > conditions.length) return false; + break; + } + + return this.path.every((v, i) => conditions[i](v)); + } +} + export class FindZonNode extends ZonWalker { position: Location; match: ZonNode | null = null; + match_path: ZonNode[] = []; constructor() { super(); this.position = new Location(0, 0); this.match = null; } - find(node: ZonNode, position: Location): ZonNode | null { + find(node: ZonNode, position: Location): [ZonNode, ZonNodePath] | null { this.position = position; this.match = null; + this.match_path = []; node.walk(this); const match = this.match; + const match_path = this.match_path; this.position = new Location(0, 0); this.match = null; + this.match_path = []; - return match; + if (match === null) return null; + return [match, new ZonNodePath(match_path)]; } - on_empty(_node: ZonEmpty): void { - if (this.position.greaterOrEqual(_node.start) && this.position.lessOrEqual(_node.end)) { - this.match = _node; + on_empty(node: ZonEmpty): void { + if (this.position.greaterOrEqual(node.start) && this.position.lessOrEqual(node.end)) { + this.match = node; + this.match_path.push(node); } } on_array(node: ZonArray): void { if (this.position.greaterOrEqual(node.start) && this.position.lessOrEqual(node.end)) { this.match = node; + this.match_path.push(node); super.on_array(node); } } on_object(node: ZonObject): void { if (this.position.greaterOrEqual(node.start) && this.position.lessOrEqual(node.end)) { this.match = node; + this.match_path.push(node); super.on_object(node); } } on_entry(node: ZonEntry): void { if (this.position.greaterOrEqual(node.start) && this.position.lessOrEqual(node.end)) { this.match = node; + this.match_path.push(node); super.on_entry(node); } } on_string(node: ZonString): void { if (this.position.greaterOrEqual(node.start) && this.position.lessOrEqual(node.end)) { this.match = node; + this.match_path.push(node); } } on_identifier(node: ZonIdentifier): void { if (this.position.greaterOrEqual(node.start) && this.position.lessOrEqual(node.end)) { this.match = node; + this.match_path.push(node); } } on_number(node: ZonNumber): void { if (this.position.greaterOrEqual(node.start) && this.position.lessOrEqual(node.end)) { this.match = node; + this.match_path.push(node); } } on_boolean(node: ZonBoolean): void { if (this.position.greaterOrEqual(node.start) && this.position.lessOrEqual(node.end)) { this.match = node; + this.match_path.push(node); } } on_null(node: ZonNull): void { if (this.position.greaterOrEqual(node.start) && this.position.lessOrEqual(node.end)) { this.match = node; + this.match_path.push(node); } } on_syntax_error(node: ZonSyntaxError): void { if (this.position.greaterOrEqual(node.start) && this.position.lessOrEqual(node.end)) { this.match = node; + this.match_path.push(node); } } on_end(node: ZonEnd): void { if (this.position.greaterOrEqual(node.start) && this.position.lessOrEqual(node.end)) { this.match = node; + this.match_path.push(node); } } } @@ -383,16 +530,19 @@ export class Parser { location: Location; source: string; rest: string; + current_path: string[]; constructor() { this.location = new Location(0, 0); this.source = ""; this.rest = ""; + this.current_path = []; } public parse(source: string): ZonNode { this.location = new Location(0, 0); this.source = source; this.rest = source; + this.current_path = []; return this.parseNode(); } @@ -411,10 +561,10 @@ export class Parser { ); } private syntaxError(): ZonNode { - const m = this.rest.match(/.*?(\n\r|\n|$)/); + const m = this.rest.match(/(.*?)\r\n|\n|$/); if (!m) throw new Error("Reached unreachable code"); const start = this.location.clone(); - const value = this.rest.slice(0, m[0].length); + const value = this.rest.slice(0, m[1].length); this.location.advance(m[0]); this.rest = this.rest.slice(m[0].length); @@ -490,16 +640,17 @@ export class Parser { this.skipWhitespace(); this.matchAdvance(/^=/); + const locationAfterEquals = this.location.clone(); this.skipWhitespace(); if (key instanceof ZonSyntaxError || this.rest.length === 0 || this.peek(/^}/)) { items.push( new ZonEntry( key, - new ZonSyntaxError("", this.location.clone(), this.location.clone()), + new ZonSyntaxError("", locationAfterEquals, this.location.clone()), key.start.clone(), - this.location.clone() - ) + this.location.clone(), + ), ); continue; } @@ -573,7 +724,11 @@ export class Parser { break; } if (this.rest[count] === "\\") escaped = !escaped; - if ((this.rest[count] === quote && !escaped) || this.rest[count] === "\n") { + if ( + (this.rest[count] === quote && !escaped) || + this.rest[count] === "\n" || + this.rest[count] === "\r" + ) { string = this.rest.slice(0, count); this.location.advance(string); this.location.advanceColumn(); @@ -607,8 +762,23 @@ export class Parser { return new ZonBoolean(m[1] === "true", start, this.location.clone()); } public parseNumber(): ZonNode | undefined { - const m = this.rest.match(/^([+-]?\d(\.\d)?|0x[0-9a-fA-F]+)/); - if (!m) return undefined; + { + const m = this.rest.match(/^0x[a-fA-F0-9]+/); + if (m) return this.finishNumber(m); + } + { + const m = this.rest.match( + /^-?(([0-9]+\.)|(\.[0-9]+)|([0-9]+\.[0-9]+))(?:[eE][+-]?[1-9]+)?/, + ); + if (m) return this.finishNumber(m); + } + { + const m = this.rest.match(/^-?[0-9]+/); + if (m) return this.finishNumber(m); + } + return undefined; + } + finishNumber(m: RegExpMatchArray): ZonNode { const start = this.location.clone(); this.location.advance(m[0]); @@ -617,33 +787,3 @@ export class Parser { return new ZonNumber(m[0], start, this.location.clone()); } } - -export class Is { - static childOfEntry(node: ZonNode): boolean { - return node.parent instanceof ZonEntry; - } - static entryKeyEqual(node: ZonNode, key: string): boolean { - if (!(node.parent instanceof ZonEntry)) return false; - if (!(node.parent.key instanceof ZonIdentifier)) return false; - return node.parent.key.value === key; - } - static entryKeyMatch(node: ZonNode, key: RegExp): boolean { - if (!(node.parent instanceof ZonEntry)) return false; - if (!(node.parent.key instanceof ZonIdentifier)) return false; - return node.parent.key.value.match(key) !== null; - } - static childOfTopLevelObject(node: ZonNode): boolean { - let objectVar = null; - if (node instanceof ZonEntry) { - objectVar = node.parent; - } else if (node.parent instanceof ZonEntry) { - objectVar = node.parent.parent; - } else { - return false; - } - return objectVar instanceof ZonObject && objectVar.parent === null; - } - static topLevelObject(node: ZonNode): boolean { - return node instanceof ZonObject && node.parent === null; - } -}