diff --git a/package-lock.json b/package-lock.json index fd0aa3c3..f18f6576 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@mcschema/java-1.18": "^0.2.15", "@mcschema/java-1.18.2": "npm:@mcschema/java-1.18@^0.3.0-beta.5", "@mcschema/locales": "^0.1.45", + "brace": "^0.11.1", "buffer": "^6.0.3", "comment-json": "^4.1.1", "deepslate": "^0.9.0-beta.9", @@ -1088,6 +1089,11 @@ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", "dev": true }, + "node_modules/brace": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/brace/-/brace-0.11.1.tgz", + "integrity": "sha1-SJb8ydVE7vRfS7dmDbMg07N5/lg=" + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -5218,6 +5224,11 @@ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", "dev": true }, + "brace": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/brace/-/brace-0.11.1.tgz", + "integrity": "sha1-SJb8ydVE7vRfS7dmDbMg07N5/lg=" + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", diff --git a/package.json b/package.json index 22316aa8..6b67d9ca 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@mcschema/java-1.18": "^0.2.15", "@mcschema/java-1.18.2": "npm:@mcschema/java-1.18@^0.3.0-beta.5", "@mcschema/locales": "^0.1.45", + "brace": "^0.11.1", "buffer": "^6.0.3", "comment-json": "^4.1.1", "deepslate": "^0.9.0-beta.9", diff --git a/src/app/components/generator/SourcePanel.tsx b/src/app/components/generator/SourcePanel.tsx index 8b946cea..4293dc38 100644 --- a/src/app/components/generator/SourcePanel.tsx +++ b/src/app/components/generator/SourcePanel.tsx @@ -1,4 +1,7 @@ import { DataModel } from '@mcschema/core' +import brace from 'brace' +import 'brace/mode/json' +import 'brace/mode/yaml' import json from 'comment-json' import yaml from 'js-yaml' import { useCallback, useEffect, useRef, useState } from 'preact/hooks' @@ -10,8 +13,6 @@ import type { BlockStateRegistry } from '../../services' import { Store } from '../../Store' import { message } from '../../Utils' -const OUTPUT_CHARS_LIMIT = 10000 - const INDENT: Record = { '2_spaces': 2, '4_spaces': 4, @@ -50,9 +51,11 @@ export function SourcePanel({ name, model, blockStates, doCopy, doDownload, doIm const { locale } = useLocale() const [indent, setIndent] = useState(Store.getIndent()) const [format, setFormat] = useState(Store.getFormat()) - const source = useRef(null) const download = useRef(null) const retransform = useRef() + const onImport = useRef<(e: any) => any>() + + const editor = useRef() const getSerializedOutput = useCallback((model: DataModel, blockStates: BlockStateRegistry) => { const data = getOutput(model, blockStates) @@ -64,18 +67,38 @@ export function SourcePanel({ name, model, blockStates, doCopy, doDownload, doIm if (!model || !blockStates) return try { const output = getSerializedOutput(model, blockStates) - if (output.length >= OUTPUT_CHARS_LIMIT) { - source.current.value = output.slice(0, OUTPUT_CHARS_LIMIT) + `\n\nOutput is too large to display (+${OUTPUT_CHARS_LIMIT} chars)\nExport to view complete output\n\n` - } else { - source.current.value = output - } + editor.current.getSession().setValue(output) } catch (e) { onError(`Error getting JSON output: ${message(e)}`) console.error(e) - source.current.value = '' + editor.current.setValue('') } } - }) + + onImport.current = () => { + const value = editor.current.getValue() + if (value.length === 0) return + try { + const data = FORMATS[format].parse(value) + model?.reset(DataModel.wrapLists(data), false) + } catch (e) { + onError(`Error importing: ${message(e)}`) + console.error(e) + } + } + }, [model, blockStates, indent, format]) + + useEffect(() => { + editor.current = brace.edit('editor') + editor.current.setOptions({ + fontSize: 14, + showFoldWidgets: false, + highlightSelectedWord: false, + }) + editor.current.$blockScrolling = Infinity + editor.current.on('blur', e => onImport.current(e)) + editor.current.getSession().setMode('ace/mode/json') + }, []) useModel(model, () => { retransform.current() @@ -85,20 +108,12 @@ export function SourcePanel({ name, model, blockStates, doCopy, doDownload, doIm }, [model]) useEffect(() => { + const softTabs = indent === 'tabs' ? false : INDENT[indent] + editor.current.setOption('useSoftTabs', softTabs) + editor.current.getSession().setMode(`ace/mode/${format}`) retransform.current() }, [indent, format]) - const onImport = () => { - if (source.current.value.length === 0) return - try { - const data = FORMATS[format].parse(source.current.value) - model?.reset(DataModel.wrapLists(data), false) - } catch (e) { - onError(`Error importing: ${message(e)}`) - console.error(e) - } - } - useEffect(() => { if (doCopy && model && blockStates) { navigator.clipboard.writeText(getSerializedOutput(model, blockStates)).then(() => { @@ -117,9 +132,9 @@ export function SourcePanel({ name, model, blockStates, doCopy, doDownload, doIm }, [doDownload]) useEffect(() => { - if (doImport && source.current) { - source.current.value = '' - source.current.select() + if (doImport && editor.current) { + editor.current.setValue('') + editor.current.selectAll() } }, [doImport]) @@ -134,7 +149,7 @@ export function SourcePanel({ name, model, blockStates, doCopy, doDownload, doIm } return <> -
+
{Object.entries(INDENT).map(([key]) => changeFormat(key)} />)}
- +

 		
 	
 }
