diff --git a/src/editor/completions.ts b/src/editor/completions.ts index 5a8db2e..5a0fa91 100644 --- a/src/editor/completions.ts +++ b/src/editor/completions.ts @@ -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], + }) +} diff --git a/src/editor/diagnostics.ts b/src/editor/diagnostics.ts index fe9af90..18c3a86 100644 --- a/src/editor/diagnostics.ts +++ b/src/editor/diagnostics.ts @@ -2,24 +2,23 @@ import { linter, type Diagnostic } from '@codemirror/lint' import { Shrimp } from '#/index' 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) => { - 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, - }) + 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 -}) + return diagnostics + }) diff --git a/src/editor/editor.css b/src/editor/editor.css index e69de29..84282f8 100644 --- a/src/editor/editor.css +++ b/src/editor/editor.css @@ -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; +} \ No newline at end of file diff --git a/src/editor/editor.tsx b/src/editor/editor.tsx index 19ee5b7..273fe34 100644 --- a/src/editor/editor.tsx +++ b/src/editor/editor.tsx @@ -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 (