) {
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 canAdd = (type.lengthRange?.max ?? Infinity) > (node?.children?.length ?? 0)
const onRemoveItem = useCallback((index: number) => {
makeEdit(() => {
node.children.splice(index, 1)
return node
})
}, [makeEdit, node])
const onMoveUp = useCallback((index: number) => {
if (node.children.length <= 1 || index <= 0) {
return
}
makeEdit(() => {
const moved = node.children.splice(index, 1)
node.children.splice(index - 1, 0, ...moved)
return node
})
}, [makeEdit])
const onMoveDown = useCallback((index: number) => {
if (node.children.length <= 1 || index >= node.children.length - 1) {
return
}
makeEdit(() => {
const moved = node.children.splice(index, 1)
node.children.splice(index + 1, 0, ...moved)
return node
})
}, [makeEdit])
const onAddBottom = 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 <>
{node.children.map((item, index) => {
const child = item.value
const itemType = getItemType(type)
const childType = simplifyType(itemType, 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
})
}
const canMoveUp = node.children.length > 1 && index > 0
const canMoveDown = node.children.length > 1 && index < (node.children.length - 1)
return
{childType.kind === 'struct'
?
: }
})}
{node.children.length > 0 && }
>
}
function TupleBody({ type, node, makeEdit, ctx }: Props) {
if (!JsonArrayNode.is(node)) {
return <>>
}
return <>
{type.items.map((itemType, index) => {
const item = node?.children?.[index]
const child = item?.value
const childType = simplifyType(itemType, 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
})}
>
}
interface ErrorsProps {
type: SimplifiedMcdocType
node: JsonNode | undefined
ctx: McdocContext
}
function Errors({ type, node, ctx }: ErrorsProps) {
const errors = useMemo(() => {
if (node === undefined) {
return []
}
const errors = ctx.err.errors
// Get all errors inside the current node
.filter(e => core.Range.containsRange(node.range, e.range, true))
// Unless they are inside a child node
.filter(e => !node.children?.some(c => (c.type === 'item' || c.type === 'pair') && core.Range.containsRange(c.range, e.range, true)))
// Filter out "Missing key" errors
.filter(e => !(core.Range.length(e.range) === 1 && (type.kind === 'struct' || (type.kind === 'union' && (findSelectedMember(type, node) ?? type.members[0]).kind === 'struct'))))
// Hide warnings if there are errors
return errors.find(e => e.severity === 3)
? errors.filter(e => e.severity === 3)
: errors
}, [type, node, ctx])
return <>
{errors.map(e => )}
>
}
interface ErrorIndicatorProps {
error: core.LanguageError
}
function ErrorIndicator({ error }: ErrorIndicatorProps) {
const [active, setActive] = useFocus()
return setActive()}>
{Octicon.issue_opened}
}
interface DocsProps {
desc: string | undefined
}
function Docs({ desc }: DocsProps) {
if (!desc || desc.length === 0) {
return <>>
}
const [active, setActive] = useFocus()
return setActive()}>
{Octicon.info}
}
function getDefault(type: SimplifiedMcdocType, 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' || field.key.kind === 'literal')) {
const key: JsonStringNode = {
type: 'json:string',
range,
options: json.parser.JsonStringOptions,
value: typeof field.key === 'string' ? field.key : field.key.value.value.toString(),
valueMap: [{ inner: core.Range.create(0), outer: core.Range.create(range.start) }],
}
const value = getDefault(simplifyType(field.type, ctx), 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(simplifyType(getItemType(type), ctx), 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(simplifyType(item, ctx), 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] }
}
return { type: 'json:null', range }
}
function formatIdentifier(id: string): string {
if (id.startsWith('!')) {
return '! ' + formatIdentifier(id.substring(1))
}
const text = id.replace(/^minecraft:/, '').replaceAll('_', ' ')
return text.charAt(0).toUpperCase() + text.substring(1)
}
function getCategory(type: McdocType) {
if (type.kind === 'reference' && type.path) {
switch (type.path) {
case '::java::data::loot::LootPool':
case '::java::data::worldgen::dimension::Dimension':
case '::java::data::worldgen::surface_rule::SurfaceRule':
case '::java::data::worldgen::template_pool::WeightedElement':
return 'pool'
case '::java::data::loot::LootCondition':
case '::java::data::advancement::AdvancementCriterion':
case '::java::data::worldgen::dimension::biome_source::BiomeSource':
case '::java::data::worldgen::processor_list::ProcessorRule':
case '::java::data::worldgen::feature::placement::PlacementModifier':
return 'predicate'
case '::java::data::loot::LootFunction':
case '::java::data::worldgen::density_function::CubicSpline':
case '::java::data::worldgen::processor_list::Processor':
return 'function'
}
}
return undefined
}
const selectRegistries = new Set([
'block_predicate_type',
'chunk_status',
'consume_effect_type',
'creative_mode_tab',
'data_component_type',
'enchantment_effect_component_type',
'enchantment_entity_effect_type',
'enchantment_level_based_value_type',
'enchantment_location_based_effect_type',
'enchantment_provider_type',
'enchantment_value_effect_type',
'entity_sub_predicate_type',
'float_provider_type',
'frog_variant',
'height_provider_type',
'int_provider_type',
'item_sub_predicate_type',
'loot_condition_type',
'loot_function_type',
'loot_nbt_provider_type',
'loot_number_provider_type',
'loot_pool_entry_type',
'loot_score_provider_type',
'map_decoration_type',
'number_format_type',
'pos_rule_test',
'position_source_type',
'recipe_book_category',
'recipe_display',
'recipe_serializer',
'recipe_type',
'rule_block_entity_modifier',
'rule_test',
'slot_display',
'stat_type',
'trigger_type',
'worldgen/biome_source',
'worldgen/block_state_provider_type',
'worldgen/carver',
'worldgen/chunk_generator',
'worldgen/density_function_type',
'worldgen/feature',
'worldgen/feature_size_type',
'worldgen/foliage_placer_type',
'worldgen/material_condition',
'worldgen/material_rule',
'worldgen/placement_modifier_type',
'worldgen/pool_alias_binding',
'worldgen/root_placer_type',
'worldgen/structure_placement',
'worldgen/structure_pool_element',
'worldgen/structure_processor',
'worldgen/structure_type',
'worldgen/tree_decorator_type',
'worldgen/trunk_placer_type',
])
export function simplifyType(type: McdocType, ctx: McdocContext, node?: JsonNode): SimplifiedMcdocType {
const key = node?.parent && JsonPairNode.is(node.parent) && JsonStringNode.is(node.parent.key) ? node.parent.key : undefined
const simplifyNode: SimplifyValueNode = {
entryNode: {
parent: undefined,
runtimeKey: key ? {
originalNode: key,
inferredType: { kind: 'literal', value: { kind: 'string', value: key.value } },
} : 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: simplifyNode, 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 undefined
}
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
}