import * as core from '@spyglassmc/core' import type { JsonNode } from '@spyglassmc/json' import * as json from '@spyglassmc/json' import { JsonArrayNode, JsonBooleanNode, JsonNumberNode, JsonObjectNode, JsonStringNode } from '@spyglassmc/json' import type { ListType, LiteralType, McdocType, NumericType, PrimitiveArrayType, StringType, TupleType, UnionType } from '@spyglassmc/mcdoc' import { TypeDefSymbolData } from '@spyglassmc/mcdoc/lib/binder/index.js' import type { McdocCheckerContext, SimplifiedEnum, SimplifiedMcdocType, SimplifiedMcdocTypeNoUnion, SimplifiedStructType, SimplifyValueNode } from '@spyglassmc/mcdoc/lib/runtime/checker/index.js' import { simplify } from '@spyglassmc/mcdoc/lib/runtime/checker/index.js' import { getValues } from '@spyglassmc/mcdoc/lib/runtime/completer/index.js' import { useCallback, useMemo } from 'preact/hooks' import { useLocale } from '../../contexts/Locale.jsx' import { hexId } from '../../Utils.js' import { Octicon } from '../Octicon.jsx' export interface McdocContext extends core.CheckerContext {} type MakeEdit = (edit: (range: core.Range) => JsonNode | undefined) => void interface Props { node: JsonNode | undefined makeEdit: MakeEdit ctx: McdocContext } export function McdocRoot({ node, makeEdit, ctx } : Props) { const type = node?.typeDef ?? { kind: 'unsafe' } if (type.kind === 'struct') { return } return <>
} interface HeadProps extends Props { type: SimplifiedMcdocType optional?: boolean } function Head({ type, optional, node, makeEdit, ctx }: HeadProps) { if (type.kind === 'string') { return } if (type.kind === 'enum') { return } if (type.kind === 'byte' || type.kind === 'short' || type.kind === 'int' || type.kind === 'long' || type.kind === 'float' || type.kind === 'double') { return } if (type.kind === 'boolean') { return } if (type.kind === 'union') { return } if (type.kind === 'struct') { return } if (type.kind === 'list' || type.kind === 'byte_array' || type.kind === 'int_array' || type.kind === 'long_array') { return } if (type.kind === 'tuple') { return <> } if (type.kind === 'literal') { return } return <> } interface StringHeadProps extends Props { type: StringType } function StringHead({ type, node, makeEdit, ctx }: StringHeadProps) { const value = JsonStringNode.is(node) ? node.value : undefined const onChangeValue = useCallback((newValue: string) => { if (value === newValue) { return } makeEdit((range) => { if (newValue.length === 0) { return undefined } return { type: 'json:string', range, options: json.parser.JsonStringOptions, value: newValue, valueMap: [{ inner: core.Range.create(0), outer: core.Range.create(range.start) }], } }) }, [node, makeEdit]) const completions = useMemo(() => { return getValues(type, { ...ctx, offset: node?.range.start ?? 0 }) .filter(c => c.kind === 'string') }, [type, node, ctx]) const datalistId = `mcdoc_completions_${hexId()}` return <> {completions.length > 0 && {completions.map(c => )} } onChangeValue((e.target as HTMLInputElement).value)} list={completions.length > 0 ? datalistId : undefined} /> } interface EnumHeadProps extends HeadProps { type: SimplifiedEnum, } function EnumHead({ type, optional, node, makeEdit }: EnumHeadProps) { const value = JsonStringNode.is(node) ? node.value : undefined const onChangeValue = useCallback((newValue: string) => { if (value === newValue) { return } makeEdit((range) => { if (newValue === '__unset__') { return undefined } if (type.enumKind === 'string') { return { type: 'json:string', range, options: json.parser.JsonStringOptions, value: newValue, valueMap: [{ inner: core.Range.create(0), outer: core.Range.create(range.start) }], } } const number: core.FloatNode = { type: 'float', range, value: parseFloat(newValue), } const result: JsonNumberNode = { type: 'json:number', range, children: [number], value: number, } number.parent = result return result }) }, [type.enumKind, value, makeEdit]) return } interface NumericHeadProps extends Props { type: NumericType, } function NumericHead({ type, node, makeEdit }: NumericHeadProps) { const value = node && JsonNumberNode.is(node) ? Number(node.value.value) : undefined const isFloat = type.kind === 'float' || type.kind === 'double' const onChangeValue = useCallback((value: string) => { const number = value.length === 0 ? undefined : Number(value) if (number !== undefined && Number.isNaN(number)) { return } makeEdit((range) => { if (number === undefined) { return undefined } const newValue: core.FloatNode | core.LongNode = isFloat ? { type: 'float', range, value: number } : { type: 'long', range, value: BigInt(number) } const newNode: JsonNumberNode = { type: 'json:number', range, value: newValue, children: [newValue], } newValue.parent = newNode return newNode }) }, [isFloat, node, makeEdit]) return onChangeValue((e.target as HTMLInputElement).value)} /> } function BooleanHead({ node, makeEdit }: Props) { const value = node && JsonBooleanNode.is(node) ? node.value : undefined const onSelect = useCallback((newValue: boolean) => { makeEdit((range) => { if (value === newValue) { return undefined } return { type: 'json:boolean', range, value: newValue, } }) }, [node, makeEdit, value]) return <> } interface UnionHeadProps extends HeadProps { type: UnionType } function UnionHead({ type, optional, node, makeEdit, ctx }: UnionHeadProps) { const selectedType = findSelectedMember(type, node) const onSelect = useCallback((newValue: string) => { makeEdit((range) => { if (newValue === '__unset__') { return undefined } const newSelected = type.members[parseInt(newValue)] return getDefault(newSelected, range, ctx) }) }, [type, makeEdit, ctx]) const memberIndex = type.members.findIndex(m => quickEqualTypes(m, selectedType)) return <> {(selectedType || !optional) && } } function StructHead({ type, optional, node, makeEdit, ctx }: HeadProps) { const { locale } = useLocale() const onRemove = useCallback(() => { makeEdit(() => { return undefined }) }, [makeEdit]) const onSetDefault = useCallback(() => { makeEdit((range) => { return getDefault(type, range, ctx) }) }, [type, ctx]) if (optional) { if (node && JsonObjectNode.is(node)) { return } else { return } } else { if (!node || !JsonObjectNode.is(node)) { return } return <> } } interface ListHeadProps extends Props { type: ListType | PrimitiveArrayType, } function ListHead({ type, node, makeEdit, ctx }: ListHeadProps) { const { locale } = useLocale() const fixedRange = type.lengthRange?.min !== undefined && type.lengthRange.min === type.lengthRange.max if (fixedRange) { return <> } const canAdd = (type.lengthRange?.max ?? Infinity) > (node?.children?.length ?? 0) const onAdd = useCallback(() => { if (canAdd) { makeEdit((range) => { const itemType = simplifyType(getItemType(type), ctx) const newValue = getDefault(itemType, range, ctx) const newItem: core.ItemNode = { type: 'item', range, children: [newValue], value: newValue, } newValue.parent = newItem if (JsonArrayNode.is(node)) { node.children.push(newItem) newItem.parent = node return node } const newArray: JsonArrayNode = { type: 'json:array', range, children: [newItem], } newItem.parent = newArray return newArray }) } }, [type, node, makeEdit, ctx, canAdd]) return } interface LiteralHeadProps extends HeadProps { type: LiteralType } function LiteralHead({ type }: LiteralHeadProps) { return } interface BodyProps extends Props { type: SimplifiedMcdocType optional?: boolean } function Body({ type, optional, node, makeEdit, ctx }: BodyProps) { if (type.kind === 'union') { return } if (type.kind === 'struct') { if (type.fields.length === 0) { return <> } if (optional && !node) { return <> } return
} if (type.kind === 'list' || type.kind === 'byte_array' || type.kind === 'int_array' || type.kind === 'long_array') { const fixedRange = type.lengthRange?.min !== undefined && type.lengthRange.min === type.lengthRange.max if (!fixedRange && (!node || node.children?.length === 0)) { return <> } return
} if (type.kind === 'tuple') { return
} if (type.kind === 'boolean' || type.kind === 'byte' || type.kind === 'short' || type.kind === 'int' || type.kind === 'float' || type.kind === 'double') { return <> } // console.warn('Unhandled body', type, node) return <> } interface UnionBodyProps extends BodyProps { type: UnionType } function UnionBody({ type, optional, node, makeEdit, ctx }: UnionBodyProps) { const selectedType = findSelectedMember(type, node) return } interface StructBodyProps extends Props { type: SimplifiedStructType } function StructBody({ type: outerType, node, makeEdit, ctx }: StructBodyProps) { if (!JsonObjectNode.is(node)) { return <> } const type = node.typeDef?.kind === 'struct' ? node.typeDef : outerType const staticFields = type.fields.filter(field => field.key.kind === 'literal' && field.key.value.kind === 'string') const dynamicFields = type.fields.filter(field => field.key.kind === 'string') if (type.fields.length !== staticFields.length + dynamicFields.length) { // console.warn('Missed struct fields', type.fields.filter(field => // !staticFields.includes(field) && !dynamicFields.includes(field))) } return <> {staticFields.map(field => { const key = (field.key as LiteralType).value.value.toString() const childIndex = node.children.findIndex(p => p.key?.value === key) const child = childIndex === -1 ? undefined : node.children[childIndex] const childValue = child?.value const fieldType = simplifyType(field.type, ctx) const makeFieldEdit: MakeEdit = (edit) => { if (child) { makeEdit(() => { const newChild = edit(childValue?.range ?? core.Range.create(child.range.end)) if (newChild === undefined) { node.children.splice(childIndex, 1) } else { node.children[childIndex] = { type: 'pair', range: child.range, key: child.key, value: newChild, } } return node }) } else { const newChild = edit(core.Range.create(node.range.end)) if (newChild) { makeEdit(() => { node.children.push({ type: 'pair', range: newChild.range, key: { type: 'json:string', range: newChild.range, options: json.parser.JsonStringOptions, value: key, valueMap: [{ inner: core.Range.create(0), outer: newChild.range }], }, value: newChild, }) return node }) } } } return
})} } function Key({ label }: { label: string | number | boolean }) { const formatted = label.toString().replaceAll('_', ' ') const captizalized = formatted.charAt(0).toUpperCase() + formatted.substring(1) return } interface ListBodyProps extends Props { type: ListType | PrimitiveArrayType } function ListBody({ type: outerType, node, makeEdit, ctx }: ListBodyProps) { const { locale } = useLocale() if (!JsonArrayNode.is(node)) { return <> } const type = (node.typeDef?.kind === 'list' || node.typeDef?.kind === 'byte_array' || node.typeDef?.kind === 'int_array' || node.typeDef?.kind === 'long_array') ? node.typeDef : outerType const onRemoveItem = useCallback((index: number) => { makeEdit(() => { node.children.splice(index, 1) return node }) }, [makeEdit, node]) return <> {node.children.map((item, index) => { const child = item.value const itemType = simplifyType(getItemType(type), ctx) const makeItemEdit: MakeEdit = (edit) => { makeEdit(() => { const newChild = edit(child?.range ?? item.range) node.children[index] = { type: 'item', range: item.range, value: newChild, } return node }) } return
{node.children.length > 1 &&
}
})} } interface TupleBodyProps extends BodyProps{ type: TupleType } function TupleBody({ type, node, makeEdit, ctx }: TupleBodyProps) { if (!JsonArrayNode.is(node)) { return <> } return <> {type.items.map((item, index) => { const child = node?.children?.[index]?.value const itemType = simplifyType(item, ctx) const makeItemEdit: MakeEdit = (edit) => { makeEdit(() => { const newChild = edit(child?.range ?? node.range) if (newChild === undefined) { return node } node.children[index] = { type: 'item', range: newChild.range, value: newChild, } return node }) } return
})} } function getDefault(type: McdocType, range: core.Range, ctx: McdocContext): JsonNode { if (type.kind === 'string') { return JsonStringNode.mock(range) } if (type.kind === 'boolean') { return { type: 'json:boolean', range, value: false } } if (type.kind === 'byte' || type.kind === 'short' || type.kind === 'int' || type.kind === 'long' || type.kind === 'float' || type.kind === 'double') { const value: core.LongNode = { type: 'long', range, value: BigInt(0) } return { type: 'json:number', range, value, children: [value] } } if (type.kind === 'struct' || type.kind === 'any' || type.kind === 'unsafe') { const object = JsonObjectNode.mock(range) if (type.kind === 'struct') { for (const field of type.fields) { if (field.kind === 'pair' && !field.optional && typeof field.key === 'string') { const key: JsonStringNode = { type: 'json:string', range, options: json.parser.JsonStringOptions, value: field.key, valueMap: [{ inner: core.Range.create(0), outer: core.Range.create(range.start) }] } const value = getDefault(field.type, range, ctx) const pair: core.PairNode = { type: 'pair', range, key: key, value: value, children: [key, value], } key.parent = pair value.parent = pair object.children.push(pair) pair.parent = object } } } return object } if (type.kind === 'list' || type.kind === 'byte_array' || type.kind === 'int_array' || type.kind === 'long_array') { const array = JsonArrayNode.mock(range) const minLength = type.lengthRange?.min ?? 0 if (minLength > 0) { for (let i = 0; i < minLength; i += 1) { const child = getDefault(getItemType(type), range, ctx) const itemNode: core.ItemNode = { type: 'item', range, children: [child], value: child, } child.parent = itemNode array.children.push(itemNode) itemNode.parent = array } } return array } if (type.kind === 'tuple') { return { type: 'json:array', range, children: type.items.map(item => { const valueNode = getDefault(item, range, ctx) const itemNode: core.ItemNode = { type: 'item', range, children: [valueNode], value: valueNode, } valueNode.parent = itemNode return itemNode }), } } if (type.kind === 'union') { return getDefault(type.members[0], range, ctx) } if (type.kind === 'enum') { return getDefault({ kind: 'literal', value: { kind: type.enumKind ?? 'string', value: type.values[0].value } as any }, range, ctx) } if (type.kind === 'literal') { if (type.value.kind === 'string') { return { type: 'json:string', range, options: json.parser.JsonStringOptions, value: type.value.value, valueMap: [{ inner: core.Range.create(0), outer: core.Range.create(range.start) }] } } if (type.value.kind === 'boolean') { return { type: 'json:boolean', range, value: type.value.value } } const value: core.FloatNode | core.LongNode = type.value.kind === 'float' || type.value.kind === 'double' ? { type: 'float', range, value: type.value.value } : { type: 'long', range, value: BigInt(type.value.value) } return { type: 'json:number', range, value, children: [value] } } if (type.kind === 'reference') { if (!type.path) { return { type: 'json:null', range } } const symbol = ctx.symbols.query(ctx.doc, 'mcdoc', type.path) const def = symbol.getData(TypeDefSymbolData.is)?.typeDef if (!def) { return { type: 'json:null', range } } if (type.attributes?.length) { return getDefault({ ...def, attributes: [...type.attributes, ...def.attributes ?? []], }, range, ctx) } return getDefault(def, range, ctx) } return { type: 'json:null', range } } function simplifyType(type: McdocType, ctx: McdocContext): SimplifiedMcdocType { const node: SimplifyValueNode = { entryNode: { parent: undefined, runtimeKey: undefined, }, node: { originalNode: null, inferredType: { kind: 'any' }, }, } const context: McdocCheckerContext = { ...ctx, allowMissingKeys: false, requireCanonical: false, isEquivalent: () => false, getChildren: () => [], reportError: () => {}, attachTypeInfo: () => {}, nodeAttacher: () => {}, stringAttacher: () => {}, } const result = simplify(type, { node, ctx: context }) return result.typeDef } function getItemType(type: ListType | PrimitiveArrayType): McdocType { return type.kind === 'list' ? type.item : type.kind === 'byte_array' ? { kind: 'byte' } : type.kind === 'int_array' ? { kind: 'int' } : type.kind === 'long_array' ? { kind: 'long' } : { kind: 'any' } } function quickEqualTypes(a: SimplifiedMcdocType, b: SimplifiedMcdocType) { if (a.kind !== b.kind) { return false } // TODO: improve this return true } function findSelectedMember(union: UnionType, node: JsonNode | undefined) { const selectedType = node?.typeDef if (!selectedType) { return union.members[0] } if (selectedType.kind === 'union') { // The node technically matches all members of this union, // ideally the editor should show a combination of all members return selectedType.members[0] } return selectedType }