Compare commits

..

3 Commits

Author SHA1 Message Date
2c7508c2a4 Better edtior 2026-01-05 11:30:32 -08:00
82722ec9e4 Update exports and editor script in package.json
Refactored the exports field to provide explicit entry points for the main module, editor, and editor CSS. Updated the editor script to point to the example server file.
2026-01-05 11:30:23 -08:00
04c2137fe2 Use isDebug 2026-01-05 11:30:02 -08:00
15 changed files with 312 additions and 78 deletions

View File

@ -1,11 +1,15 @@
{ {
"name": "shrimp", "name": "shrimp",
"version": "0.1.0", "version": "0.1.0",
"exports": "./src/index.ts", "exports": {
".": "./src/index.ts",
"./editor": "./src/editor/index.ts",
"./editor.css": "./src/editor/editor.css"
},
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"editor": "bun --hot src/editor/server.tsx", "editor": "bun --hot src/editor/example/server.tsx",
"repl": "bun bin/repl", "repl": "bun bin/repl",
"update-reef": "rm -rf ~/.bun/install/cache/ && rm bun.lock && bun update reefvm", "update-reef": "rm -rf ~/.bun/install/cache/ && rm bun.lock && bun update reefvm",
"cli:install": "ln -s \"$(pwd)/bin/shrimp\" ~/.bun/bin/shrimp", "cli:install": "ln -s \"$(pwd)/bin/shrimp\" ~/.bun/bin/shrimp",
@ -33,4 +37,4 @@
"singleQuote": true, "singleQuote": true,
"printWidth": 100 "printWidth": 100
} }
} }

View File

@ -1,2 +1,102 @@
// Autocomplete for Shrimp import { autocompletion, type CompletionContext, type Completion } from '@codemirror/autocomplete'
// TODO: Keywords and prelude function names import { Shrimp, type Value } from '#/index'
const keywords = [
'import',
'end',
'do',
'if',
'else',
'while',
'try',
'catch',
'finally',
'throw',
'not',
'and',
'or',
'true',
'false',
'null',
]
const keywordCompletions: Completion[] = keywords.map((k) => ({
label: k,
type: 'keyword',
boost: -1,
}))
const buildFunctionCompletion = (name: string, value: unknown): Completion => {
let detail: string | undefined
console.log(`🌭`, { name, fn: value?.toString() })
// console.log(`🌭`, { name, value })
// const isFunction = value?.type === 'function'
// if (isFunction) {
// const paramStrs = value.params.map((p) => (p in value.defaults ? `${p}?` : p))
// if (value.variadic && paramStrs.length > 0) {
// paramStrs[paramStrs.length - 1] = `...${paramStrs[paramStrs.length - 1]}`
// }
// detail = `(${paramStrs.join(', ')})`
// }
return {
label: name,
type: 'function',
detail,
}
}
export const createShrimpCompletions = (shrimp: Shrimp) => {
// Build completions from all names in the shrimp scope
const scopeNames = shrimp.vm.scope.vars()
const functionCompletions: Completion[] = scopeNames.map((name) =>
buildFunctionCompletion(name, shrimp.get(name))
)
const allCompletions = [...keywordCompletions, ...functionCompletions]
// Get methods for a module (e.g., math, str, list)
const getModuleMethods = (moduleName: string): Completion[] => {
const module = shrimp.get(moduleName)
if (!module || module.type !== 'dict') return []
return Array.from(module.value.keys()).map((name) =>
buildFunctionCompletion(name, module.value.get(name))
)
}
const shrimpCompletionSource = (context: CompletionContext) => {
// Check for module.method pattern (e.g., "math.")
const dotMatch = context.matchBefore(/[\w\-]+\.[\w\-\?]*/)
if (dotMatch) {
const [moduleName, methodPrefix] = dotMatch.text.split('.')
const methods = getModuleMethods(moduleName!)
if (methods.length > 0) {
const dotPos = dotMatch.from + moduleName!.length + 1
return {
from: dotPos,
options: methods.filter((m) => m.label.startsWith(methodPrefix ?? '')),
}
}
}
// Regular completions
const word = context.matchBefore(/[\w\-\?\$]+/)
if (!word || (word.from === word.to && !context.explicit)) return null
return {
from: word.from,
options: allCompletions.filter((c) => c.label.startsWith(word.text)),
}
}
return autocompletion({
override: [shrimpCompletionSource],
})
}

