wip
This commit is contained in:
parent
1a9cd0f6ca
commit
7585f0e8a2
|
|
@ -1,5 +1,6 @@
|
||||||
import { nodeToString } from '@/evaluator/treeHelper'
|
import { nodeToString } from '@/evaluator/treeHelper'
|
||||||
import { Tree, type SyntaxNode } from '@lezer/common'
|
import { Tree, type SyntaxNode } from '@lezer/common'
|
||||||
|
import * as terms from '../parser/shrimp.terms.ts'
|
||||||
|
|
||||||
type Context = Map<string, any>
|
type Context = Map<string, any>
|
||||||
|
|
||||||
|
|
@ -29,19 +30,19 @@ const evaluateNode = (node: SyntaxNode, input: string, context: Context): any =>
|
||||||
const value = input.slice(node.from, node.to)
|
const value = input.slice(node.from, node.to)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
switch (node.name) {
|
switch (node.type.id) {
|
||||||
case 'Number': {
|
case terms.Number: {
|
||||||
return parseFloat(value)
|
return parseFloat(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'Identifier': {
|
case terms.Identifier: {
|
||||||
if (!context.has(value)) {
|
if (!context.has(value)) {
|
||||||
throw new Error(`Undefined identifier: ${value}`)
|
throw new Error(`Undefined identifier: ${value}`)
|
||||||
}
|
}
|
||||||
return context.get(value)
|
return context.get(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'BinOp': {
|
case terms.BinOp: {
|
||||||
let [left, op, right] = getChildren(node)
|
let [left, op, right] = getChildren(node)
|
||||||
|
|
||||||
left = assertNode(left, 'LeftOperand')
|
left = assertNode(left, 'LeftOperand')
|
||||||
|
|
@ -66,7 +67,7 @@ const evaluateNode = (node: SyntaxNode, input: string, context: Context): any =>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'Assignment': {
|
case terms.Assignment: {
|
||||||
const [identifier, expr] = getChildren(node)
|
const [identifier, expr] = getChildren(node)
|
||||||
|
|
||||||
const identifierNode = assertNode(identifier, 'Identifier')
|
const identifierNode = assertNode(identifier, 'Identifier')
|
||||||
|
|
@ -78,7 +79,7 @@ const evaluateNode = (node: SyntaxNode, input: string, context: Context): any =>
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'Function': {
|
case terms.Function: {
|
||||||
const [params, body] = getChildren(node)
|
const [params, body] = getChildren(node)
|
||||||
|
|
||||||
const paramNodes = getChildren(assertNode(params, 'Parameters'))
|
const paramNodes = getChildren(assertNode(params, 'Parameters'))
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,14 @@
|
||||||
|
import { Identifier, Params } from '@/parser/shrimp.terms'
|
||||||
import { styleTags, tags } from '@lezer/highlight'
|
import { styleTags, tags } from '@lezer/highlight'
|
||||||
|
|
||||||
export const highlighting = styleTags({
|
export const highlighting = styleTags({
|
||||||
Identifier: tags.name,
|
Identifier: tags.name,
|
||||||
Number: tags.number,
|
Number: tags.number,
|
||||||
String: tags.string,
|
String: tags.string,
|
||||||
|
Boolean: tags.bool,
|
||||||
|
Keyword: tags.keyword,
|
||||||
|
Operator: tags.operator,
|
||||||
|
// Params: tags.definition(tags.variableName),
|
||||||
|
'Params/Identifier': tags.definition(tags.variableName),
|
||||||
|
Paren: tags.paren,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -12,15 +12,15 @@
|
||||||
Boolean { "true" | "false" }
|
Boolean { "true" | "false" }
|
||||||
String { '"' !["]* '"' }
|
String { '"' !["]* '"' }
|
||||||
Identifier { $[A-Za-z_]$[A-Za-z_0-9-]* }
|
Identifier { $[A-Za-z_]$[A-Za-z_0-9-]* }
|
||||||
fn { "fn" }
|
fn[@name=Keyword] { "fn" }
|
||||||
arrow { "->" }
|
equals[@name=Operator] { "=" }
|
||||||
equals { "=" }
|
":"[@name=Colon]
|
||||||
"+"
|
"+"[@name=Operator]
|
||||||
"-"
|
"-"[@name=Operator]
|
||||||
"*"
|
"*"[@name=Operator]
|
||||||
"/"
|
"/"[@name=Operator]
|
||||||
leftParen { "(" }
|
leftParen[@name=Paren] { "(" }
|
||||||
rightParen { ")" }
|
rightParen[@name=Paren] { ")" }
|
||||||
}
|
}
|
||||||
|
|
||||||
@precedence {
|
@precedence {
|
||||||
|
|
@ -45,7 +45,7 @@ BinOp {
|
||||||
}
|
}
|
||||||
|
|
||||||
Params { Identifier* }
|
Params { Identifier* }
|
||||||
Function { !function fn Params arrow expr }
|
Function { !function fn Params ":" expr }
|
||||||
|
|
||||||
atom { Identifier | Number | String | Boolean | leftParen expr rightParen }
|
atom { Identifier | Number | String | Boolean | leftParen expr rightParen }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,13 @@ export const
|
||||||
Program = 1,
|
Program = 1,
|
||||||
Assignment = 2,
|
Assignment = 2,
|
||||||
Identifier = 3,
|
Identifier = 3,
|
||||||
Function = 4,
|
equals = 4,
|
||||||
Params = 5,
|
Function = 5,
|
||||||
BinOp = 6,
|
fn = 6,
|
||||||
Number = 11,
|
Params = 7,
|
||||||
String = 12,
|
BinOp = 9,
|
||||||
Boolean = 13
|
Number = 14,
|
||||||
|
String = 15,
|
||||||
|
Boolean = 16,
|
||||||
|
leftParen = 17,
|
||||||
|
rightParen = 18
|
||||||
|
|
|
||||||
|
|
@ -1,59 +1,59 @@
|
||||||
import { expectTree, regenerateParser } from '@/parser/test-helper'
|
import { regenerateParser } from '@/parser/test-helper'
|
||||||
import { beforeAll, describe, test } from 'bun:test'
|
import { expect, beforeAll, describe, test } from 'bun:test'
|
||||||
|
|
||||||
describe('BinOp', () => {
|
describe('BinOp', () => {
|
||||||
beforeAll(() => regenerateParser())
|
beforeAll(() => regenerateParser())
|
||||||
|
|
||||||
test('addition tests', () => {
|
test('addition tests', () => {
|
||||||
expectTree('2 + 3').toMatch(`
|
expect('2 + 3').toMatchTree(`
|
||||||
BinOp
|
BinOp
|
||||||
Number 2
|
Number 2
|
||||||
+
|
Operator +
|
||||||
Number 3
|
Number 3
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('subtraction tests', () => {
|
test('subtraction tests', () => {
|
||||||
expectTree('5 - 2').toMatch(`
|
expect('5 - 2').toMatchTree(`
|
||||||
BinOp
|
BinOp
|
||||||
Number 5
|
Number 5
|
||||||
-
|
Operator -
|
||||||
Number 2
|
Number 2
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('multiplication tests', () => {
|
test('multiplication tests', () => {
|
||||||
expectTree('4 * 3').toMatch(`
|
expect('4 * 3').toMatchTree(`
|
||||||
BinOp
|
BinOp
|
||||||
Number 4
|
Number 4
|
||||||
*
|
Operator *
|
||||||
Number 3
|
Number 3
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('division tests', () => {
|
test('division tests', () => {
|
||||||
expectTree('8 / 2').toMatch(`
|
expect('8 / 2').toMatchTree(`
|
||||||
BinOp
|
BinOp
|
||||||
Number 8
|
Number 8
|
||||||
/
|
Operator /
|
||||||
Number 2
|
Number 2
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('mixed operations with precedence', () => {
|
test('mixed operations with precedence', () => {
|
||||||
expectTree('2 + 3 * 4 - 5 / 1').toMatch(`
|
expect('2 + 3 * 4 - 5 / 1').toMatchTree(`
|
||||||
BinOp
|
BinOp
|
||||||
BinOp
|
BinOp
|
||||||
Number 2
|
Number 2
|
||||||
+
|
Operator +
|
||||||
BinOp
|
BinOp
|
||||||
Number 3
|
Number 3
|
||||||
*
|
Operator *
|
||||||
Number 4
|
Number 4
|
||||||
-
|
Operator -
|
||||||
BinOp
|
BinOp
|
||||||
Number 5
|
Number 5
|
||||||
/
|
Operator /
|
||||||
Number 1
|
Number 1
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
|
@ -63,39 +63,47 @@ describe('Fn', () => {
|
||||||
beforeAll(() => regenerateParser())
|
beforeAll(() => regenerateParser())
|
||||||
|
|
||||||
test('parses function with single parameter', () => {
|
test('parses function with single parameter', () => {
|
||||||
expectTree('fn x -> x + 1').toMatch(`
|
expect('fn x: x + 1').toMatchTree(`
|
||||||
Function
|
Function
|
||||||
|
Keyword fn
|
||||||
Params
|
Params
|
||||||
Identifier x
|
Identifier x
|
||||||
|
Colon :
|
||||||
BinOp
|
BinOp
|
||||||
Identifier x
|
Identifier x
|
||||||
+
|
Operator +
|
||||||
Number 1`)
|
Number 1`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('parses function with multiple parameters', () => {
|
test('parses function with multiple parameters', () => {
|
||||||
expectTree('fn x y -> x * y').toMatch(`
|
expect('fn x y: x * y').toMatchTree(`
|
||||||
Function
|
Function
|
||||||
|
Keyword fn
|
||||||
Params
|
Params
|
||||||
Identifier x
|
Identifier x
|
||||||
Identifier y
|
Identifier y
|
||||||
|
Colon :
|
||||||
BinOp
|
BinOp
|
||||||
Identifier x
|
Identifier x
|
||||||
*
|
Operator *
|
||||||
Identifier y`)
|
Identifier y`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('parses nested functions', () => {
|
test('parses nested functions', () => {
|
||||||
expectTree('fn x -> fn y -> x + y').toMatch(`
|
expect('fn x: fn y: x + y').toMatchTree(`
|
||||||
Function
|
Function
|
||||||
|
Keyword fn
|
||||||
Params
|
Params
|
||||||
Identifier x
|
Identifier x
|
||||||
|
Colon :
|
||||||
Function
|
Function
|
||||||
|
Keyword fn
|
||||||
Params
|
Params
|
||||||
Identifier y
|
Identifier y
|
||||||
|
Colon :
|
||||||
BinOp
|
BinOp
|
||||||
Identifier x
|
Identifier x
|
||||||
+
|
Operator +
|
||||||
Identifier y`)
|
Identifier y`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -104,22 +112,22 @@ describe('Identifier', () => {
|
||||||
beforeAll(() => regenerateParser())
|
beforeAll(() => regenerateParser())
|
||||||
|
|
||||||
test('parses hyphenated identifiers correctly', () => {
|
test('parses hyphenated identifiers correctly', () => {
|
||||||
expectTree('my-var - another-var').toMatch(`
|
expect('my-var - another-var').toMatchTree(`
|
||||||
BinOp
|
BinOp
|
||||||
Identifier my-var
|
Identifier my-var
|
||||||
-
|
Operator -
|
||||||
Identifier another-var`)
|
Identifier another-var`)
|
||||||
|
|
||||||
expectTree('double--trouble - another-var').toMatch(`
|
expect('double--trouble - another-var').toMatchTree(`
|
||||||
BinOp
|
BinOp
|
||||||
Identifier double--trouble
|
Identifier double--trouble
|
||||||
-
|
Operator -
|
||||||
Identifier another-var`)
|
Identifier another-var`)
|
||||||
|
|
||||||
expectTree('tail-- - another-var').toMatch(`
|
expect('tail-- - another-var').toMatchTree(`
|
||||||
BinOp
|
BinOp
|
||||||
Identifier tail--
|
Identifier tail--
|
||||||
-
|
Operator -
|
||||||
Identifier another-var`)
|
Identifier another-var`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -128,26 +136,30 @@ describe('Assignment', () => {
|
||||||
beforeAll(() => regenerateParser())
|
beforeAll(() => regenerateParser())
|
||||||
|
|
||||||
test('parses assignment with addition', () => {
|
test('parses assignment with addition', () => {
|
||||||
expectTree('x = 5 + 3').toMatch(`
|
expect('x = 5 + 3').toMatchTree(`
|
||||||
Assignment
|
Assignment
|
||||||
Identifier x
|
Identifier x
|
||||||
|
Operator =
|
||||||
BinOp
|
BinOp
|
||||||
Number 5
|
Number 5
|
||||||
+
|
Operator +
|
||||||
Number 3`)
|
Number 3`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('parses assignment with functions', () => {
|
test('parses assignment with functions', () => {
|
||||||
expectTree('add = fn a b -> a + b').toMatch(`
|
expect('add = fn a b: a + b').toMatchTree(`
|
||||||
Assignment
|
Assignment
|
||||||
Identifier add
|
Identifier add
|
||||||
|
Operator =
|
||||||
Function
|
Function
|
||||||
|
Keyword fn
|
||||||
Params
|
Params
|
||||||
Identifier a
|
Identifier a
|
||||||
Identifier b
|
Identifier b
|
||||||
|
Colon :
|
||||||
BinOp
|
BinOp
|
||||||
Identifier a
|
Identifier a
|
||||||
+
|
Operator +
|
||||||
Identifier b`)
|
Identifier b`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -156,30 +168,38 @@ describe('Parentheses', () => {
|
||||||
beforeAll(() => regenerateParser())
|
beforeAll(() => regenerateParser())
|
||||||
|
|
||||||
test('parses expressions with parentheses correctly', () => {
|
test('parses expressions with parentheses correctly', () => {
|
||||||
expectTree('(2 + 3) * 4').toMatch(`
|
expect('(2 + 3) * 4').toMatchTree(`
|
||||||
BinOp
|
BinOp
|
||||||
|
Paren (
|
||||||
BinOp
|
BinOp
|
||||||
Number 2
|
Number 2
|
||||||
+
|
Operator +
|
||||||
Number 3
|
Number 3
|
||||||
*
|
Paren )
|
||||||
|
Operator *
|
||||||
Number 4`)
|
Number 4`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('parses nested parentheses correctly', () => {
|
test('parses nested parentheses correctly', () => {
|
||||||
expectTree('((1 + 2) * (3 - 4)) / 5').toMatch(`
|
expect('((1 + 2) * (3 - 4)) / 5').toMatchTree(`
|
||||||
BinOp
|
BinOp
|
||||||
|
Paren (
|
||||||
BinOp
|
BinOp
|
||||||
|
Paren (
|
||||||
BinOp
|
BinOp
|
||||||
Number 1
|
Number 1
|
||||||
+
|
Operator +
|
||||||
Number 2
|
Number 2
|
||||||
*
|
Paren )
|
||||||
|
Operator *
|
||||||
|
Paren (
|
||||||
BinOp
|
BinOp
|
||||||
Number 3
|
Number 3
|
||||||
-
|
Operator -
|
||||||
Number 4
|
Number 4
|
||||||
/
|
Paren )
|
||||||
|
Paren )
|
||||||
|
Operator /
|
||||||
Number 5`)
|
Number 5`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -188,20 +208,22 @@ describe('multiline', () => {
|
||||||
beforeAll(() => regenerateParser())
|
beforeAll(() => regenerateParser())
|
||||||
|
|
||||||
test('parses multiline expressions', () => {
|
test('parses multiline expressions', () => {
|
||||||
expectTree(`
|
expect(`
|
||||||
5 + 4
|
5 + 4
|
||||||
fn x -> x - 1
|
fn x: x - 1
|
||||||
`).toMatch(`
|
`).toMatchTree(`
|
||||||
BinOp
|
BinOp
|
||||||
Number 5
|
Number 5
|
||||||
+
|
Operator +
|
||||||
Number 4
|
Number 4
|
||||||
Function
|
Function
|
||||||
|
Keyword fn
|
||||||
Params
|
Params
|
||||||
Identifier x
|
Identifier x
|
||||||
|
Colon :
|
||||||
BinOp
|
BinOp
|
||||||
Identifier x
|
Identifier x
|
||||||
-
|
Operator -
|
||||||
Number 1
|
Number 1
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,16 @@ import {LRParser} from "@lezer/lr"
|
||||||
import {highlighting} from "./highlight.js"
|
import {highlighting} from "./highlight.js"
|
||||||
export const parser = LRParser.deserialize({
|
export const parser = LRParser.deserialize({
|
||||||
version: 14,
|
version: 14,
|
||||||
states: "$OQVQPOOOkQPO'#CsO!fQQO'#C`O!nQPO'#CjOOQO'#Cs'#CsOVQPO'#CsOOQO'#Co'#CoQVQPOOOVQPO,58xOOQO'#Ck'#CkO#cQQO'#CaO#kQQO,58zOVQPO,58|OVQPO,58|O#pQPO,59_OOQO-E6h-E6hO$RQPO1G.dOOQO-E6i-E6iOVQPO1G.fOOQO1G.h1G.hO$yQPO1G.hOOQO1G.y1G.yO%qQPO7+$Q",
|
states: "$OQVQPOOOkQPO'#CuO!fQPO'#CaO!nQPO'#CoOOQO'#Cu'#CuOVQPO'#CuOOQO'#Ct'#CtQVQPOOOVQPO,58xOOQO'#Cp'#CpO#cQPO'#CcO#kQPO,58{OVQPO,59POVQPO,59PO#pQPO,59aOOQO-E6m-E6mO$RQPO1G.dOOQO-E6n-E6nOVQPO1G.gOOQO1G.k1G.kO$yQPO1G.kOOQO1G.{1G.{O%qQPO7+$R",
|
||||||
stateData: "&n~ObOS~ORPOZSO[SO]SOeQOhTO~OdWORgXVgXWgXXgXYgXZgX[gX]gX`gXegXhgXigX~ORXOfTP~OV[OW[OX]OY]OR^XZ^X[^X]^X`^Xe^Xh^X~ORXOfTX~OfbO~OV[OW[OX]OY]OieO~OV[OW[OX]OY]ORQiZQi[Qi]Qi`QieQihQiiQi~OV[OW[ORUiXUiYUiZUi[Ui]Ui`UieUihUiiUi~OV[OW[OX]OY]ORSqZSq[Sq]Sq`SqeSqhSqiSq~Oe]R]~",
|
stateData: "&n~OgOS~ORPOUQO^SO_SO`SOaTO~OSWORiXUiXYiXZiX[iX]iX^iX_iX`iXaiXeiXbiX~ORXOWVP~OY[OZ[O[]O]]ORcXUcX^cX_cX`cXacXecX~ORXOWVX~OWbO~OY[OZ[O[]O]]ObeO~OY[OZ[O[]O]]ORQiUQi^Qi_Qi`QiaQieQibQi~OY[OZ[ORXiUXi[Xi]Xi^Xi_Xi`XiaXieXibXi~OY[OZ[O[]O]]ORTqUTq^Tq_Tq`TqaTqeTqbTq~OU`R`~",
|
||||||
goto: "!fhPPiPiriPPPPPPPu{PPP!RPPPi_UOTVW[]bRZQQVOR_VQYQRaYSROVQ^TQ`WQc[Qd]Rfb",
|
goto: "!hjPPkPPkPtPkPPPPPPPPPw}PPP!Tk_UOTVW[]bRZQQVOR_VQYQRaYSROVQ^TQ`WQc[Qd]Rfb",
|
||||||
nodeNames: "⚠ Program Assignment Identifier Function Params BinOp * / + - Number String Boolean",
|
nodeNames: "⚠ Program Assignment Identifier Operator Function Keyword Params Colon BinOp Operator Operator Operator Operator Number String Boolean Paren Paren",
|
||||||
maxTerm: 25,
|
maxTerm: 25,
|
||||||
propSources: [highlighting],
|
propSources: [highlighting],
|
||||||
skippedNodes: [0],
|
skippedNodes: [0],
|
||||||
repeatNodeCount: 2,
|
repeatNodeCount: 2,
|
||||||
tokenData: "*f~RjX^!spq!srs#hxy$Vyz$[z{$a{|$f}!O$k!P!Q$x!Q![$}!_!`%h!c!}%m#R#S%m#T#Y%m#Y#Z&R#Z#h%m#h#i)`#i#o%m#y#z!s$f$g!s#BY#BZ!s$IS$I_!s$I|$JO!s$JT$JU!s$KV$KW!s&FU&FV!s~!xYb~X^!spq!s#y#z!s$f$g!s#BY#BZ!s$IS$I_!s$I|$JO!s$JT$JU!s$KV$KW!s&FU&FV!s~#kTOr#hrs#zs;'S#h;'S;=`$P<%lO#h~$PO[~~$SP;=`<%l#h~$[Oh~~$aOi~~$fOV~~$kOX~R$pPYP!`!a$sQ$xOfQ~$}OW~~%SQZ~!O!P%Y!Q![$}~%]P!Q![%`~%ePZ~!Q![%`~%mOd~~%rTR~}!O%m!Q![%m!c!}%m#R#S%m#T#o%m~&WWR~}!O%m!Q![%m!c!}%m#R#S%m#T#U&p#U#b%m#b#c(x#c#o%m~&uVR~}!O%m!Q![%m!c!}%m#R#S%m#T#`%m#`#a'[#a#o%m~'aVR~}!O%m!Q![%m!c!}%m#R#S%m#T#g%m#g#h'v#h#o%m~'{VR~}!O%m!Q![%m!c!}%m#R#S%m#T#X%m#X#Y(b#Y#o%m~(iT]~R~}!O%m!Q![%m!c!}%m#R#S%m#T#o%m~)PTe~R~}!O%m!Q![%m!c!}%m#R#S%m#T#o%m~)eVR~}!O%m!Q![%m!c!}%m#R#S%m#T#f%m#f#g)z#g#o%m~*PVR~}!O%m!Q![%m!c!}%m#R#S%m#T#i%m#i#j'v#j#o%m",
|
tokenData: "*f~RkX^!vpq!vrs#kxy$Yyz$_z{$d{|$i}!O$n!P!Q$s!Q![$x![!]%c!_!`%h!c!}%m#R#S%m#T#Y%m#Y#Z&R#Z#h%m#h#i)`#i#o%m#y#z!v$f$g!v#BY#BZ!v$IS$I_!v$I|$JO!v$JT$JU!v$KV$KW!v&FU&FV!v~!{Yg~X^!vpq!v#y#z!v$f$g!v#BY#BZ!v$IS$I_!v$I|$JO!v$JT$JU!v$KV$KW!v&FU&FV!v~#nTOr#krs#}s;'S#k;'S;=`$S<%lO#k~$SO_~~$VP;=`<%l#k~$_Oa~~$dOb~~$iOY~~$nO[~~$sO]~~$xOZ~~$}Q^~!O!P%T!Q![$x~%WP!Q![%Z~%`P^~!Q![%Z~%hOW~~%mOS~~%rTR~}!O%m!Q![%m!c!}%m#R#S%m#T#o%m~&WWR~}!O%m!Q![%m!c!}%m#R#S%m#T#U&p#U#b%m#b#c(x#c#o%m~&uVR~}!O%m!Q![%m!c!}%m#R#S%m#T#`%m#`#a'[#a#o%m~'aVR~}!O%m!Q![%m!c!}%m#R#S%m#T#g%m#g#h'v#h#o%m~'{VR~}!O%m!Q![%m!c!}%m#R#S%m#T#X%m#X#Y(b#Y#o%m~(iT`~R~}!O%m!Q![%m!c!}%m#R#S%m#T#o%m~)PTU~R~}!O%m!Q![%m!c!}%m#R#S%m#T#o%m~)eVR~}!O%m!Q![%m!c!}%m#R#S%m#T#f%m#f#g)z#g#o%m~*PVR~}!O%m!Q![%m!c!}%m#R#S%m#T#i%m#i#j'v#j#o%m",
|
||||||
tokenizers: [0, 1],
|
tokenizers: [0],
|
||||||
topRules: {"Program":[0,1]},
|
topRules: {"Program":[0,1]},
|
||||||
tokenPrec: 255
|
tokenPrec: 255
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -16,14 +16,37 @@ export const regenerateParser = async () => {
|
||||||
await $`bun generate-parser `
|
await $`bun generate-parser `
|
||||||
}
|
}
|
||||||
|
|
||||||
export const expectTree = (input: string) => {
|
// Type declaration for TypeScript
|
||||||
const tree = parser.parse(input)
|
declare module 'bun:test' {
|
||||||
|
interface Matchers<T> {
|
||||||
|
toMatchTree(expected: string): T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect.extend({
|
||||||
|
toMatchTree(received: unknown, expected: string) {
|
||||||
|
if (typeof received !== 'string') {
|
||||||
return {
|
return {
|
||||||
toMatch: (expected: string) => {
|
message: () => 'toMatchTree can only be used with string values',
|
||||||
expect(treeToString(tree, input)).toEqual(trimWhitespace(expected))
|
pass: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tree = parser.parse(received)
|
||||||
|
const actual = treeToString(tree, received)
|
||||||
|
const normalizedExpected = trimWhitespace(expected)
|
||||||
|
try {
|
||||||
|
// A hacky way to show the colorized diff in the test output
|
||||||
|
expect(actual).toEqual(normalizedExpected)
|
||||||
|
return { pass: true, message: () => '' }
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
message: () => (error as Error).message,
|
||||||
|
pass: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
}
|
|
||||||
|
|
||||||
const treeToString = (tree: Tree, input: string): string => {
|
const treeToString = (tree: Tree, input: string): string => {
|
||||||
const lines: string[] = []
|
const lines: string[] = []
|
||||||
|
|
@ -44,7 +67,7 @@ const treeToString = (tree: Tree, input: string): string => {
|
||||||
} else {
|
} else {
|
||||||
const cleanText = nodeName === 'String' ? text.slice(1, -1) : text
|
const cleanText = nodeName === 'String' ? text.slice(1, -1) : text
|
||||||
// Node names that should be displayed as single tokens (operators, keywords)
|
// Node names that should be displayed as single tokens (operators, keywords)
|
||||||
const singleTokens = ['+', '-', '*', '/', '->']
|
const singleTokens = ['+', '-', '*', '/', '->', 'fn', '=', 'equals']
|
||||||
if (singleTokens.includes(nodeName)) {
|
if (singleTokens.includes(nodeName)) {
|
||||||
lines.push(`${indent}${nodeName}`)
|
lines.push(`${indent}${nodeName}`)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
71
src/server/debugPlugin.ts
Normal file
71
src/server/debugPlugin.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import {
|
||||||
|
EditorView,
|
||||||
|
Decoration,
|
||||||
|
ViewPlugin,
|
||||||
|
ViewUpdate,
|
||||||
|
WidgetType,
|
||||||
|
type DecorationSet,
|
||||||
|
} from '@codemirror/view'
|
||||||
|
import { syntaxTree } from '@codemirror/language'
|
||||||
|
|
||||||
|
const createDebugWidget = (tags: string) =>
|
||||||
|
Decoration.widget({
|
||||||
|
widget: new (class extends WidgetType {
|
||||||
|
toDOM() {
|
||||||
|
const div = document.createElement('div')
|
||||||
|
div.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
background: #000;
|
||||||
|
color: #00ff00;
|
||||||
|
padding: 8px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
border: 1px solid #333;
|
||||||
|
z-index: 1000;
|
||||||
|
max-width: 300px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
`
|
||||||
|
div.textContent = tags
|
||||||
|
return div
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const debugTags = ViewPlugin.fromClass(
|
||||||
|
class {
|
||||||
|
decorations: DecorationSet = Decoration.none
|
||||||
|
|
||||||
|
constructor(view: EditorView) {
|
||||||
|
this.updateDecorations(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
update(update: ViewUpdate) {
|
||||||
|
if (update.docChanged || update.selectionSet) {
|
||||||
|
this.updateDecorations(update.view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDecorations(view: EditorView) {
|
||||||
|
const pos = view.state.selection.main.head
|
||||||
|
const tree = syntaxTree(view.state)
|
||||||
|
|
||||||
|
let tags: string[] = []
|
||||||
|
let node = tree.resolveInner(pos, -1)
|
||||||
|
|
||||||
|
while (node) {
|
||||||
|
tags.push(node.type.name)
|
||||||
|
node = node.parent!
|
||||||
|
if (!node) break
|
||||||
|
}
|
||||||
|
|
||||||
|
const debugText = tags.length ? tags.join(' > ') : 'No nodes'
|
||||||
|
this.decorations = Decoration.set([createDebugWidget(debugText).range(pos)])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
decorations: (v) => v.decorations,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import { basicSetup } from 'codemirror'
|
import { basicSetup } from 'codemirror'
|
||||||
import { EditorView } from '@codemirror/view'
|
import { EditorView } from '@codemirror/view'
|
||||||
import { editorTheme } from './editorTheme'
|
import { editorTheme } from './editorTheme'
|
||||||
|
import { shrimpLanguage } from './shrimpLanguage'
|
||||||
|
import { shrimpHighlighting } from './editorTheme'
|
||||||
|
import { debugTags } from '@/server/debugPlugin'
|
||||||
|
|
||||||
export const Editor = () => {
|
export const Editor = () => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -10,9 +13,14 @@ export const Editor = () => {
|
||||||
|
|
||||||
console.log('init editor')
|
console.log('init editor')
|
||||||
new EditorView({
|
new EditorView({
|
||||||
doc: '',
|
doc: `a = 3
|
||||||
|
fn x y: x + y
|
||||||
|
aa = fn radius: 3.14 * radius * radius
|
||||||
|
b = true
|
||||||
|
c = "cyan"
|
||||||
|
`,
|
||||||
parent: ref,
|
parent: ref,
|
||||||
extensions: [basicSetup, editorTheme],
|
extensions: [basicSetup, editorTheme, shrimpLanguage(), shrimpHighlighting],
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,50 @@
|
||||||
import { EditorView } from '@codemirror/view'
|
import { EditorView } from '@codemirror/view'
|
||||||
|
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language'
|
||||||
|
import { tags } from '@lezer/highlight'
|
||||||
|
|
||||||
|
const highlightStyle = HighlightStyle.define([
|
||||||
|
{ tag: tags.keyword, color: '#C792EA' }, // fn - soft purple (Night Owl inspired)
|
||||||
|
{ tag: tags.name, color: '#82AAFF' }, // identifiers - bright blue (Night Owl style)
|
||||||
|
{ tag: tags.string, color: '#C3E88D' }, // strings - soft green
|
||||||
|
{ tag: tags.number, color: '#F78C6C' }, // numbers - warm orange
|
||||||
|
{ tag: tags.bool, color: '#FF5370' }, // booleans - coral red
|
||||||
|
{ tag: tags.operator, color: '#89DDFF' }, // operators - cyan blue
|
||||||
|
{ tag: tags.paren, color: '#676E95' }, // parens - muted blue-gray
|
||||||
|
{
|
||||||
|
tag: tags.definition(tags.variableName),
|
||||||
|
color: '#FFCB6B', // warm yellow
|
||||||
|
backgroundColor: '#1E2A4A', // dark blue background
|
||||||
|
padding: '1px 2px',
|
||||||
|
borderRadius: '2px',
|
||||||
|
fontWeight: '500',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
export const shrimpHighlighting = syntaxHighlighting(highlightStyle)
|
||||||
|
|
||||||
export const editorTheme = EditorView.theme(
|
export const editorTheme = EditorView.theme(
|
||||||
{
|
{
|
||||||
'&': {
|
'&': {
|
||||||
color: '#7C70DA',
|
color: '#D6DEEB', // Night Owl text color
|
||||||
backgroundColor: '#40318D',
|
backgroundColor: '#011627', // Night Owl dark blue
|
||||||
fontFamily: '"Pixeloid Mono", "Courier New", monospace',
|
fontFamily: '"Pixeloid Mono", "Courier New", monospace',
|
||||||
fontSize: '18px',
|
fontSize: '18px',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
},
|
},
|
||||||
'.cm-content': {
|
'.cm-content': {
|
||||||
caretColor: '#7C70DA',
|
caretColor: '#80A4C2', // soft blue caret
|
||||||
padding: '0px',
|
padding: '0px',
|
||||||
minHeight: '100px',
|
minHeight: '100px',
|
||||||
borderBottom: '3px solid #7C70DA',
|
borderBottom: '3px solid #1E2A4A',
|
||||||
},
|
},
|
||||||
'.cm-activeLine': {
|
'.cm-activeLine': {
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
},
|
},
|
||||||
'&.cm-focused .cm-cursor': {
|
'&.cm-focused .cm-cursor': {
|
||||||
borderLeftColor: '#7C70DA',
|
borderLeftColor: '#80A4C2',
|
||||||
},
|
},
|
||||||
'&.cm-focused .cm-selectionBackground, ::selection': {
|
'&.cm-focused .cm-selectionBackground, ::selection': {
|
||||||
backgroundColor: '#5A4FCF',
|
backgroundColor: '#1D3B53', // darker blue selection
|
||||||
},
|
},
|
||||||
'.cm-gutters': {
|
'.cm-gutters': {
|
||||||
display: 'none',
|
display: 'none',
|
||||||
|
|
|
||||||
11
src/server/shrimpLanguage.ts
Normal file
11
src/server/shrimpLanguage.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { parser } from '../parser/shrimp'
|
||||||
|
import { LRLanguage, LanguageSupport } from '@codemirror/language'
|
||||||
|
import { highlighting } from '../parser/highlight.js'
|
||||||
|
|
||||||
|
export const shrimpLanguage = () => {
|
||||||
|
const language = LRLanguage.define({
|
||||||
|
parser: parser.configure({ props: [highlighting] }),
|
||||||
|
})
|
||||||
|
|
||||||
|
return new LanguageSupport(language)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user