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