View File

@ -2,24 +2,23 @@ import { linter, type Diagnostic } from '@codemirror/lint'
import { Shrimp } from '#/index' import { Shrimp } from '#/index'
import { CompilerError } from '#compiler/compilerError' import { CompilerError } from '#compiler/compilerError'
const shrimp = new Shrimp() export const createShrimpDiagnostics = (shrimp: Shrimp) =>
linter((view) => {
const code = view.state.doc.toString()
const diagnostics: Diagnostic[] = []
export const shrimpDiagnostics = linter((view) => { try {
const code = view.state.doc.toString() shrimp.parse(code)
const diagnostics: Diagnostic[] = [] } catch (err) {
if (err instanceof CompilerError) {
try { diagnostics.push({
shrimp.parse(code) from: err.from,
} catch (err) { to: err.to,
if (err instanceof CompilerError) { severity: 'error',
diagnostics.push({ message: err.message,
from: err.from, })
to: err.to, }
severity: 'error',
message: err.message,
})
} }
}
return diagnostics return diagnostics
}) })

View File

@ -0,0 +1,78 @@
:root {
/* Background colors */
--bg-editor: #011627;
--bg-output: #40318D;
--bg-status-bar: #1E2A4A;
--bg-status-border: #0E1A3A;
--bg-selection: #1D3B53;
/* Text colors */
--text-editor: #D6DEEB;
--text-output: #7C70DA;
--text-status: #B3A9FF55;
--caret: #80A4C2;
/* Syntax highlighting colors */
--color-keyword: #C792EA;
--color-function: #82AAFF;
--color-string: #C3E88D;
--color-number: #F78C6C;
--color-bool: #FF5370;
--color-operator: #89DDFF;
--color-paren: #676E95;
--color-function-call: #FF9CAC;
--color-variable-def: #FFCB6B;
--color-error: #FF6E6E;
--color-regex: #E1ACFF;
/* ANSI terminal colors */
--ansi-black: #011627;
--ansi-red: #FF5370;
--ansi-green: #C3E88D;
--ansi-yellow: #FFCB6B;
--ansi-blue: #82AAFF;
--ansi-magenta: #C792EA;
--ansi-cyan: #89DDFF;
--ansi-white: #D6DEEB;
/* ANSI bright colors (slightly more vibrant) */
--ansi-bright-black: #676E95;
--ansi-bright-red: #FF6E90;
--ansi-bright-green: #D4F6A8;
--ansi-bright-yellow: #FFE082;
--ansi-bright-blue: #A8C7FA;
--ansi-bright-magenta: #E1ACFF;
--ansi-bright-cyan: #A8F5FF;
--ansi-bright-white: #FFFFFF;
}
@font-face {
font-family: 'C64ProMono';
src: url('../../assets/C64_Pro_Mono-STYLE.woff2') format('woff2');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Pixeloid Mono';
src: url('../../assets/PixeloidMono.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: var(--bg-editor);
font-family: 'Pixeloid Mono', 'Courier New', monospace;
font-size: 18px;
height: 100vh;
}
#root {
height: 100vh;
}

View File

