diff --git a/package.json b/package.json index 758b491..3bd935d 100644 --- a/package.json +++ b/package.json @@ -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": { - "dev": "bun --hot src/server/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", @@ -33,4 +37,4 @@ "singleQuote": true, "printWidth": 100 } -} +} \ No newline at end of file diff --git a/src/editor/commands.ts b/src/editor/commands.ts deleted file mode 100644 index 021aefb..0000000 --- a/src/editor/commands.ts +++ /dev/null @@ -1,261 +0,0 @@ -export type CommandShape = { - command: string - description?: string - args: ArgShape[] - execute: string | ((...args: any[]) => any) -} - -type ArgShape = { - name: string - type: T - description?: string - optional?: boolean - default?: ArgTypeMap[T] -} - -type ArgTypeMap = { - string: string - number: number - boolean: boolean -} - -const commandShapes: CommandShape[] = [ - { - command: 'ls', - description: 'List the contents of a directory', - execute: './commands/ls.ts', - args: [ - { name: 'path', type: 'string', description: 'The path to list' }, - { name: 'all', type: 'boolean', description: 'Show hidden files', default: false }, - { name: 'long', type: 'boolean', description: 'List in long format', default: false }, - { - name: 'short-names', - type: 'boolean', - description: 'Only print file names', - default: false, - }, - { name: 'full-paths', type: 'boolean', description: 'Display full paths', default: false }, - ], - }, - - { - command: 'cd', - description: 'Change the current working directory', - execute: './commands/cd.ts', - args: [{ name: 'path', type: 'string', description: 'The path to change to' }], - }, - - { - command: 'cp', - description: 'Copy files or directories', - execute: './commands/cp.ts', - args: [ - { name: 'source', type: 'string', description: 'Source file or directory' }, - { name: 'destination', type: 'string', description: 'Destination path' }, - { name: 'recursive', type: 'boolean', description: 'Copy recursively', default: false }, - { name: 'verbose', type: 'boolean', description: 'Verbose output', default: false }, - ], - }, - - { - command: 'mv', - description: 'Move files or directories', - execute: './commands/mv.ts', - args: [ - { name: 'source', type: 'string', description: 'Source file or directory' }, - { name: 'destination', type: 'string', description: 'Destination path' }, - { name: 'verbose', type: 'boolean', description: 'Verbose output', default: false }, - ], - }, - - { - command: 'rm', - description: 'Remove files or directories', - execute: './commands/rm.ts', - args: [ - { name: 'path', type: 'string', description: 'Path to remove' }, - { name: 'recursive', type: 'boolean', description: 'Remove recursively', default: false }, - { name: 'force', type: 'boolean', description: 'Force removal', default: false }, - { name: 'verbose', type: 'boolean', description: 'Verbose output', default: false }, - ], - }, - - { - command: 'mkdir', - description: 'Create directories', - execute: './commands/mkdir.ts', - args: [ - { name: 'path', type: 'string', description: 'Directory path to create' }, - { name: 'verbose', type: 'boolean', description: 'Verbose output', default: false }, - ], - }, - - { - command: 'touch', - description: 'Create empty files or update timestamps', - execute: './commands/touch.ts', - args: [ - { name: 'path', type: 'string', description: 'File path to touch' }, - { name: 'access', type: 'boolean', description: 'Update access time only', default: false }, - { - name: 'modified', - type: 'boolean', - description: 'Update modified time only', - default: false, - }, - ], - }, - - { - command: 'echo', - description: 'Display a string', - execute: './commands/echo.ts', - args: [ - { name: 'text', type: 'string', description: 'Text to display' }, - { name: 'no-newline', type: 'boolean', description: "Don't append newline", default: false }, - ], - }, - - { - command: 'cat', - description: 'Display file contents', - execute: './commands/cat.ts', - args: [ - { name: 'path', type: 'string', description: 'File to display' }, - { name: 'numbered', type: 'boolean', description: 'Show line numbers', default: false }, - ], - }, - - { - command: 'head', - description: 'Show first lines of input', - execute: './commands/head.ts', - args: [ - { name: 'path', type: 'string', description: 'File to read from' }, - { name: 'lines', type: 'number', description: 'Number of lines', default: 10 }, - ], - }, - - { - command: 'tail', - description: 'Show last lines of input', - execute: './commands/tail.ts', - args: [ - { name: 'path', type: 'string', description: 'File to read from' }, - { name: 'lines', type: 'number', description: 'Number of lines', default: 10 }, - { name: 'follow', type: 'boolean', description: 'Follow file changes', default: false }, - ], - }, - - { - command: 'grep', - description: 'Search for patterns in text', - execute: './commands/grep.ts', - args: [ - { name: 'pattern', type: 'string', description: 'Pattern to search for' }, - { - name: 'ignore-case', - type: 'boolean', - description: 'Case insensitive search', - default: false, - }, - { name: 'invert-match', type: 'boolean', description: 'Invert match', default: false }, - { name: 'line-number', type: 'boolean', description: 'Show line numbers', default: false }, - ], - }, - - { - command: 'sort', - description: 'Sort input', - execute: './commands/sort.ts', - args: [ - { name: 'reverse', type: 'boolean', description: 'Sort in reverse order', default: false }, - { - name: 'ignore-case', - type: 'boolean', - description: 'Case insensitive sort', - default: false, - }, - { name: 'numeric', type: 'boolean', description: 'Numeric sort', default: false }, - ], - }, - - { - command: 'uniq', - description: 'Filter out repeated lines', - execute: './commands/uniq.ts', - args: [ - { name: 'count', type: 'boolean', description: 'Show count of occurrences', default: false }, - { - name: 'repeated', - type: 'boolean', - description: 'Show only repeated lines', - default: false, - }, - { name: 'unique', type: 'boolean', description: 'Show only unique lines', default: false }, - ], - }, - - { - command: 'select', - description: 'Select specific columns from data', - execute: './commands/select.ts', - args: [{ name: 'columns', type: 'string', description: 'Columns to select' }], - }, - - { - command: 'where', - description: 'Filter data based on conditions', - execute: './commands/where.ts', - args: [{ name: 'condition', type: 'string', description: 'Filter condition' }], - }, - - { - command: 'group-by', - description: 'Group data by column values', - execute: './commands/group-by.ts', - args: [{ name: 'column', type: 'string', description: 'Column to group by' }], - }, - - { - command: 'ps', - description: 'List running processes', - execute: './commands/ps.ts', - args: [ - { name: 'long', type: 'boolean', description: 'Show detailed information', default: false }, - ], - }, - - { - command: 'sys', - description: 'Show system information', - execute: './commands/sys.ts', - args: [], - }, - - { - command: 'which', - description: 'Find the location of a command', - execute: './commands/which.ts', - args: [ - { name: 'command', type: 'string', description: 'Command to locate' }, - { name: 'all', type: 'boolean', description: 'Show all matches', default: false }, - ], - }, -] as const - -let commandSource = () => commandShapes -export const setCommandSource = (fn: () => CommandShape[]) => { - commandSource = fn -} - -export const resetCommandSource = () => { - commandSource = () => commandShapes -} - -export const matchingCommands = (prefix: string) => { - const match = commandSource().find((cmd) => cmd.command === prefix) - const partialMatches = commandSource().filter((cmd) => cmd.command.startsWith(prefix)) - - return { match, partialMatches } -} diff --git a/src/editor/completions.ts b/src/editor/completions.ts new file mode 100644 index 0000000..a090ac5 --- /dev/null +++ b/src/editor/completions.ts @@ -0,0 +1,87 @@ +import { autocompletion, type CompletionContext, type Completion } from '@codemirror/autocomplete' +import { Shrimp } 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, +})) + +export const createShrimpCompletions = (shrimp: Shrimp) => { + // Build completions from all names in the shrimp scope + const scopeNames = shrimp.vm.scope.vars() + console.log(`🌭`, shrimp.vm.vars()) + + const functionCompletions: Completion[] = scopeNames.map((name) => { + const value = shrimp.vm.scope.get(name) + let type + if (value?.type === 'function' || value?.type === 'native') { + type = 'function' + } else if (value?.type === 'dict') { + type = 'namespace' + } else { + type = 'variable' + } + + return { type, label: 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 || typeof module !== 'object') return [] + + return Object.keys(module).map((m) => ({ label: m, type: 'function' })) + } + + 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], + }) +} diff --git a/src/editor/diagnostics.ts b/src/editor/diagnostics.ts new file mode 100644 index 0000000..18c3a86 --- /dev/null +++ b/src/editor/diagnostics.ts @@ -0,0 +1,24 @@ +import { linter, type Diagnostic } from '@codemirror/lint' +import { Shrimp } from '#/index' +import { CompilerError } from '#compiler/compilerError' + +export const createShrimpDiagnostics = (shrimp: Shrimp) => + linter((view) => { + const code = view.state.doc.toString() + const diagnostics: Diagnostic[] = [] + + try { + shrimp.parse(code) + } catch (err) { + if (err instanceof CompilerError) { + diagnostics.push({ + from: err.from, + to: err.to, + severity: 'error', + message: err.message, + }) + } + } + + return diagnostics + }) diff --git a/src/editor/editor.css b/src/editor/editor.css index bc09fd9..84282f8 100644 --- a/src/editor/editor.css +++ b/src/editor/editor.css @@ -1,53 +1,78 @@ -#output { - flex: 1; - background: var(--bg-output); - color: var(--text-output); - padding: 20px; - overflow-y: auto; - white-space: pre-wrap; +: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; } -#status-bar { - height: 30px; - background: var(--bg-status-bar); - color: var(--text-status); - display: flex; - align-items: center; - padding: 0 10px; - font-size: 14px; - border-top: 3px solid var(--bg-status-border); - border-bottom: 3px solid var(--bg-status-border); - - display: flex; - justify-content: space-between; -} - -#status-bar .left, -#status-bar .right { - display: flex; - justify-content: center; - gap: 2rem; -} - -#status-bar .multiline { - display: flex; - - .dot { - padding-top: 1px; - margin-right: 4px; - } - - .active { - color: var(--color-string); - } - - .inactive { - color: inherit; - } -} - -.syntax-error { - text-decoration: underline dotted var(--color-error); -} +#root { + height: 100vh; +} \ No newline at end of file diff --git a/src/editor/editor.tsx b/src/editor/editor.tsx index 91cc130..273fe34 100644 --- a/src/editor/editor.tsx +++ b/src/editor/editor.tsx @@ -1,140 +1,70 @@ -import { EditorView } from '@codemirror/view' -import { asciiEscapeToHtml, assertNever, log, toElement } from '#utils/utils' -import { Signal } from '#utils/signal' -import { getContent } from '#editor/plugins/persistence' -import type { HtmlEscapedString } from 'hono/utils/html' -import { connectToNose, noseSignals } from '#editor/noseClient' -import type { Value } from 'reefvm' -import { Compartment } from '@codemirror/state' -import { lineNumbers } from '@codemirror/view' -import { shrimpSetup } from '#editor/plugins/shrimpSetup' +import { EditorView, basicSetup } from 'codemirror' +import { Shrimp } from '#/index' -import '#editor/editor.css' +import { shrimpTheme } from './theme' +import { createShrimpDiagnostics } from './diagnostics' +import { shrimpHighlighter } from './highlighter' +import { createShrimpCompletions } from './completions' +import { shrimpKeymap } from './keymap' +import { getContent, persistence } from './persistence' -const lineNumbersCompartment = new Compartment() +type EditorProps = { + initialCode?: string + onChange?: (code: string) => void + extensions?: import('@codemirror/state').Extension[] + shrimp?: Shrimp +} -connectToNose() - -export const outputSignal = new Signal() -export const errorSignal = new Signal() -export const multilineModeSignal = new Signal() - -export const Editor = () => { +export const Editor = ({ + initialCode = '', + onChange, + extensions: customExtensions = [], + shrimp = new Shrimp(), +}: EditorProps) => { return ( - <> -
{ - if (ref?.querySelector('.cm-editor')) return - const view = new EditorView({ - parent: ref, - doc: getContent(), - extensions: shrimpSetup(lineNumbersCompartment), - }) - - multilineModeSignal.connect((isMultiline) => { - view.dispatch({ - effects: lineNumbersCompartment.reconfigure(isMultiline ? lineNumbers() : []), - }) - }) - - requestAnimationFrame(() => view.focus()) - }} - /> -
-
-
-
-
-
- +
{ + if (!el?.querySelector('.cm-editor')) + createEditorView(el, getContent() ?? initialCode, onChange, customExtensions, shrimp) + }} + /> ) } -noseSignals.connect((message) => { - if (message.type === 'error') { - log.error(`Nose error: ${message.data}`) - errorSignal.emit(`Nose error: ${message.data}`) - } else if (message.type === 'reef-output') { - const x = outputSignal.emit(message.data) - } else if (message.type === 'connected') { - outputSignal.emit(`╞ Connected to Nose VM`) +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()) + } + }) + ) } -}) -outputSignal.connect((value) => { - const el = document.querySelector('#output')! - el.innerHTML = '' - el.innerHTML = asciiEscapeToHtml(valueToString(value)) -}) - -errorSignal.connect((error) => { - const el = document.querySelector('#output')! - el.innerHTML = '' - el.classList.add('error') - el.innerHTML = asciiEscapeToHtml(error) -}) - -type StatusBarMessage = { - side: 'left' | 'right' - message: string | Promise - className: string - order?: number -} -export const statusBarSignal = new Signal() -statusBarSignal.connect(async ({ side, message, className, order }) => { - document.querySelector(`#status-bar .${className}`)?.remove() - - const sideEl = document.querySelector(`#status-bar .${side}`)! - const messageEl = ( -
- {await message} -
- ) - - // Now go through the nodes and put it in the right spot based on order. Higher number means further right - const nodes = Array.from(sideEl.childNodes) - const index = nodes.findIndex((node) => { - if (!(node instanceof HTMLElement)) return false - return Number(node.dataset.order) > (order ?? 0) + new EditorView({ + parent: el, + doc: initialCode, + extensions, }) - if (index === -1) { - sideEl.appendChild(toElement(messageEl)) - } else { - sideEl.insertBefore(toElement(messageEl), nodes[index]!) - } -}) - -const valueToString = (value: Value | string): string => { - if (typeof value === 'string') { - return value - } - - switch (value.type) { - case 'null': - return 'null' - case 'boolean': - return value.value ? 'true' : 'false' - case 'number': - return value.value.toString() - case 'string': - return value.value - case 'array': - return `${value.value.map(valueToString).join('\n')}` - case 'dict': { - const entries = Array.from(value.value.entries()).map( - ([key, val]) => `"${key}": ${valueToString(val)}`, - ) - return `{${entries.join(', ')}}` - } - case 'regex': - return `/${value.value.source}/` - case 'function': - return `` - case 'native': - return `` - default: - assertNever(value) - return `` - } + // Trigger onChange with initial content + onChange?.(initialCode) } diff --git a/src/editor/example/frontend.tsx b/src/editor/example/frontend.tsx new file mode 100644 index 0000000..da34265 --- /dev/null +++ b/src/editor/example/frontend.tsx @@ -0,0 +1,6 @@ +import { Editor } from '#editor/editor' +import { render } from 'hono/jsx/dom' + +if (typeof document !== 'undefined') { + render(, document.getElementById('root')!) +} diff --git a/src/editor/example/index.html b/src/editor/example/index.html new file mode 100644 index 0000000..8ef8eba --- /dev/null +++ b/src/editor/example/index.html @@ -0,0 +1,11 @@ + + + + + + + +
+ + + diff --git a/src/editor/example/server.tsx b/src/editor/example/server.tsx new file mode 100644 index 0000000..dbe8621 --- /dev/null +++ b/src/editor/example/server.tsx @@ -0,0 +1,14 @@ +import index from './index.html' + +const server = Bun.serve({ + port: 3000, + routes: { + '/': index, + }, + development: { + hmr: true, + console: true, + }, +}) + +console.log(`Editor running at ${server.url}`) diff --git a/src/editor/highlighter.ts b/src/editor/highlighter.ts new file mode 100644 index 0000000..93e1f3d --- /dev/null +++ b/src/editor/highlighter.ts @@ -0,0 +1,73 @@ +import { EditorView, Decoration, DecorationSet, ViewPlugin, ViewUpdate } from '@codemirror/view' +import { RangeSetBuilder } from '@codemirror/state' +import { Shrimp } from '#/index' +import { type SyntaxNode } from '#parser/node' +import { log } from '#utils/utils' + +const shrimp = new Shrimp() + +export const shrimpHighlighter = ViewPlugin.fromClass( + class { + decorations: DecorationSet + + constructor(view: EditorView) { + this.decorations = this.highlight(view) + } + + update(update: ViewUpdate) { + if (!update.docChanged) return + + this.decorations = this.highlight(update.view) + } + + highlight(view: EditorView): DecorationSet { + const builder = new RangeSetBuilder() + const code = view.state.doc.toString() + + try { + const tree = shrimp.parse(code) + const decorations: { from: number; to: number; class: string }[] = [] + + tree.iterate({ + enter: (node) => { + const cls = tokenStyles[node.type.name] + const isLeaf = node.children.length === 0 + if (cls && isLeaf) { + decorations.push({ from: node.from, to: node.to, class: cls }) + } + }, + }) + + // Sort by position (required by RangeSetBuilder) + decorations.sort((a, b) => a.from - b.from) + + for (const d of decorations) { + builder.add(d.from, d.to, Decoration.mark({ class: d.class })) + } + } catch (error) { + log('Parsing error in highlighter', error) + } + + return builder.finish() + } + }, + { + decorations: (v) => v.decorations, + } +) + +// Map node types to CSS classes +const tokenStyles: Record = { + keyword: 'tok-keyword', + String: 'tok-string', + StringFragment: 'tok-string', + CurlyString: 'tok-string', + Number: 'tok-number', + Boolean: 'tok-bool', + Null: 'tok-null', + Identifier: 'tok-identifier', + AssignableIdentifier: 'tok-variable-def', + Comment: 'tok-comment', + operator: 'tok-operator', + Regex: 'tok-regex', +} diff --git a/src/editor/index.ts b/src/editor/index.ts new file mode 100644 index 0000000..eac45fe --- /dev/null +++ b/src/editor/index.ts @@ -0,0 +1 @@ +export { Editor } from './editor' diff --git a/src/editor/keymap.ts b/src/editor/keymap.ts new file mode 100644 index 0000000..7a3e2e7 --- /dev/null +++ b/src/editor/keymap.ts @@ -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, +]) diff --git a/src/editor/noseClient.ts b/src/editor/noseClient.ts deleted file mode 100644 index a581ae7..0000000 --- a/src/editor/noseClient.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Signal } from '#utils/signal' -import type { Bytecode, Value } from 'reefvm' -let ws: WebSocket - -type IncomingMessage = - | { type: 'connected' } - | { type: 'ping'; data: number } - | { type: 'commands'; data: number } - | { - type: 'apps' - data: { - name: string - type: 'browser' | 'server' - }[] - } - | { - type: 'session:start' - data: { - NOSE_DIR: string - cwd: string - hostname: string - mode: string - project: string - } - } - | { type: 'reef-output'; data: Value } - | { type: 'error'; data: string } - -export const noseSignals = new Signal() - -export const connectToNose = (url: string = 'ws://localhost:3000/ws') => { - ws = new WebSocket(url) - ws.onopen = () => noseSignals.emit({ type: 'connected' }) - - ws.onmessage = (event) => { - const message = JSON.parse(event.data) - noseSignals.emit(message) - } - - ws.onerror = (event) => { - console.error(`💥WebSocket error:`, event) - } - - ws.onclose = () => { - console.log(`🚪 Connection closed`) - } -} - -let id = 0 -export const sendToNose = (code: Bytecode) => { - if (!ws) { - throw new Error('WebSocket is not connected.') - } else if (ws.readyState !== WebSocket.OPEN) { - throw new Error(`WebSocket is not open, current status is ${ws.readyState}.`) - } - - id += 1 - ws.send(JSON.stringify({ type: 'reef-bytecode', data: code, id })) -} diff --git a/src/editor/plugins/persistence.ts b/src/editor/persistence.ts similarity index 86% rename from src/editor/plugins/persistence.ts rename to src/editor/persistence.ts index 4654593..d729b26 100644 --- a/src/editor/plugins/persistence.ts +++ b/src/editor/persistence.ts @@ -1,6 +1,6 @@ import { ViewPlugin, ViewUpdate } from '@codemirror/view' -export const persistencePlugin = ViewPlugin.fromClass( +export const persistence = ViewPlugin.fromClass( class { saveTimeout?: ReturnType @@ -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) } diff --git a/src/editor/plugins/catchErrors.ts b/src/editor/plugins/catchErrors.ts deleted file mode 100644 index 1cc9e68..0000000 --- a/src/editor/plugins/catchErrors.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { errorSignal } from '#editor/editor' -import { EditorView } from '@codemirror/view' - -export const catchErrors = EditorView.exceptionSink.of((exception) => { - console.error('CodeMirror error:', exception) - errorSignal.emit( - `Editor error: ${exception instanceof Error ? exception.message : String(exception)}`, - ) -}) diff --git a/src/editor/plugins/debugTags.ts b/src/editor/plugins/debugTags.ts deleted file mode 100644 index e44f24e..0000000 --- a/src/editor/plugins/debugTags.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { EditorView, ViewPlugin, ViewUpdate } from '@codemirror/view' -import { syntaxTree } from '@codemirror/language' -import { statusBarSignal } from '#editor/editor' - -export const debugTags = ViewPlugin.fromClass( - class { - update(update: ViewUpdate) { - if (update.docChanged || update.selectionSet || update.geometryChanged) { - this.updateStatusBar(update.view) - } - } - - updateStatusBar(view: EditorView) { - const pos = view.state.selection.main.head + 1 - 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.reverse().slice(1).join(' > ') : 'No nodes' - statusBarSignal.emit({ - side: 'right', - message: debugText, - className: 'debug-tags', - order: -1, - }) - } - }, -) diff --git a/src/editor/plugins/errors.ts b/src/editor/plugins/errors.ts deleted file mode 100644 index 5cb19b2..0000000 --- a/src/editor/plugins/errors.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { parser } from '#parser/shrimp' -import type { Timeout } from '#utils/utils' -import { Range } from '@codemirror/state' -import { - Decoration, - EditorView, - ViewPlugin, - ViewUpdate, - type DecorationSet, -} from '@codemirror/view' - -export const shrimpErrors = ViewPlugin.fromClass( - class { - timeout?: Timeout - decorations: DecorationSet = Decoration.none - - constructor(view: EditorView) { - this.updateErrors(view) - } - - update(update: ViewUpdate) { - if (update.docChanged) { - this.debounceUpdate(update.view) - } - } - - updateErrors(view: EditorView) { - this.decorations = Decoration.none - try { - const decorations: Range[] = [] - const tree = parser.parse(view.state.doc.toString()) - tree.iterate({ - enter: (node) => { - if (!node.type.isError) return - - // Skip empty error nodes - if (node.from === node.to) return - - const decoration = Decoration.mark({ - class: 'syntax-error', - attributes: { title: 'COREY REPLACE THIS' }, - }).range(node.from, node.to) - decorations.push(decoration) - }, - }) - - this.decorations = Decoration.set(decorations) - // requestAnimationFrame(() => view.dispatch({})) - } catch (e) { - console.error('🙈 Error parsing document', e) - } - } - - debounceUpdate = (view: EditorView) => { - clearTimeout(this.timeout) - this.timeout = setTimeout(() => this.updateErrors(view), 250) - } - }, - { - decorations: (v) => v.decorations, - }, -) diff --git a/src/editor/plugins/inlineHints.tsx b/src/editor/plugins/inlineHints.tsx deleted file mode 100644 index c1a130e..0000000 --- a/src/editor/plugins/inlineHints.tsx +++ /dev/null @@ -1,232 +0,0 @@ -import { - ViewPlugin, - ViewUpdate, - EditorView, - Decoration, - type DecorationSet, -} from '@codemirror/view' -import { syntaxTree } from '@codemirror/language' -import { type SyntaxNode } from '@lezer/common' -import { WidgetType } from '@codemirror/view' -import { toElement } from '#utils/utils' -import { matchingCommands } from '#editor/commands' -import * as Terms from '#parser/shrimp.terms' - -const ghostTextTheme = EditorView.theme({ - '.ghost-text': { - color: '#666', - opacity: '0.6', - fontStyle: 'italic', - }, -}) - -type Hint = { cursor: number; hintText?: string; completionText?: string } - -export const inlineHints = [ - ViewPlugin.fromClass( - class { - decorations: DecorationSet = Decoration.none - currentHint?: Hint - - update(update: ViewUpdate) { - if (!update.docChanged && !update.selectionSet) return - - this.clearHints() - let hint = this.getContext(update.view) - this.currentHint = hint - this.showHint(hint) - } - - handleTab(view: EditorView) { - if (!this.currentHint?.completionText) return false - - this.decorations = Decoration.none - view.dispatch({ - changes: { - from: this.currentHint.cursor, - insert: this.currentHint.completionText, - }, - selection: { - anchor: this.currentHint.cursor + this.currentHint.completionText.length, - }, - }) - this.currentHint = undefined - return true - } - - clearHints() { - this.currentHint = undefined - this.decorations = Decoration.none - } - - getContext(view: EditorView): Hint { - const cursor = view.state.selection.main.head - - const isCursorAtEnd = cursor === view.state.doc.length - if (!isCursorAtEnd) return { cursor } - - const token = this.getCommandContextToken(view, cursor) - if (!token) return { cursor } - - const text = view.state.doc.sliceString(token.from, token.to) - const tokenId = token.type.id - - let completionText = '' - let hintText = '' - const justSpaces = view.state.doc.sliceString(cursor - 1, cursor) === ' ' - - if (tokenId === Terms.CommandPartial) { - const { partialMatches } = matchingCommands(text) - const match = partialMatches[0] - if (match) { - completionText = match.command.slice(text.length) + ' ' - hintText = completionText - } - } else if ( - tokenId === Terms.Identifier && - token.parent?.type.id === Terms.Arg && - !justSpaces - ) { - const { availableArgs } = this.getCommandContext(view, token) - const matchingArgs = availableArgs.filter((arg) => arg.name.startsWith(text)) - const match = matchingArgs[0] - if (match) { - hintText = `${match.name.slice(text.length)}=<${match.type}>` - completionText = `${match.name.slice(text.length)}=` - } - } else if (this.containedBy(token, Terms.PartialNamedArg)) { - const { availableArgs } = this.getCommandContext(view, token) - const textWithoutEquals = text.slice(0, -1) - const matchingArgs = availableArgs.filter((arg) => arg.name == textWithoutEquals) - const match = matchingArgs[0] - if (match) { - hintText = `<${match.type}>` - completionText = 'default' in match ? `${match.default}` : '' - } - } else { - const { availableArgs } = this.getCommandContext(view, token) - const nextArg = Array.from(availableArgs)[0] - const space = justSpaces ? '' : ' ' - if (nextArg) { - hintText = `${space}${nextArg.name}=<${nextArg.type}>` - if (nextArg) { - completionText = `${space}${nextArg.name}=` - } - } - } - - return { completionText, hintText, cursor } - } - - getCommandContextToken(view: EditorView, cursor: number) { - const tree = syntaxTree(view.state) - let node = tree.resolveInner(cursor, -1) - - // If we're in a CommandCall, return the token before cursor - if (this.containedBy(node, Terms.CommandCall)) { - return tree.resolveInner(cursor, -1) - } - - // If we're in Program, look backward - while (node.name === 'Program' && cursor > 0) { - cursor -= 1 - node = tree.resolveInner(cursor, -1) - if (this.containedBy(node, Terms.CommandCall)) { - return tree.resolveInner(cursor, -1) - } - } - } - - containedBy(node: SyntaxNode, nodeId: number): SyntaxNode | undefined { - let current: SyntaxNode | undefined = node - - while (current) { - if (current.type.id === nodeId) { - return current - } - - current = current.parent ?? undefined - } - } - - showHint(hint: Hint) { - if (!hint.hintText) return - - const widget = new GhostTextWidget(hint.hintText) - const afterCursor = 1 - const decoration = Decoration.widget({ widget, side: afterCursor }).range(hint.cursor) - this.decorations = Decoration.set([decoration]) - } - - getCommandContext(view: EditorView, currentToken: SyntaxNode) { - let commandCallNode = currentToken.parent - while (commandCallNode?.type.name !== 'CommandCall') { - if (!commandCallNode) { - throw new Error('No CommandCall parent found, must be an error in the grammar') - } - commandCallNode = commandCallNode.parent - } - - const commandToken = commandCallNode.firstChild - if (!commandToken) { - throw new Error('CommandCall has no children, must be an error in the grammar') - } - - const commandText = view.state.doc.sliceString(commandToken.from, commandToken.to) - const { match: commandShape } = matchingCommands(commandText) - if (!commandShape) { - throw new Error(`No command shape found for command "${commandText}"`) - } - - let availableArgs = [...commandShape.args] - - // Walk through all NamedArg children - let child = commandToken.nextSibling - - while (child) { - console.log('child', child.type.name, child.to - child.from) - if (child.type.id === Terms.NamedArg) { - const argName = child.firstChild // Should be the Identifier - if (argName) { - const argText = view.state.doc.sliceString(argName.from, argName.to - 1) - availableArgs = availableArgs.filter((arg) => arg.name !== argText) - } - } else if (child.type.id == Terms.Arg) { - const hasSpaceAfter = view.state.doc.sliceString(child.to, child.to + 1) === ' ' - if (hasSpaceAfter) { - availableArgs.shift() - } - } - - child = child.nextSibling - } - - return { commandShape, availableArgs } - } - }, - { - decorations: (v) => v.decorations, - eventHandlers: { - keydown(event, view) { - if (event.key === 'Tab') { - event.preventDefault() - const plugin = view.plugin(inlineHints[0]! as ViewPlugin) - plugin?.handleTab(view) - } - }, - }, - }, - ), - ghostTextTheme, -] - -class GhostTextWidget extends WidgetType { - constructor(private text: string) { - super() - } - - toDOM() { - const el = {this.text} - return toElement(el) - } -} diff --git a/src/editor/plugins/keymap.tsx b/src/editor/plugins/keymap.tsx deleted file mode 100644 index 6c77f14..0000000 --- a/src/editor/plugins/keymap.tsx +++ /dev/null @@ -1,184 +0,0 @@ -import { multilineModeSignal, outputSignal } from '#editor/editor' -import { printBytecodeOutput, printParserOutput, runCode } from '#editor/runCode' -import { EditorState } from '@codemirror/state' -import { keymap } from '@codemirror/view' - -let multilineMode = false - -const customKeymap = keymap.of([ - { - key: 'Enter', - run: (view) => { - if (multilineMode) return false - - const input = view.state.doc.toString() - history.push(input) - runCode(input) - view.dispatch({ - changes: { from: 0, to: view.state.doc.length, insert: '' }, - selection: { anchor: 0 }, - }) - return true - }, - }, - - { - key: 'Shift-Enter', - run: (view) => { - if (multilineMode) { - const input = view.state.doc.toString() - runCode(input) - - return true - } else { - outputSignal.emit('Press Shift+Enter to insert run the code.') - } - - multilineModeSignal.emit(true) - multilineMode = true - view.dispatch({ - changes: { from: view.state.doc.length, insert: '\n' }, - selection: { anchor: view.state.doc.length + 1 }, - }) - - return true - }, - }, - - { - key: 'Tab', - preventDefault: true, - run: (view) => { - view.dispatch({ - changes: { from: view.state.selection.main.from, insert: ' ' }, - selection: { anchor: view.state.selection.main.from + 2 }, - }) - return true - }, - }, - - { - key: 'ArrowUp', - run: (view) => { - if (multilineMode) return false - - const command = history.previous() - if (command === undefined) return false - view.dispatch({ - changes: { from: 0, to: view.state.doc.length, insert: command }, - selection: { anchor: command.length }, - }) - return true - }, - }, - - { - key: 'ArrowDown', - run: (view) => { - if (multilineMode) return false - - const command = history.next() - if (command === undefined) return false - view.dispatch({ - changes: { from: 0, to: view.state.doc.length, insert: command }, - selection: { anchor: command.length }, - }) - return true - }, - }, - - { - key: 'Mod-k 1', - preventDefault: true, - run: (view) => { - const input = view.state.doc.toString() - printParserOutput(input) - - return true - }, - }, - - { - key: 'Mod-k 2', - preventDefault: true, - run: (view) => { - const input = view.state.doc.toString() - printBytecodeOutput(input) - - return true - }, - }, -]) - -let firstTime = true -const singleLineFilter = EditorState.transactionFilter.of((transaction) => { - if (multilineMode) return transaction // Allow everything in multiline mode - - if (firstTime) { - firstTime = false - if (transaction.newDoc.toString().includes('\n')) { - multilineMode = true - return transaction - } - } - - transaction.changes.iterChanges((fromA, toA, fromB, toB, inserted) => { - if (inserted.toString().includes('\n')) { - multilineMode = true - return - } - }) - - return transaction -}) - -export const shrimpKeymap = [customKeymap, singleLineFilter] - -class History { - private commands: string[] = [] - private index: number | undefined - private storageKey = 'shrimp-command-history' - - constructor() { - try { - this.commands = JSON.parse(localStorage.getItem(this.storageKey) || '[]') - } catch { - console.warn('Failed to load command history from localStorage') - } - } - - push(command: string) { - this.commands.push(command) - - // Limit to last 50 commands - this.commands = this.commands.slice(-50) - localStorage.setItem(this.storageKey, JSON.stringify(this.commands)) - this.index = undefined - } - - previous(): string | undefined { - if (this.commands.length === 0) return - - if (this.index === undefined) { - this.index = this.commands.length - 1 - } else if (this.index > 0) { - this.index -= 1 - } - - return this.commands[this.index] - } - - next(): string | undefined { - if (this.commands.length === 0 || this.index === undefined) return - - if (this.index < this.commands.length - 1) { - this.index += 1 - return this.commands[this.index] - } else { - this.index = undefined - return '' - } - } -} - -const history = new History() diff --git a/src/editor/plugins/shrimpLanguage.ts b/src/editor/plugins/shrimpLanguage.ts deleted file mode 100644 index f23e73e..0000000 --- a/src/editor/plugins/shrimpLanguage.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { parser } from '#/parser/shrimp' -import { LRLanguage, LanguageSupport } from '@codemirror/language' -import { highlighting } from '#/parser/highlight.js' - -const language = LRLanguage.define({ - parser: parser.configure({ props: [highlighting] }), -}) - -export const shrimpLanguage = new LanguageSupport(language) diff --git a/src/editor/plugins/shrimpSetup.ts b/src/editor/plugins/shrimpSetup.ts deleted file mode 100644 index 9adaea2..0000000 --- a/src/editor/plugins/shrimpSetup.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { history, defaultKeymap, historyKeymap } from '@codemirror/commands' -import { bracketMatching, indentOnInput } from '@codemirror/language' -import { highlightSpecialChars, drawSelection, dropCursor, keymap } from '@codemirror/view' -import { closeBrackets, autocompletion, completionKeymap } from '@codemirror/autocomplete' -import { EditorState, Compartment } from '@codemirror/state' -import { searchKeymap } from '@codemirror/search' -import { shrimpKeymap } from './keymap' -import { shrimpTheme, shrimpHighlighting } from './theme' -import { shrimpLanguage } from './shrimpLanguage' -import { shrimpErrors } from './errors' -import { persistencePlugin } from './persistence' -import { catchErrors } from './catchErrors' - -export const shrimpSetup = (lineNumbersCompartment: Compartment) => { - return [ - catchErrors, - shrimpKeymap, - highlightSpecialChars(), - history(), - drawSelection(), - dropCursor(), - EditorState.allowMultipleSelections.of(true), - bracketMatching(), - closeBrackets(), - autocompletion(), - indentOnInput(), - keymap.of([...defaultKeymap, ...historyKeymap, ...searchKeymap, ...completionKeymap]), - lineNumbersCompartment.of([]), - shrimpTheme, - shrimpLanguage, - shrimpHighlighting, - shrimpErrors, - persistencePlugin, - ] -} diff --git a/src/editor/plugins/theme.tsx b/src/editor/plugins/theme.tsx deleted file mode 100644 index d8c6cc3..0000000 --- a/src/editor/plugins/theme.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { EditorView } from '@codemirror/view' -import { HighlightStyle, syntaxHighlighting } from '@codemirror/language' -import { tags } from '@lezer/highlight' - -const highlightStyle = HighlightStyle.define([ - { tag: tags.keyword, color: 'var(--color-keyword)' }, - { tag: tags.name, color: 'var(--color-function)' }, - { tag: tags.string, color: 'var(--color-string)' }, - { tag: tags.number, color: 'var(--color-number)' }, - { tag: tags.bool, color: 'var(--color-bool)' }, - { tag: tags.operator, color: 'var(--color-operator)' }, - { tag: tags.paren, color: 'var(--color-paren)' }, - { tag: tags.regexp, color: 'var(--color-regex)' }, - { tag: tags.function(tags.variableName), color: 'var(--color-function-call)' }, - { tag: tags.function(tags.invalid), color: 'white' }, - { - tag: tags.definition(tags.variableName), - color: 'var(--color-variable-def)', - backgroundColor: 'var(--bg-variable-def)', - padding: '1px 2px', - borderRadius: '2px', - fontWeight: '500', - }, -]) - -export const shrimpHighlighting = syntaxHighlighting(highlightStyle) - -export const shrimpTheme = EditorView.theme( - { - '&': { - color: 'var(--text-editor)', - backgroundColor: 'var(--bg-editor)', - height: '100%', - fontSize: '18px', - }, - '.cm-content': { - fontFamily: '"Pixeloid Mono", "Courier New", monospace', - caretColor: 'var(--caret)', - padding: '0px', - }, - '&.cm-focused .cm-cursor': { - borderLeftColor: 'var(--caret)', - }, - '&.cm-focused .cm-selectionBackground, ::selection': { - backgroundColor: 'var(--bg-selection)', - }, - '.cm-editor': { - border: 'none', - outline: 'none', - height: '100%', - }, - '.cm-matchingBracket': { - backgroundColor: 'var(--color-bool)', - }, - '.cm-nonmatchingBracket': { - backgroundColor: 'var(--color-string)', - }, - }, - { dark: true }, -) diff --git a/src/editor/runCode.tsx b/src/editor/runCode.tsx deleted file mode 100644 index 1583b5f..0000000 --- a/src/editor/runCode.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { outputSignal, errorSignal } from '#editor/editor' -import { Compiler } from '#compiler/compiler' -import { errorMessage, log } from '#utils/utils' -import { bytecodeToString } from 'reefvm' -import { parser } from '#parser/shrimp' -import { sendToNose } from '#editor/noseClient' -import { treeToString } from '#utils/tree' - -export const runCode = async (input: string) => { - try { - const compiler = new Compiler(input) - sendToNose(compiler.bytecode) - } catch (error) { - log.error(error) - errorSignal.emit(`${errorMessage(error)}`) - } -} - -export const printParserOutput = (input: string) => { - try { - const cst = parser.parse(input) - const string = treeToString(cst, input) - outputSignal.emit(string) - } catch (error) { - log.error(error) - errorSignal.emit(`${errorMessage(error)}`) - } -} - -export const printBytecodeOutput = (input: string) => { - try { - const compiler = new Compiler(input) - outputSignal.emit(bytecodeToString(compiler.bytecode)) - } catch (error) { - log.error(error) - errorSignal.emit(`${errorMessage(error)}`) - } -} diff --git a/src/editor/theme.ts b/src/editor/theme.ts new file mode 100644 index 0000000..5420320 --- /dev/null +++ b/src/editor/theme.ts @@ -0,0 +1,51 @@ +import { EditorView } from '@codemirror/view' + +export const shrimpTheme = EditorView.theme( + { + '&': { + color: 'var(--text-editor)', + backgroundColor: 'var(--bg-editor)', + height: '100%', + fontSize: '18px', + }, + '.cm-content': { + 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-selectionLayer .cm-selectionBackground': { + backgroundColor: 'var(--bg-selection)', + }, + '.cm-editor': { + border: 'none', + outline: 'none', + height: '100%', + }, + '.cm-matchingBracket': { + backgroundColor: 'var(--color-bool)', + }, + '.cm-nonmatchingBracket': { + backgroundColor: 'var(--color-string)', + }, + '.cm-activeLine': { + 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 } +) diff --git a/src/parser/tokenizer2.ts b/src/parser/tokenizer2.ts index a5d4a89..7b9886b 100644 --- a/src/parser/tokenizer2.ts +++ b/src/parser/tokenizer2.ts @@ -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) } diff --git a/src/prelude/index.ts b/src/prelude/index.ts index ba1126b..cada59d 100644 --- a/src/prelude/index.ts +++ b/src/prelude/index.ts @@ -19,6 +19,7 @@ import { list } from './list' import { math } from './math' import { str } from './str' import { types } from './types' +import { runningInBrowser } from '#utils/utils' export const globals: Record = { date, @@ -31,17 +32,28 @@ export const globals: Record = { str, // shrimp runtime info - $: { - args: Bun.argv.slice(3), - argv: Bun.argv.slice(1), - env: process.env, - pid: process.pid, - cwd: process.env.PWD, - script: { - name: Bun.argv[2] || '(shrimp)', - path: resolve(join('.', Bun.argv[2] ?? '')), - }, - }, + $: runningInBrowser + ? { + args: [], + argv: [], + env: {}, + pid: 0, + cwd: '', + script: { + name: '', + path: '.', + }, + } : { + args: Bun.argv.slice(3), + argv: Bun.argv.slice(1), + env: process.env, + pid: process.pid, + cwd: process.env.PWD, + script: { + name: Bun.argv[2] || '(shrimp)', + path: resolve(join('.', Bun.argv[2] ?? '')), + }, + } // hello echo: (...args: any[]) => { @@ -49,7 +61,7 @@ export const globals: Record = { ...args.map((a) => { const v = toValue(a) return ['array', 'dict'].includes(v.type) ? formatValue(v, true) : v.value - }), + }) ) return toValue(null) }, @@ -120,7 +132,7 @@ export const globals: Record = { const idx = typeof index === 'number' ? index : parseInt(index as string) if (idx < 0 || idx >= value.value.length) { throw new Error( - `at: index ${idx} out of bounds for ${value.type} of length ${value.value.length}`, + `at: index ${idx} out of bounds for ${value.type} of length ${value.value.length}` ) } return value.value[idx] diff --git a/src/server/app.tsx b/src/server/app.tsx deleted file mode 100644 index d5c088b..0000000 --- a/src/server/app.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { Editor } from '#/editor/editor' -import { render } from 'hono/jsx/dom' -import './index.css' - -const App = () => { - return -} - -const root = document.getElementById('root')! -render(, root) diff --git a/src/server/index.css b/src/server/index.css deleted file mode 100644 index d1bb38e..0000000 --- a/src/server/index.css +++ /dev/null @@ -1,84 +0,0 @@ -:root { - /* Background colors */ - --bg-editor: #011627; - --bg-output: #40318d; - --bg-status-bar: #1e2a4a; - --bg-status-border: #0e1a3a; - --bg-selection: #1d3b53; - --bg-variable-def: #1e2a4a; - - /* 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: var(--bg-output); - color: var(--text-output); - font-family: 'Pixeloid Mono', 'Courier New', monospace; - font-size: 18px; - height: 100vh; - overflow: hidden; -} - -#root { - height: 100vh; - background: var(--bg-output); - display: flex; - flex-direction: column; -} diff --git a/src/server/index.html b/src/server/index.html deleted file mode 100644 index eebe715..0000000 --- a/src/server/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - Shrimp - - -
- - - diff --git a/src/server/server.tsx b/src/server/server.tsx deleted file mode 100644 index 1c7291d..0000000 --- a/src/server/server.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import index from './index.html' - -const server = Bun.serve({ - port: process.env.PORT ? Number(process.env.PORT) : 3001, - routes: { - '/*': index, - - '/api/hello': { - async GET(req) { - return Response.json({ - message: 'Hello, world!', - method: 'GET', - }) - }, - async PUT(req) { - return Response.json({ - message: 'Hello, world!', - method: 'PUT', - }) - }, - }, - }, - development: process.env.NODE_ENV !== 'production' && { - hmr: true, - console: true, - }, -}) - -console.log(`🚀 Server running at ${server.url}`) diff --git a/src/utils/signal.ts b/src/utils/signal.ts deleted file mode 100644 index d9629a4..0000000 --- a/src/utils/signal.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * How to use a Signal: - * - * Create a signal with primitives: - * const nameSignal = new Signal() - * const countSignal = new Signal() - * - * Create a signal with objects: - * const chatSignal = new Signal<{ username: string, message: string }>() - * - * Create a signal with no data (void): - * const clickSignal = new Signal() - * const clickSignal2 = new Signal() // Defaults to void - * - * Connect to the signal: - * const disconnect = chatSignal.connect((data) => { - * const {username, message} = data; - * console.log(`${username} said "${message}"`); - * }) - * - * Emit a signal: - * nameSignal.emit("Alice") - * countSignal.emit(42) - * chatSignal.emit({ username: "Chad", message: "Hey everyone, how's it going?" }); - * clickSignal.emit() // No argument for void signals - * - * Forward a signal: - * const relaySignal = new Signal<{ username: string, message: string }>() - * const disconnectRelay = chatSignal.connect(relaySignal) - * // Now, when chatSignal emits, relaySignal will also emit the same data - * - * Disconnect a single listener: - * disconnect(); // The disconnect function is returned when you connect to a signal - * - * Disconnect all listeners: - * chatSignal.disconnect() - */ - -export class Signal { - private listeners: Array<(data: T) => void> = [] - - connect(listenerOrSignal: Signal | ((data: T) => void)) { - let listener: (data: T) => void - - // If it is a signal, forward the data to the signal - if (listenerOrSignal instanceof Signal) { - listener = (data: T) => listenerOrSignal.emit(data) - } else { - listener = listenerOrSignal - } - - this.listeners.push(listener) - - return () => { - this.listeners = this.listeners.filter((l) => l !== listener) - } - } - - emit(data: T) { - for (const listener of this.listeners) { - listener(data) - } - } - - disconnect() { - this.listeners = [] - } -} diff --git a/src/utils/utils.tsx b/src/utils/utils.tsx index a33cef5..9fe6749 100644 --- a/src/utils/utils.tsx +++ b/src/utils/utils.tsx @@ -136,3 +136,12 @@ export const asciiEscapeToHtml = (str: string): HtmlEscapedString => { return result as HtmlEscapedString } + +export const isDebug = (): boolean => { + if (typeof process !== 'undefined' && process.env) { + return !!process.env.DEBUG + } + return false +} + +export const runningInBrowser = typeof window !== 'undefined'