diff --git a/src/styles/global.css b/src/styles/global.css
index c5cba983..f01cb710 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -20,12 +20,16 @@
 	--nav-hover: #b4b3b0;
 	--nav-faded: #4d4c4c;
 	--nav-faded-hover: #6e6e6e;
-	--selection: #6786dd99;
+	--selection: #445a9599;
 	--errors-background: #62190f;
 	--errors-text: #ffffffcc;
   --invalid-text: #fd7951;
 	--text-saturation: 60%;
 	--text-lightness: 45%;
+	--editor-variable: #9CDCFE;
+	--editor-string: #CE9178;
+	--editor-constant: #569CD6;
+	--editor-number: #B5CEA8;
 }
 
 :root[data-theme=light] {
@@ -56,6 +60,10 @@
   --invalid-text: #a32600;
 	--text-saturation: 100%;
 	--text-lightness: 30%;
+	--editor-variable: #0451A5;
+	--editor-string: #A31515;
+	--editor-constant: #0000FF;
+	--editor-number: #098658;
 }
 
 @media (prefers-color-scheme: light) {
@@ -307,6 +315,7 @@ main > .controls {
 
 .source-controls {
 	justify-content: flex-end;
+	z-index: 10;
 }
 
 .tree {
@@ -1249,6 +1258,49 @@ hr {
 	margin: 8px 0 0;
 }
 
+.ace_editor,
+.ace_gutter,
+.ace_gutter .ace_layer,
+.ace_content {
+	color: var(--text-2) !important;
+	background-color: var(--background-2) !important;
+	border: none;
+}
+
+.ace_cursor {
+	color: var(--text-1) !important;
+}
+
+.ace_gutter-active-line {
+	background-color: var(--background-3) !important;
+}
+
+.ace_tag,
+.ace_variable {
+	color: var(--editor-variable) !important;
+}
+
+.ace_string {
+	color: var(--editor-string) !important;
+}
+
+.ace_constant {
+	color: var(--editor-constant) !important;
+}
+
+.ace_numeric {
+	color: var(--editor-number) !important;
+}
+
+.ace_markup,
+.ace_keyword {
+	color: unset !important;
+}
+
+.ace_marker-layer .ace_selection {
+	background-color: var(--selection) !important;
+}
+
 @media screen and (max-width: 720px) {
 	.sound-search-group {
 		margin-bottom: 8px;