@ -1,30 +1,70 @@
import { EditorView, basicSetup } from 'codemirror' import { EditorView, basicSetup } from 'codemirror'
import { render } from 'hono/jsx/dom' import { Shrimp } from '#/index'
import { shrimpTheme } from './theme' import { shrimpTheme } from './theme'
import { shrimpDiagnostics } from './diagnostics' import { createShrimpDiagnostics } from './diagnostics'
import { persistencePlugin, getContent } from './persistence'
import { shrimpHighlighter } from './highlighter' import { shrimpHighlighter } from './highlighter'
import { createShrimpCompletions } from './completions'
import { shrimpKeymap } from './keymap'
import { getContent, persistence } from './persistence'
export const Editor = () => { type EditorProps = {
initialCode?: string
onChange?: (code: string) => void
extensions?: import('@codemirror/state').Extension[]
shrimp?: Shrimp
}
export const Editor = ({
initialCode = '',
onChange,
extensions: customExtensions = [],
shrimp = new Shrimp(),
}: EditorProps) => {
return ( return (
<div <div
ref={(el: Element) => { ref={(el: Element) => {
if (!el?.querySelector('.cm-editor')) createEditorView(el) if (!el?.querySelector('.cm-editor'))
createEditorView(el, getContent() ?? initialCode, onChange, customExtensions, shrimp)
}} }}
/> />
) )
} }
const createEditorView = (el: Element) => { const createEditorView = (
el: Element,
initialCode: string,
onChange: ((code: string) => void) | undefined,
customExtensions: import('@codemirror/state').Extension[],
shrimp: Shrimp
) => {
const extensions = [
basicSetup,
shrimpTheme,
createShrimpDiagnostics(shrimp),
createShrimpCompletions(shrimp),
shrimpHighlighter,
shrimpKeymap,
persistence,
...customExtensions,
]
if (onChange) {
extensions.push(
EditorView.updateListener.of((update) => {
if (update.docChanged) {
onChange(update.state.doc.toString())
}
})
)
}
new EditorView({ new EditorView({
parent: el, parent: el,
doc: getContent() || '# type some code\necho hello', doc: initialCode,
extensions: [basicSetup, shrimpTheme, shrimpDiagnostics, persistencePlugin, shrimpHighlighter], extensions,
}) })
}
// Mount when running in browser // Trigger onChange with initial content
if (typeof document !== 'undefined') { onChange?.(initialCode)
render(<Editor />, document.getElementById('root')!)
} }

View File

@ -0,0 +1,6 @@
import { Editor } from '#editor/editor'
import { render } from 'hono/jsx/dom'
if (typeof document !== 'undefined') {
render(<Editor initialCode={'# type some code'} />, document.getElementById('root')!)
}

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" href="../editor.css" />
</head>
<body>
<div id="root"></div>
<script type="module" src="./frontend.tsx"></script>
</body>
</html>

View File

