From d2487324691bbc9ce2c3f61d02575affb5a501f9 Mon Sep 17 00:00:00 2001 From: Misode Date: Thu, 17 Oct 2024 15:14:30 +0200 Subject: [PATCH] Basic mcdoc tree rendering --- .../components/generator/McdocRenderer.tsx | 182 ++++++++++++++++++ src/app/components/generator/Tree.tsx | 14 +- src/app/contexts/Spyglass.tsx | 8 +- src/app/services/Spyglass.ts | 11 +- src/styles/nodes.css | 20 +- 5 files changed, 214 insertions(+), 21 deletions(-) create mode 100644 src/app/components/generator/McdocRenderer.tsx diff --git a/src/app/components/generator/McdocRenderer.tsx b/src/app/components/generator/McdocRenderer.tsx new file mode 100644 index 00000000..9b572007 --- /dev/null +++ b/src/app/components/generator/McdocRenderer.tsx @@ -0,0 +1,182 @@ +import type { JsonNode } from '@spyglassmc/json' +import { JsonArrayNode, JsonBooleanNode, JsonNumberNode, JsonObjectNode, JsonStringNode } from '@spyglassmc/json' +import type { ListType, LiteralType, McdocType } from '@spyglassmc/mcdoc' +import type { SimplifiedStructType } from '@spyglassmc/mcdoc/lib/runtime/checker/index.js' +import { useLocale } from '../../contexts/Locale.jsx' +import { Octicon } from '../Octicon.jsx' + +interface Props { + node: JsonNode | undefined +} +export function McdocRoot({ node } : Props) { + const type = node?.typeDef ?? { kind: 'unsafe' } + + if (type.kind === 'struct') { + return + } + + return <> +
+ +
+ + +} + +interface HeadProps extends Props { + simpleType: McdocType + optional?: boolean +} +function Head({ simpleType, optional, node }: HeadProps) { + const { locale } = useLocale() + const type = node?.typeDef ?? simpleType + if (type.kind === 'string') { + const value = JsonStringNode.is(node) ? node.value : undefined + + return + } + if (type.kind === 'enum') { + const value = JsonStringNode.is(node) ? node.value : undefined + return + } + if (type.kind === 'byte' || type.kind === 'short' || type.kind === 'int' || type.kind === 'long' || type.kind === 'float' || type.kind === 'double') { + const value = node && JsonNumberNode.is(node) ? Number(node.value.value) : undefined + return + } + if (type.kind === 'boolean') { + const value = node && JsonBooleanNode.is(node) ? node.value : undefined + return <> + + + + } + if (type.kind === 'union') { + return + } + if (type.kind === 'struct' && optional) { + console.log(type, node) + if (node && JsonObjectNode.is(node)) { + return + } else { + 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) { + return <> + } + return + } + console.warn('Unhandled head', type) + return <> +} + +interface BodyProps extends Props { + simpleType: McdocType +} +function Body({ simpleType, node }: BodyProps) { + const type = node?.typeDef ?? simpleType + if (node?.typeDef?.kind === 'struct') { + if (node.typeDef.fields.length === 0) { + return <> + } + return
+ +
+ } + if (node?.typeDef?.kind === 'list') { + return
+ +
+ } + if (type.kind === 'byte' || type.kind === 'short' || type.kind === 'int' || type.kind === 'boolean') { + return <> + } + console.warn('Unhandled body', type, node) + return <> +} + +interface StructBodyProps extends Props { + type: SimplifiedStructType +} +function StructBody({ type, node }: StructBodyProps) { + if (!JsonObjectNode.is(node)) { + return <> + } + 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 + const child = node.children.find(p => p.key?.value === key)?.value + 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 +} +function ListBody({ type, node }: ListBodyProps) { + const { locale } = useLocale() + if (!JsonArrayNode.is(node)) { + return <> + } + return <> + {node.children.map((item, index) => { + const child = item.value + return
+
+ + {node.children.length > 1 &&
+ + +
} + + +
+ +
+ })} + +} diff --git a/src/app/components/generator/Tree.tsx b/src/app/components/generator/Tree.tsx index 01837026..32c5278e 100644 --- a/src/app/components/generator/Tree.tsx +++ b/src/app/components/generator/Tree.tsx @@ -1,22 +1,30 @@ import type { DocAndNode } from '@spyglassmc/core' +import { JsonFileNode } from '@spyglassmc/json' import { useErrorBoundary } from 'preact/hooks' import { useLocale } from '../../contexts/index.js' +import { useDocAndNode } from '../../contexts/Spyglass.jsx' +import { McdocRoot } from './McdocRenderer.jsx' type TreePanelProps = { docAndNode: DocAndNode, onError: (message: string) => unknown, } -export function Tree({ onError }: TreePanelProps) { +export function Tree({ docAndNode, onError }: TreePanelProps) { const { lang } = useLocale() if (lang === 'none') return <> + const fileChild = useDocAndNode(docAndNode).node.children[0] + if (!JsonFileNode.is(fileChild)) { + return <> + } + const [error] = useErrorBoundary(e => { onError(`Error rendering the tree: ${e.message}`) console.error(e) }) if (error) return <> - return
- {/* TODO: render tree */} + return
+
} diff --git a/src/app/contexts/Spyglass.tsx b/src/app/contexts/Spyglass.tsx index bfea67d6..9bc46cb5 100644 --- a/src/app/contexts/Spyglass.tsx +++ b/src/app/contexts/Spyglass.tsx @@ -34,11 +34,15 @@ export function watchSpyglassUri( }, [spyglass, uri, handler, ...inputs]) } -export function useDocAndNode(origina: DocAndNode, inputs?: Inputs): DocAndNode -export function useDocAndNode(origina: DocAndNode | undefined, inputs?: Inputs): DocAndNode | undefined +export function useDocAndNode(original: DocAndNode, inputs?: Inputs): DocAndNode +export function useDocAndNode(original: DocAndNode | undefined, inputs?: Inputs): DocAndNode | undefined export function useDocAndNode(original: DocAndNode | undefined, inputs: Inputs = []) { const [wrapped, setWrapped] = useState(original) + useEffect(() => { + setWrapped(original) + }, [original, setWrapped, ...inputs]) + watchSpyglassUri(original?.doc.uri, updated => { setWrapped(updated) }, [original?.doc.uri, setWrapped, ...inputs]) diff --git a/src/app/services/Spyglass.ts b/src/app/services/Spyglass.ts index f44efbbd..48fb6363 100644 --- a/src/app/services/Spyglass.ts +++ b/src/app/services/Spyglass.ts @@ -36,8 +36,12 @@ export class Spyglass { if (contents !== undefined) { await this.service.project.externals.fs.writeFile(uri, contents) } else { - const buffer = await this.service.project.externals.fs.readFile(uri) - contents = new TextDecoder().decode(buffer) + try { + const buffer = await this.service.project.externals.fs.readFile(uri) + contents = new TextDecoder().decode(buffer) + } catch (e) { + contents = '{}' + } } await this.service.project.onDidOpen(uri, 'json', 1, contents) const docAndNode = await this.service.project.ensureClientManagedChecked(uri) @@ -52,6 +56,9 @@ export class Spyglass { } public getUnsavedFileUri(gen: ConfigGenerator) { + if (gen.id === 'pack_mcmeta') { + return 'file:///project/pack.mcmeta' + } return `file:///project/data/draft/${genPath(gen, this.version.id)}/unsaved.json` } diff --git a/src/styles/nodes.css b/src/styles/nodes.css index 22035cd9..72ced6ac 100644 --- a/src/styles/nodes.css +++ b/src/styles/nodes.css @@ -344,26 +344,18 @@ button.move:disabled { /* Node body and list entry */ -.node { - margin-bottom: 4px; -} - -.node-body > .node:first-child { - margin-top: 4px; -} - -.node:last-child { - margin-bottom: 0; -} - -.node-body { - border-left: 3px solid var(--node-indent-border); +.node, .node-root { + display: flex; + flex-direction: column; + gap: 4px; } .node-body { display: flex; flex-direction: column; + gap: 4px; padding-left: 18px; + border-left: 3px solid var(--node-indent-border); } .list-node > .node-body > .object-node > .node-body,