Better edtior
This commit is contained in:
parent
82722ec9e4
commit
2c7508c2a4
|
|
@ -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],
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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[] = []
|
||||
|
||||
|
|
@ -22,4 +21,4 @@ export const shrimpDiagnostics = linter((view) => {
|
|||
}
|
||||
|
||||
return diagnostics
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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) => {
|
||||
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({
|
||||
parent: el,
|
||||
doc: getContent() || '# type some code\necho hello',
|
||||
extensions: [basicSetup, shrimpTheme, shrimpDiagnostics, persistencePlugin, shrimpHighlighter],
|
||||
doc: initialCode,
|
||||
extensions,
|
||||
})
|
||||
}
|
||||
|
||||
// Mount when running in browser
|
||||
if (typeof document !== 'undefined') {
|
||||
render(<Editor />, document.getElementById('root')!)
|
||||
// Trigger onChange with initial content
|
||||
onChange?.(initialCode)
|
||||
}
|
||||
|
|
|
|||
6
src/editor/example/frontend.tsx
Normal file
6
src/editor/example/frontend.tsx
Normal 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')!)
|
||||
}
|
||||
11
src/editor/example/index.html
Normal file
11
src/editor/example/index.html
Normal 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>
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
1
src/editor/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { Editor } from './editor'
|
||||
8
src/editor/keymap.ts
Normal file
8
src/editor/keymap.ts
Normal 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,
|
||||
])
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user