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",
"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,
"type": "module",
"scripts": {
"editor": "bun --hot src/editor/server.tsx",
"editor": "bun --hot src/editor/example/server.tsx",
"repl": "bun bin/repl",
"update-reef": "rm -rf ~/.bun/install/cache/ && rm bun.lock && bun update reefvm",
"cli:install": "ln -s \"$(pwd)/bin/shrimp\" ~/.bun/bin/shrimp",

View File

@ -1,2 +1,102 @@
// Autocomplete for Shrimp
// TODO: Keywords and prelude function names
import { autocompletion, type CompletionContext, type Completion } from '@codemirror/autocomplete'
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,9 +2,8 @@ import { linter, type Diagnostic } from '@codemirror/lint'
import { Shrimp } from '#/index'
import { CompilerError } from '#compiler/compilerError'
const shrimp = new Shrimp()
export const shrimpDiagnostics = linter((view) => {
export const createShrimpDiagnostics = (shrimp: Shrimp) =>
linter((view) => {
const code = view.state.doc.toString()
const diagnostics: Diagnostic[] = []

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 { render } from 'hono/jsx/dom'
import { Shrimp } from '#/index'
import { shrimpTheme } from './theme'
import { shrimpDiagnostics } from './diagnostics'
import { persistencePlugin, getContent } from './persistence'
import { createShrimpDiagnostics } from './diagnostics'
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 (
<div
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) => {
new EditorView({
parent: el,
doc: getContent() || '# type some code\necho hello',
extensions: [basicSetup, shrimpTheme, shrimpDiagnostics, persistencePlugin, shrimpHighlighter],
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())
}
})
)
}
// Mount when running in browser
if (typeof document !== 'undefined') {
render(<Editor />, document.getElementById('root')!)
new EditorView({
parent: el,
doc: initialCode,
extensions,
})
// Trigger onChange with initial content
onChange?.(initialCode)
}

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 { Shrimp } from '#/index'
import { type SyntaxNode } from '#parser/node'
import { log } from '#utils/utils'
const shrimp = new Shrimp()
@ -14,10 +15,10 @@ export const shrimpHighlighter = ViewPlugin.fromClass(
}
update(update: ViewUpdate) {
if (update.docChanged) {
if (!update.docChanged) return
this.decorations = this.highlight(update.view)
}
}
highlight(view: EditorView): DecorationSet {
const builder = new RangeSetBuilder<Decoration>()
@ -30,7 +31,8 @@ export const shrimpHighlighter = ViewPlugin.fromClass(
tree.iterate({
enter: (node) => {
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 })
}
},
@ -42,8 +44,8 @@ export const shrimpHighlighter = ViewPlugin.fromClass(
for (const d of decorations) {
builder.add(d.from, d.to, Decoration.mark({ class: d.class }))
}
} catch {
// Parse failed, no highlighting
} catch (error) {
log('Parsing error in highlighter', error)
}
return builder.finish()
@ -69,8 +71,3 @@ const tokenStyles: Record<string, string> = {
operator: 'tok-operator',
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'
export const persistencePlugin = ViewPlugin.fromClass(
export const persistence = ViewPlugin.fromClass(
class {
saveTimeout?: ReturnType<typeof setTimeout>
@ -17,13 +17,13 @@ export const persistencePlugin = ViewPlugin.fromClass(
destroy() {
if (this.saveTimeout) clearTimeout(this.saveTimeout)
}
},
}
)
export const getContent = () => {
return localStorage.getItem('shrimp-editor-content') || ''
}
const setContent = (data: string) => {
export const setContent = (data: string) => {
localStorage.setItem('shrimp-editor-content', data)
}

View File

@ -12,11 +12,12 @@ export const shrimpTheme = EditorView.theme(
fontFamily: '"Pixeloid Mono", "Courier New", monospace',
caretColor: 'var(--caret)',
padding: '0px',
borderTop: '1px solid var(--bg-editor)',
},
'&.cm-focused .cm-cursor': {
borderLeftColor: 'var(--caret)',
},
'&.cm-focused .cm-selectionBackground, ::selection': {
'.cm-selectionLayer .cm-selectionBackground': {
backgroundColor: 'var(--bg-selection)',
},
'.cm-editor': {
@ -31,8 +32,20 @@ export const shrimpTheme = EditorView.theme(
backgroundColor: 'var(--color-string)',
},
'.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 }
)

View File

@ -1,4 +1,4 @@
const DEBUG = process.env.DEBUG || false
import { isDebug } from '#utils/utils'
export type Token = {
type: TokenType
@ -169,11 +169,11 @@ export class Scanner {
from,
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)
console.log(`≫ PUSH(${from},${to})`, TokenType[tok?.type || 0], '—', tok?.value)
}