mirror of
https://github.com/misode/misode.github.io.git
synced 2026-04-23 07:10:41 +00:00
706 lines
22 KiB
TypeScript
706 lines
22 KiB
TypeScript
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 <StructBody type={type} node={node} makeEdit={makeEdit} ctx={ctx} />
|
|
}
|
|
|
|
return <>
|
|
<div class="node-header">
|
|
<Head type={type} node={node} makeEdit={makeEdit} ctx={ctx} />
|
|
</div>
|
|
<Body type={type} node={node} makeEdit={makeEdit} ctx={ctx} />
|
|
</>
|
|
}
|
|
|
|
interface HeadProps extends Props {
|
|
type: SimplifiedMcdocType
|
|
optional?: boolean
|
|
}
|
|
function Head({ type, optional, node, makeEdit, ctx }: HeadProps) {
|
|
if (type.kind === 'string') {
|
|
return <StringHead type={type} node={node} makeEdit={makeEdit} ctx={ctx} />
|
|
}
|
|
if (type.kind === 'enum') {
|
|
return <EnumHead type={type} optional={optional} node={node} makeEdit={makeEdit} ctx={ctx} />
|
|
}
|
|
if (type.kind === 'byte' || type.kind === 'short' || type.kind === 'int' || type.kind === 'long' || type.kind === 'float' || type.kind === 'double') {
|
|
return <NumericHead type={type} node={node} makeEdit={makeEdit} ctx={ctx} />
|
|
}
|
|
if (type.kind === 'boolean') {
|
|
return <BooleanHead node={node} makeEdit={makeEdit} ctx={ctx} />
|
|
}
|
|
if (type.kind === 'union') {
|
|
return <UnionHead type={type} optional={optional} node={node} makeEdit={makeEdit} ctx={ctx} />
|
|
}
|
|
if (type.kind === 'struct') {
|
|
return <StructHead type={type} optional={optional} node={node} makeEdit={makeEdit} ctx={ctx} />
|
|
}
|
|
if (type.kind === 'list' || type.kind === 'byte_array' || type.kind === 'int_array' || type.kind === 'long_array') {
|
|
return <ListHead type={type} node={node} makeEdit={makeEdit} ctx={ctx} />
|
|
}
|
|
if (type.kind === 'tuple') {
|
|
return <></>
|
|
}
|
|
if (type.kind === 'literal') {
|
|
return <LiteralHead type={type} node={node} makeEdit={makeEdit} ctx={ctx} />
|
|
}
|
|
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 && <datalist id={datalistId}>
|
|
{completions.map(c => <option>{c.value}</option>)}
|
|
</datalist>}
|
|
<input value={value} onInput={(e) => 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 <select value={value} onInput={(e) => onChangeValue((e.target as HTMLSelectElement).value)}>
|
|
{(value === undefined || optional) && <option value="__unset__">-- unset --</option>}
|
|
{type.values.map(value =>
|
|
<option value={value.value}>{value.value}</option>
|
|
)}
|
|
</select>
|
|
}
|
|
|
|
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 <input type="number" value={value} onInput={(e) => 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 <>
|
|
<button class={value === false ? 'selected' : ''} onClick={() => onSelect(false)}>False</button>
|
|
<button class={value === true ? 'selected' : ''} onClick={() => onSelect(true)}>True</button>
|
|
</>
|
|
}
|
|
|
|
interface UnionHeadProps extends HeadProps {
|
|
type: UnionType<SimplifiedMcdocTypeNoUnion>
|
|
}
|
|
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 <>
|
|
<select value={memberIndex > -1 ? memberIndex : '__unset__'} onInput={(e) => onSelect((e.target as HTMLSelectElement).value)}>
|
|
{optional && <option value="__unset__">-- unset --</option>}
|
|
{type.members.map((member, index) =>
|
|
<option value={index}>{member.kind}</option>
|
|
)}
|
|
</select>
|
|
{(selectedType || !optional) && <Head type={selectedType ?? type.members[0]} optional={optional} node={node} makeEdit={makeEdit} ctx={ctx} />}
|
|
</>
|
|
}
|
|
|
|
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 <button class="node-collapse open tooltipped tip-se" aria-label={locale('remove')} onClick={onRemove}>
|
|
{Octicon.trashcan}
|
|
</button>
|
|
} else {
|
|
return <button class="node-collapse closed tooltipped tip-se" aria-label={locale('expand')} onClick={onSetDefault}>
|
|
{Octicon.plus_circle}
|
|
</button>
|
|
}
|
|
} else {
|
|
if (!node || !JsonObjectNode.is(node)) {
|
|
return <button class="add tooltipped tip-se" aria-label={locale('reset')} onClick={onSetDefault}>{Octicon.history}</button>
|
|
}
|
|
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<JsonNode> = {
|
|
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 <button class="add tooltipped tip-se" aria-label={locale('add_top')} onClick={() => onAdd()} disabled={!canAdd}>
|
|
{Octicon.plus_circle}
|
|
</button>
|
|
}
|
|
|
|
interface LiteralHeadProps extends HeadProps {
|
|
type: LiteralType
|
|
}
|
|
function LiteralHead({ type }: LiteralHeadProps) {
|
|
return <input value={type.value.value.toString()} disabled />
|
|
}
|
|
|
|
interface BodyProps extends Props {
|
|
type: SimplifiedMcdocType
|
|
optional?: boolean
|
|
}
|
|
function Body({ type, optional, node, makeEdit, ctx }: BodyProps) {
|
|
if (type.kind === 'union') {
|
|
return <UnionBody type={type} optional={optional} node={node} makeEdit={makeEdit} ctx={ctx} />
|
|
}
|
|
if (type.kind === 'struct') {
|
|
if (type.fields.length === 0) {
|
|
return <></>
|
|
}
|
|
if (optional && !node) {
|
|
return <></>
|
|
}
|
|
return <div class="node-body">
|
|
<StructBody type={type} node={node} makeEdit={makeEdit} ctx={ctx} />
|
|
</div>
|
|
}
|
|
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 <div class="node-body">
|
|
<ListBody type={type} node={node} makeEdit={makeEdit} ctx={ctx} />
|
|
</div>
|
|
}
|
|
if (type.kind === 'tuple') {
|
|
return <div class="node-body">
|
|
<TupleBody type={type} node={node} makeEdit={makeEdit} ctx={ctx} />
|
|
</div>
|
|
}
|
|
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<SimplifiedMcdocTypeNoUnion>
|
|
}
|
|
function UnionBody({ type, optional, node, makeEdit, ctx }: UnionBodyProps) {
|
|
const selectedType = findSelectedMember(type, node)
|
|
return <Body type={selectedType} optional={optional} node={node} makeEdit={makeEdit} ctx={ctx} />
|
|
}
|
|
|
|
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 <div class="node">
|
|
<div class="node-header">
|
|
<Key label={key} />
|
|
<Head type={fieldType} node={childValue} optional={field.optional} makeEdit={makeFieldEdit} ctx={ctx} />
|
|
</div>
|
|
<Body type={fieldType} node={childValue} optional={field.optional} makeEdit={makeFieldEdit} ctx={ctx} />
|
|
</div>
|
|
})}
|
|
</>
|
|
}
|
|
|
|
function Key({ label }: { label: string | number | boolean }) {
|
|
const formatted = label.toString().replaceAll('_', ' ')
|
|
const captizalized = formatted.charAt(0).toUpperCase() + formatted.substring(1)
|
|
return <label>{captizalized}</label>
|
|
}
|
|
|
|
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 <div class="node">
|
|
<div class="node-header">
|
|
<button class="remove tooltipped tip-se" aria-label={locale('remove')} onClick={() => onRemoveItem(index)}>
|
|
{Octicon.trashcan}
|
|
</button>
|
|
{node.children.length > 1 && <div class="node-move">
|
|
<button class="move tooltipped tip-se" aria-label={locale('move_up')} disabled={index === 0}>
|
|
{Octicon.chevron_up}
|
|
</button>
|
|
<button class="move tooltipped tip-se" aria-label={locale('move_down')} disabled={index === node.children.length - 1}>
|
|
{Octicon.chevron_down}
|
|
</button>
|
|
</div>}
|
|
<Key label="entry" />
|
|
<Head type={itemType} node={child} makeEdit={makeItemEdit} ctx={ctx} />
|
|
</div>
|
|
<Body type={itemType} node={child} makeEdit={makeItemEdit} ctx={ctx} />
|
|
</div>
|
|
})}
|
|
</>
|
|
}
|
|
|
|
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 <div class="node">
|
|
<div class="node-header">
|
|
<Key label="entry" />
|
|
<Head type={itemType} node={child} makeEdit={makeItemEdit} ctx={ctx} />
|
|
</div>
|
|
<Body type={itemType} node={child} makeEdit={makeItemEdit} ctx={ctx} />
|
|
</div>
|
|
})}
|
|
</>
|
|
}
|
|
|
|
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<JsonStringNode, JsonNode> = {
|
|
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<JsonNode> = {
|
|
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<JsonNode> = {
|
|
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<any> = {
|
|
entryNode: {
|
|
parent: undefined,
|
|
runtimeKey: undefined,
|
|
},
|
|
node: {
|
|
originalNode: null,
|
|
inferredType: { kind: 'any' },
|
|
},
|
|
}
|
|
const context: McdocCheckerContext<any> = {
|
|
...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<SimplifiedMcdocTypeNoUnion>, 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
|
|
}
|