diff --git a/vweb/package-lock.json b/vweb/package-lock.json index ef56863..0ef0750 100644 --- a/vweb/package-lock.json +++ b/vweb/package-lock.json @@ -5,6 +5,9 @@ "packages": { "": { "dependencies": { + "@codemirror/commands": "^6.5.0", + "@codemirror/language": "^6.10.1", + "@codemirror/view": "^6.26.3", "cm6-theme-gruvbox-dark": "^0.2.0", "codemirror": "^6.0.1", "esbuild": "^0.16.12", diff --git a/vweb/package.json b/vweb/package.json index 105294e..f5b5c20 100644 --- a/vweb/package.json +++ b/vweb/package.json @@ -1,5 +1,8 @@ { "dependencies": { + "@codemirror/commands": "^6.5.0", + "@codemirror/language": "^6.10.1", + "@codemirror/view": "^6.26.3", "cm6-theme-gruvbox-dark": "^0.2.0", "codemirror": "^6.0.1", "esbuild": "^0.16.12", diff --git a/vweb/src/forge_editor.jsx b/vweb/src/forge_editor.jsx index a119d96..2038c59 100644 --- a/vweb/src/forge_editor.jsx +++ b/vweb/src/forge_editor.jsx @@ -1,28 +1,31 @@ import './snippet_emulator.css' -import React, {useCallback, useState, useRef, useEffect} from 'react' +import React, {useEffect, useRef} from 'react' import {basicSetup, EditorView} from 'codemirror' +import {keymap} from '@codemirror/view' +import {indentWithTab} from '@codemirror/commands' +import {StreamLanguage} from '@codemirror/language' import {gruvboxDark} from 'cm6-theme-gruvbox-dark' +import ForgeHighlighter from './forge_highlighter' export default function ForgeEditor({ src, updateSrc }) { const editor = useRef(null) const container = useRef(null) useEffect(() => { - if (container.current) { - editor.current = new EditorView({ - doc: src, - extensions: [ - basicSetup, - gruvboxDark, - EditorView.updateListener.of((update) => { - updateSrc(update.state.doc.text.join('\n')) - }) - ], - parent: container.current - }) - } - }, [container.current]) + editor.current = new EditorView({ + doc: src, + extensions: [ + basicSetup, + keymap.of([indentWithTab]), + gruvboxDark, + EditorView.updateListener.of((update) => { + updateSrc(update.state.doc.text.join('\n')) + }), + StreamLanguage.define(ForgeHighlighter) + ], + parent: container.current + }) + }, []) - const onChangeSrc = useCallback((e) => { updateSrc(e.target.value) }, [updateSrc]) return
} diff --git a/vweb/src/forge_highlighter.js b/vweb/src/forge_highlighter.js new file mode 100644 index 0000000..7d99ecf --- /dev/null +++ b/vweb/src/forge_highlighter.js @@ -0,0 +1,108 @@ +export default { + name: 'forge', + + startState: function () { + // asm is "have we seen the asm keyword." The first time we see braces after that keyword (ignoring + // strings, because string literals in an asm arg block is legal) we start highlighting it as asm. + // The current state is just the current state, null means normal. + return {current: null, asm: false} + }, + + token: function (stream, state) { + if (stream.eatSpace()) return null + + if (state.current === null) { + let word + if (word = stream.match(/^[a-zA-Z_$][a-zA-Z0-9_$]*/)) { + // This is either a keyword or an identifier. + // All the keywords are valid names, so, check that: + if (word[0].match(/^fn|var|new|static|asm|return|if|while|repeat|global|const$/)) { + if (word[0] === 'asm') { + state.asm = true + } // This is the start of an asm control structure + return 'keyword' + } else { + return 'variable' + } + } else if (stream.match(/^-?((0b[01]+)|(0x[0-9a-fA-F]+)|(0o[0-7]+)|0|([1-9][0-9]*?))/)) { + // All the number formats. Unary minus is just an operator + return 'number' + } else if (stream.match(/^"/)) { + // Open quote to start a string + state.current = 'string' + return 'string' + } else if (stream.match(/^{/)) { + // Open brace + if (state.asm) { + // This is the start of the _actual_ asm block, kick us into asm mode and gooooo + state.current = 'asm' + state.asm = false // clear the flag + return 'quote' + } else { + // A normal open brace, just highlight it. If we do indentation stuff it goes here. + return 'strong' + } + } else if (stream.match(/^[};]/)) { + // Close brace and semicolon get bolded. + return 'strong' + } else if (stream.match(/^\/\/.*/)) { + // Normal line comment + return 'comment' + } else if (stream.match(/^\/\*/)) { + // Start of a block comment, switch our mode + state.current = 'comment' + return 'comment' + } else if (stream.match(/^&&|\|\||==|!=|>=|<=|[-+*/%&|^><=]/)) { + // These operators have to be after the comment check + return 'operator' + } else { + // Dunno what this is, so just return content + stream.next() + return 'content' + } + } else if (state.current === 'comment') { + // We're in a block comment + if (stream.match(/^\*\//)) { + // End of a block comment, clear the state + state.current = null + } else { + // Have to move forward + stream.next() + } + return 'comment' // No matter whether we clear the state or not, it's still currently a comment + } else if (state.current === 'string') { + // We're in a string + if (stream.match(/^"/)) { + // We're no longer in a string + state.current = null + } else { + // If the next char is a backslash... + stream.eat(/^\\/) + stream.next() // Advance past the _next_ char, skip whatever the escape is. + } + return 'string' + } else if (state.current === 'asm') { + // We're in an asm block + if (stream.match(/^}/)) { + // asm blocks end on close-brace, whether quoted or not! Forge is unaware of any brace rules in + // the assembler. This is a reality in the compiler as well, not just this highlighter. + state.current = null + } else { + // If it's not a brace move forward anyway + stream.next() + } + return 'quote' + } + }, + + indent: function (state, textAfter, cx) { + return 0 + //const closing = dedentPartial.test(textAfter); + //return state.basecol + cx.unit * (state.indentDepth - (closing ? 1 : 0)); + }, + + languageData: { + //indentOnInput: /^\s*(?:end|until|else|\)|\})$/, + //commentTokens: {line: '//', block: {open: '/*', close: '*/'}} + } +} \ No newline at end of file