Skip to content

Commit d434878

Browse files
committed
Add codemirror repl and arithmetic functions
1 parent 9667f09 commit d434878

File tree

21 files changed

+835
-96
lines changed

21 files changed

+835
-96
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,5 @@ jspm_packages/
5454

5555
# Output of 'npm pack'
5656
*.tgz
57+
58+
/dist

.idea/codeStyles/Project.xml

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codemirror/editor.js

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import {
2+
Compartment,
3+
EditorSelection,
4+
EditorState,
5+
Prec,
6+
} from '@codemirror/state'
7+
import { EditorView } from 'codemirror'
8+
import { lisp } from './lispLanguage.js'
9+
import { basicSetup } from '@uiw/codemirror-extensions-basic-setup'
10+
import { atomoneInit } from '@uiw/codemirror-theme-atomone'
11+
import { gutter, GutterMarker, keymap } from '@codemirror/view'
12+
import { evalQuote, NIL } from '../lib/lisp.js'
13+
import { parse } from '../lib/parser.js'
14+
import { print } from '../lib/print.js'
15+
16+
const createNewPromptHistory = () => ({
17+
in: '',
18+
out: null,
19+
inView: null,
20+
outView: null,
21+
})
22+
23+
let historyPointer = 0
24+
let latestPrompt = createNewPromptHistory()
25+
let loadedHistory = latestPrompt
26+
const promptHistory = [latestPrompt]
27+
28+
const createEditor = (parent, { isEditable, doc, isUser }) => {
29+
const userMarker = new (class extends GutterMarker {
30+
toDOM(_view) {
31+
const arrowElem = document.createElement('sl-icon')
32+
arrowElem.setAttribute('name', isUser ? 'chevron-right' : 'chevron-left')
33+
arrowElem.classList.add(
34+
'repl-marker',
35+
isUser ? 'user-marker' : 'out-marker',
36+
)
37+
return arrowElem
38+
}
39+
})()
40+
41+
const loadHistory = (view) => {
42+
if (promptHistory[historyPointer] === loadedHistory) {
43+
return
44+
}
45+
loadedHistory = promptHistory[historyPointer]
46+
const doc = promptHistory[historyPointer].in
47+
console.log('loading history', historyPointer, {
48+
from: 0,
49+
to: view.state.doc.length,
50+
insert: doc,
51+
})
52+
view.dispatch({
53+
changes: {
54+
from: 0,
55+
to: view.state.doc.length,
56+
insert: doc,
57+
},
58+
selection: EditorSelection.cursor(doc.length),
59+
})
60+
}
61+
62+
const editable = new Compartment()
63+
const getEditableExtensions = (isEditable) => [
64+
basicSetup({
65+
foldGutter: false,
66+
lineNumbers: false,
67+
highlightActiveLine: isEditable,
68+
highlightSelectionMatches: isEditable,
69+
drawSelection: isEditable,
70+
dropCursor: isEditable,
71+
bracketMatching: isEditable,
72+
highlightActiveLineGutter: isEditable,
73+
}),
74+
EditorView.editable.of(isEditable),
75+
gutter({
76+
class: 'cm-mygutter',
77+
lineMarker: (view, line) => {
78+
return line.top === 0 ? userMarker : null
79+
},
80+
}),
81+
EditorView.editorAttributes.of({ class: isEditable ? 'editable' : '' }),
82+
Prec.highest(
83+
keymap.of(
84+
isEditable
85+
? [
86+
{
87+
key: 'Enter',
88+
run: (view) => {
89+
const program = view.state.doc.toString()
90+
latestPrompt.in = program
91+
latestPrompt.inView = createEditor(historyElem, {
92+
isEditable: false,
93+
doc: latestPrompt.in,
94+
isUser: true,
95+
})
96+
try {
97+
const results = evalQuote(parse(program), NIL)
98+
latestPrompt.out = print(results).toString()
99+
} catch (e) {
100+
latestPrompt.out = e.stack
101+
}
102+
latestPrompt.outView = createEditor(historyElem, {
103+
isEditable: false,
104+
doc: latestPrompt.out,
105+
isUser: false,
106+
})
107+
view.dispatch({
108+
changes: {
109+
from: 0,
110+
to: view.state.doc.length,
111+
insert: '',
112+
},
113+
})
114+
latestPrompt = createNewPromptHistory()
115+
promptHistory.push(latestPrompt)
116+
historyPointer = promptHistory.length - 1
117+
loadedHistory = latestPrompt
118+
return true
119+
},
120+
},
121+
{
122+
any: (view, event) => {
123+
const currentLine = view.state.doc.lineAt(
124+
view.state.selection.main.head,
125+
).number
126+
const maxLineI = view.state.doc.lines
127+
if (event.key === 'ArrowUp') {
128+
if (currentLine === 1) {
129+
historyPointer = Math.max(0, historyPointer - 1)
130+
loadHistory(view)
131+
return true
132+
}
133+
} else if (event.key === 'ArrowDown') {
134+
if (currentLine === maxLineI) {
135+
historyPointer = Math.min(
136+
promptHistory.length - 1,
137+
historyPointer + 1,
138+
)
139+
loadHistory(view)
140+
return true
141+
}
142+
}
143+
},
144+
},
145+
]
146+
: [],
147+
),
148+
),
149+
]
150+
const setEditable = (view, isEditable) => {
151+
view.dispatch({
152+
effects: editable.reconfigure(getEditableExtensions(isEditable)),
153+
})
154+
}
155+
156+
const state = EditorState.create({
157+
doc,
158+
extensions: [
159+
atomoneInit({
160+
settings: {
161+
fontFamily: 'Jetbrains Mono, monospace',
162+
fontSize: '0.85rem',
163+
},
164+
}),
165+
lisp(),
166+
editable.of(getEditableExtensions(isEditable)),
167+
EditorState.tabSize.of(2),
168+
],
169+
})
170+
171+
const view = new EditorView({
172+
state,
173+
parent,
174+
})
175+
176+
return [view, setEditable]
177+
}
178+
179+
const historyElem = document.getElementById('history')
180+
const userPromptElem = document.getElementById('user-prompt')
181+
const [view, setEditable] = createEditor(userPromptElem, {
182+
isEditable: true,
183+
doc: latestPrompt.in,
184+
isUser: true,
185+
})
186+
window.s = (isEditable) => setEditable(view, isEditable)