@ -2,6 +2,7 @@ import { EditorView, Decoration, DecorationSet, ViewPlugin, ViewUpdate } from '@
import { RangeSetBuilder } from '@codemirror/state' import { RangeSetBuilder } from '@codemirror/state'
import { Shrimp } from '#/index' import { Shrimp } from '#/index'
import { type SyntaxNode } from '#parser/node' import { type SyntaxNode } from '#parser/node'
import { log } from '#utils/utils'
const shrimp = new Shrimp() const shrimp = new Shrimp()
@ -14,9 +15,9 @@ export const shrimpHighlighter = ViewPlugin.fromClass(
} }
update(update: ViewUpdate) { update(update: ViewUpdate) {
if (update.docChanged) { if (!update.docChanged) return
this.decorations = this.highlight(update.view)
} this.decorations = this.highlight(update.view)
} }
highlight(view: EditorView): DecorationSet { highlight(view: EditorView): DecorationSet {
@ -30,7 +31,8 @@ export const shrimpHighlighter = ViewPlugin.fromClass(
tree.iterate({ tree.iterate({
enter: (node) => { enter: (node) => {
const cls = tokenStyles[node.type.name] const cls = tokenStyles[node.type.name]
if (cls && isLeaf(node)) { const isLeaf = node.children.length === 0
if (cls && isLeaf) {
decorations.push({ from: node.from, to: node.to, class: cls }) decorations.push({ from: node.from, to: node.to, class: cls })
} }
}, },
@ -42,8 +44,8 @@ export const shrimpHighlighter = ViewPlugin.fromClass(
for (const d of decorations) { for (const d of decorations) {
builder.add(d.from, d.to, Decoration.mark({ class: d.class })) builder.add(d.from, d.to, Decoration.mark({ class: d.class }))
} }
} catch { } catch (error) {
// Parse failed, no highlighting log('Parsing error in highlighter', error)
} }
return builder.finish() return builder.finish()
@ -69,8 +71,3 @@ const tokenStyles: Record<string, string> = {
operator: 'tok-operator', operator: 'tok-operator',
Regex: 'tok-regex', Regex: 'tok-regex',
} }
// Check if node is a leaf (should be highlighted)
const isLeaf = (node: SyntaxNode): boolean => {
return node.children.length === 0
}

View File

@ -1,23 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
</head>
<body>
<div id="root"></div>
<style>
body {
margin: 0;
padding: 0;
}
#root {
margin: 0;
padding: 0;
height: 100vh;
width: 100vw;
}
</style>
<script type="module" src="./editor.tsx"></script>
</body>
</html>

1
src/editor/index.ts Normal file
View File

@ -0,0 +1 @@
export { Editor } from './editor'

8
src/editor/keymap.ts Normal file
View File

@ -0,0 +1,8 @@
import { keymap } from '@codemirror/view'
import { acceptCompletion } from '@codemirror/autocomplete'
import { indentWithTab } from '@codemirror/commands'
export const shrimpKeymap = keymap.of([
{ key: 'Tab', run: acceptCompletion },
indentWithTab,
])

View File

@ -1,6 +1,6 @@
import { ViewPlugin, ViewUpdate } from '@codemirror/view' import { ViewPlugin, ViewUpdate } from '@codemirror/view'
export const persistencePlugin = ViewPlugin.fromClass( export const persistence = ViewPlugin.fromClass(
class { class {
saveTimeout?: ReturnType<typeof setTimeout> saveTimeout?: ReturnType<typeof setTimeout>
@ -17,13 +17,13 @@ export const persistencePlugin = ViewPlugin.fromClass(
destroy() { destroy() {
if (this.saveTimeout) clearTimeout(this.saveTimeout) if (this.saveTimeout) clearTimeout(this.saveTimeout)
} }
}, }
) )
export const getContent = () => { export const getContent = () => {
return localStorage.getItem('shrimp-editor-content') || '' return localStorage.getItem('shrimp-editor-content') || ''
} }
const setContent = (data: string) => { export const setContent = (data: string) => {
localStorage.setItem('shrimp-editor-content', data) localStorage.setItem('shrimp-editor-content', data)
} }

View File

@ -12,11 +12,12 @@ export const shrimpTheme = EditorView.theme(
fontFamily: '"Pixeloid Mono", "Courier New", monospace', fontFamily: '"Pixeloid Mono", "Courier New", monospace',
caretColor: 'var(--caret)', caretColor: 'var(--caret)',
padding: '0px', padding: '0px',
borderTop: '1px solid var(--bg-editor)',
}, },
'&.cm-focused .cm-cursor': { '&.cm-focused .cm-cursor': {
borderLeftColor: 'var(--caret)', borderLeftColor: 'var(--caret)',
}, },
'&.cm-focused .cm-selectionBackground, ::selection': { '.cm-selectionLayer .cm-selectionBackground': {
backgroundColor: 'var(--bg-selection)', backgroundColor: 'var(--bg-selection)',
}, },
'.cm-editor': { '.cm-editor': {
@ -31,8 +32,20 @@ export const shrimpTheme = EditorView.theme(
backgroundColor: 'var(--color-string)', backgroundColor: 'var(--color-string)',
}, },
'.cm-activeLine': { '.cm-activeLine': {
backgroundColor: 'var(--bg-active-line)', backgroundColor: 'rgba(255, 255, 255, 0.03)',
}, },
// Token highlighting
'.tok-keyword': { color: 'var(--color-keyword)' },
'.tok-string': { color: 'var(--color-string)' },
'.tok-number': { color: 'var(--color-number)' },
'.tok-bool': { color: 'var(--color-bool)' },
'.tok-null': { color: 'var(--color-number)', fontStyle: 'italic' },
'.tok-identifier': { color: 'var(--color-function)' },
'.tok-variable-def': { color: 'var(--color-variable-def)' },
'.tok-comment': { color: 'var(--ansi-bright-black)', fontStyle: 'italic' },
'.tok-operator': { color: 'var(--color-operator)' },
'.tok-regex': { color: 'var(--color-regex)' },
}, },
{ dark: true } { dark: true }
) )

View File

@ -1,4 +1,4 @@
const DEBUG = process.env.DEBUG || false import { isDebug } from '#utils/utils'
export type Token = { export type Token = {
type: TokenType type: TokenType
@ -169,11 +169,11 @@ export class Scanner {
from, from,
to, to,
}, },
valueTokens.has(type) ? { value: this.input.slice(from, to) } : {}, valueTokens.has(type) ? { value: this.input.slice(from, to) } : {}
), )
) )
if (DEBUG) { if (isDebug()) {
const tok = this.tokens.at(-1) const tok = this.tokens.at(-1)
console.log(`≫ PUSH(${from},${to})`, TokenType[tok?.type || 0], '—', tok?.value) console.log(`≫ PUSH(${from},${to})`, TokenType[tok?.type || 0], '—', tok?.value)
} }