codemirror/lezer/highlight.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { styleTags, tags as t } from '@lezer/highlight'
2+
3+
export const highlighting = styleTags({
4+
'VarName/Symbol': t.function(t.variableName),
5+
'Property/Symbol': t.propertyName,
6+
'Operator/Symbol': t.function(t.variableName),
7+
'DefLike Lambda': t.keyword,
8+
Symbol: t.variableName,
9+
Number: t.number,
10+
String: t.string,
11+
LineComment: t.lineComment,
12+
'Quote Quote/Symbol': t.keyword,
13+
'( )': t.punctuation,
14+
})

codemirror/lezer/lisp.grammar

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
atom {
2+
Symbol |
3+
String |
4+
Number
5+
}
6+
7+
expression {
8+
atom |
9+
List |
10+
Quote
11+
}
12+
13+
listContents {
14+
defList { DefLike VarName expression* } |
15+
lambdaList { Lambda "(" Property* ")" expression*} |
16+
anyList { Operator? expression* }
17+
}
18+
19+
VarName { Symbol }
20+
Property { Symbol }
21+
22+
@precedence { operator @left }
23+
Operator { !operator Symbol }
24+
25+
Symbol {
26+
trivialSymbol |
27+
barredSymbol
28+
}
29+
30+
List { "(" listContents ")" }
31+
32+
Quote { "'" expression }
33+
34+
@top Program { expression* }
35+
36+
@tokens {
37+
trivialSymbol { (!["| \t\n\r;:()'] | "\\" _)+ }
38+
barredSymbol { '|' (![|\\] | "\\" _)* '|' }
39+
String { '"' (!["\\] | "\\" _)* '"' }
40+
Number {
41+
("+" | "-")? (@digit+ ("." @digit* "M"?)? | "." @digit+) (("e" | "E") ("+" | "-")? @digit+ "M"?)?
42+
}
43+
LineComment { ";" ![\n]* }
44+
space { $[ \t\n\r]+ }
45+
"(" ")"
46+
47+
@precedence {Number, trivialSymbol}
48+
}
49+
50+
DefLike[@dynamicPrecedence=1] { @extend<trivialSymbol, "label"> }
51+
Lambda[@dynamicPrecedence=2] { @extend<trivialSymbol, "lambda"> }
52+
53+
@skip { space | LineComment }
54+
55+
@detectDelim
56+
57+
@external propSource highlighting from "./highlight.js"

codemirror/lezer/parser.js

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codemirror/lezer/parser.terms.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// This file was generated by lezer-generator. You probably shouldn't edit it.
2+
export const
3+
LineComment = 1,
4+
Program = 2,
5+
Symbol = 3,
6+
String = 4,
7+
Number = 5,
8+
List = 8,
9+
DefLike = 9,
10+
VarName = 10,
11+
Lambda = 11,
12+
Property = 12,
13+
Operator = 13,
14+
Quote = 14

codemirror/lispLanguage.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { parser } from './lezer/parser.js'
2+
import {
3+
foldInside,
4+
foldNodeProp,
5+
indentNodeProp,
6+
LanguageSupport,
7+
LRLanguage,
8+
} from '@codemirror/language'
9+
import { completeFromList } from '@codemirror/autocomplete'
10+
import { symbolsMap } from '../lib/parser.js'
11+
12+
const parserWithMetadata = parser.configure({
13+
props: [
14+
indentNodeProp.add({
15+
List: (context) => context.column(context.node.from) + context.unit,
16+
}),
17+
foldNodeProp.add({
18+
List: foldInside,
19+
}),
20+
],
21+
})
22+
23+
export const lispLanguage = LRLanguage.define({
24+
parser: parserWithMetadata,
25+
languageData: {
26+
commentTokens: { line: ';;' },
27+
},
28+
})
29+
30+
export const lispCompletion = lispLanguage.data.of({
31+
autocomplete: completeFromList(
32+
Object.keys(symbolsMap).map((keyword) => ({
33+
label: keyword,
34+
type: 'keyword',
35+
})),
36+
),
37+
closeBrackets: {
38+
brackets: ['(', '[', '{', '"'],
39+
},
40+
})
41+
42+
export function lisp() {
43+
return new LanguageSupport(lispLanguage, [lispCompletion])
44+
}

0 commit comments

Comments
 (0)