@codonsplice/editor 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.js +172 -0
  2. package/package.json +25 -0
package/index.js ADDED
@@ -0,0 +1,172 @@
1
+ // @codonsplice/editor — a self-contained CodeMirror 6 SpliceQL editor with
2
+ // IntelliSense (syntax highlighting + context-aware autocompletion).
3
+ //
4
+ // Framework-agnostic: `spliceqlExtensions()` returns an array of CM6 extensions
5
+ // you can drop into any EditorView, and `mountSpliceEditor()` constructs one for
6
+ // you. The framework wrappers (@codonsplice/react, /vue, /svelte) build on this.
7
+ //
8
+ // The vocabulary below mirrors the SpliceQL lexer/grammar (CodonSplice engine)
9
+ // and is kept identical to the `splice create` scaffold's editor. Keep in sync
10
+ // with crates/spliceql + the engine builtins when the language grows.
11
+ import { EditorView, minimalSetup } from 'codemirror'
12
+ import { lineNumbers, highlightActiveLine, keymap } from '@codemirror/view'
13
+ import { EditorState } from '@codemirror/state'
14
+ import { StreamLanguage, HighlightStyle, syntaxHighlighting, bracketMatching } from '@codemirror/language'
15
+ import { autocompletion, completionKeymap, closeBrackets, closeBracketsKeymap, snippetCompletion } from '@codemirror/autocomplete'
16
+ import { tags as t } from '@lezer/highlight'
17
+
18
+ export const KEYWORDS = ['FROM', 'SELECT', 'WHERE', 'AND', 'OR', 'NOT', 'CALL', 'WITH', 'ORDER', 'BY', 'ASC', 'DESC', 'LIMIT', 'INTO', 'AS']
19
+ export const BOOLEANS = ['true', 'false']
20
+ export const FORMATS = ['bam', 'vcf', 'bed', 'fasta', 'cram', 'json', 'tsv']
21
+ export const OPS = ['variants', 'cnv', 'coverage', 'reads', 'header']
22
+ export const PARAMS = ['min_af', 'min_allele_freq', 'min_depth', 'min_base_quality', 'min_mapping_quality', 'min_variant_reads', 'min_strand_bias', 'window_size', 'amp_threshold', 'del_threshold', 'min_windows', 'segmentation_method']
23
+ export const FIELDS = ['chr', 'chrom', 'pos', 'ref', 'alt', 'qual', 'depth', 'ref_count', 'alt_count', 'af', 'allele_freq', 'strand_bias', 'kind', 'filter', 'id', 'mapq', 'flag', 'strand', 'is_reverse', 'is_duplicate', 'is_secondary', 'start', 'end', 'coverage', 'normalized', 'masked']
24
+ export const FUNCTIONS = ['abs', 'floor', 'ceil', 'round', 'sqrt', 'pow', 'min', 'max', 'log', 'coalesce', 'len', 'upper', 'lower', 'concat', 'contains', 'starts_with', 'ends_with', 'substr', 'gc', 'revcomp', 'translate', 'codon_at']
25
+
26
+ const KW = new Set(KEYWORDS.map((s) => s.toLowerCase()))
27
+ const BOOL = new Set(BOOLEANS)
28
+ const TY = new Set([...FORMATS, ...OPS])
29
+ const PR = new Set(PARAMS)
30
+ const FD = new Set(FIELDS)
31
+ const FN = new Set(FUNCTIONS)
32
+
33
+ const tokenTable = {
34
+ spliceKw: t.keyword, spliceStr: t.string, spliceNum: t.number, spliceCom: t.lineComment,
35
+ spliceOp: t.operator, spliceTy: t.typeName, splicePr: t.propertyName, spliceFd: t.variableName,
36
+ spliceVar: t.special(t.variableName), spliceFn: t.function(t.variableName), spliceBool: t.bool,
37
+ }
38
+
39
+ const language = StreamLanguage.define({
40
+ token(stream) {
41
+ if (stream.match(/--.*/)) return 'spliceCom'
42
+ if (stream.match(/"(?:[^"\\]|\\.)*"/)) return 'spliceStr'
43
+ if (stream.match(/\$[A-Za-z_]\w*/)) return 'spliceVar'
44
+ if (stream.match(/\d+(?:\.\d+)?/)) return 'spliceNum'
45
+ if (stream.match(/>=|<=|!=|=|>|<|\+|-|\*|\//)) return 'spliceOp'
46
+ const m = stream.match(/[A-Za-z_]\w*/)
47
+ if (m) {
48
+ const w = m[0].toLowerCase()
49
+ if (KW.has(w)) return 'spliceKw'
50
+ if (BOOL.has(w)) return 'spliceBool'
51
+ if (TY.has(w)) return 'spliceTy'
52
+ if (PR.has(w)) return 'splicePr'
53
+ if (FN.has(w)) return 'spliceFn'
54
+ if (FD.has(w)) return 'spliceFd'
55
+ return null
56
+ }
57
+ stream.next()
58
+ return null
59
+ },
60
+ tokenTable,
61
+ })
62
+
63
+ const highlight = HighlightStyle.define([
64
+ { tag: t.keyword, color: '#cba6f7', fontWeight: '600' },
65
+ { tag: t.string, color: '#a6e3a1' },
66
+ { tag: t.number, color: '#fab387' },
67
+ { tag: t.bool, color: '#fab387', fontWeight: '600' },
68
+ { tag: t.lineComment, color: '#6c7086', fontStyle: 'italic' },
69
+ { tag: t.operator, color: '#89dceb' },
70
+ { tag: t.typeName, color: '#f9e2af' },
71
+ { tag: t.propertyName, color: '#74c7ec' },
72
+ { tag: t.variableName, color: '#cdd6f4' },
73
+ { tag: t.special(t.variableName), color: '#f5c2e7' },
74
+ { tag: t.function(t.variableName), color: '#89b4fa' },
75
+ ])
76
+
77
+ // Pre-built completion groups. Functions expand to `name()` with the cursor
78
+ // placed between the parens via a CodeMirror snippet.
79
+ const KEYWORD_COMPLETIONS = KEYWORDS.map((l) => ({ label: l, type: 'keyword', detail: 'clause' }))
80
+ const FORMAT_COMPLETIONS = FORMATS.map((l) => ({ label: l, type: 'type', detail: 'format' }))
81
+ const OP_COMPLETIONS = OPS.map((l) => ({ label: l, type: 'function', detail: 'operation' }))
82
+ const PARAM_COMPLETIONS = PARAMS.map((l) => ({ label: l, type: 'property', detail: 'param' }))
83
+ const FIELD_COMPLETIONS = FIELDS.map((l) => ({ label: l, type: 'variable', detail: 'field' }))
84
+ const BOOL_COMPLETIONS = BOOLEANS.map((l) => ({ label: l, type: 'constant', detail: 'literal' }))
85
+ const FUNCTION_COMPLETIONS = FUNCTIONS.map((l) =>
86
+ snippetCompletion(l + '(${})', { label: l, type: 'function', detail: 'function' })
87
+ )
88
+ const ALL_COMPLETIONS = [
89
+ ...KEYWORD_COMPLETIONS, ...FORMAT_COMPLETIONS, ...OP_COMPLETIONS,
90
+ ...FUNCTION_COMPLETIONS, ...PARAM_COMPLETIONS, ...FIELD_COMPLETIONS, ...BOOL_COMPLETIONS,
91
+ ]
92
+
93
+ // The word immediately before the token being typed, lower-cased — used to pick
94
+ // a context-appropriate completion set (e.g. formats right after FROM/INTO).
95
+ function prevWord(ctx) {
96
+ const before = ctx.state.sliceDoc(Math.max(0, ctx.pos - 240), ctx.pos)
97
+ const m = /([A-Za-z_]\w*)\s+[\w$]*$/.exec(before)
98
+ return m ? m[1].toLowerCase() : null
99
+ }
100
+
101
+ function complete(ctx) {
102
+ const word = ctx.matchBefore(/[\w$]+/)
103
+ if (!word && !ctx.explicit) return null
104
+ const prev = prevWord(ctx)
105
+ let options
106
+ if (prev === 'from' || prev === 'into') options = FORMAT_COMPLETIONS
107
+ else if (prev === 'call') options = OP_COMPLETIONS
108
+ else if (prev === 'with') options = PARAM_COMPLETIONS
109
+ else if (prev === 'order' || prev === 'by') options = [...FIELD_COMPLETIONS, ...FUNCTION_COMPLETIONS]
110
+ else options = ALL_COMPLETIONS
111
+ return { from: word ? word.from : ctx.pos, options, validFor: /[\w$]*/ }
112
+ }
113
+
114
+ const theme = EditorView.theme(
115
+ {
116
+ '&': { height: '100%', backgroundColor: '#11111b', color: '#cdd6f4', fontSize: '13px', textAlign: 'left' },
117
+ '.cm-scroller': { fontFamily: "'JetBrains Mono', ui-monospace, monospace", lineHeight: '1.6', overflow: 'auto' },
118
+ '.cm-content': { caretColor: '#cba6f7', padding: '8px 0', textAlign: 'left' },
119
+ '.cm-gutters': { backgroundColor: '#11111b', color: '#45475a', border: 'none' },
120
+ '.cm-activeLine': { backgroundColor: 'rgba(49,50,68,0.25)' },
121
+ '.cm-activeLineGutter': { backgroundColor: 'transparent', color: '#7f849c' },
122
+ '.cm-cursor': { borderLeftColor: '#cba6f7' },
123
+ '.cm-selectionBackground, &.cm-focused .cm-selectionBackground': { backgroundColor: 'rgba(203,166,247,0.18)' },
124
+ '&.cm-focused': { outline: 'none' },
125
+ '.cm-tooltip.cm-tooltip-autocomplete': { border: '1px solid #313244', backgroundColor: '#181825', borderRadius: '6px' },
126
+ '.cm-tooltip.cm-tooltip-autocomplete > ul': { fontFamily: "'JetBrains Mono', ui-monospace, monospace", maxHeight: '14em' },
127
+ '.cm-tooltip-autocomplete ul li': { color: '#bac2de', padding: '3px 10px' },
128
+ '.cm-tooltip-autocomplete ul li[aria-selected]': { backgroundColor: '#cba6f7', color: '#11111b' },
129
+ '.cm-completionLabel': { fontWeight: '500' },
130
+ '.cm-completionDetail': { color: '#7f849c', fontStyle: 'normal', marginLeft: '1.5em' },
131
+ '.cm-matchingBracket, &.cm-focused .cm-matchingBracket': { backgroundColor: 'rgba(137,180,250,0.22)', color: '#89dceb' },
132
+ '.cm-nonmatchingBracket': { color: '#f38ba8' },
133
+ },
134
+ { dark: true }
135
+ )
136
+
137
+ // The full set of CM6 extensions that make up the SpliceQL editor: the
138
+ // StreamLanguage token mode, Catppuccin syntax highlighting, context-aware
139
+ // autocompletion, bracket matching/closing, and the theme. Drop these into any
140
+ // EditorView, optionally alongside your own extensions.
141
+ export function spliceqlExtensions() {
142
+ return [
143
+ keymap.of([...closeBracketsKeymap, ...completionKeymap]),
144
+ minimalSetup,
145
+ lineNumbers(),
146
+ highlightActiveLine(),
147
+ bracketMatching(),
148
+ closeBrackets(),
149
+ language,
150
+ syntaxHighlighting(highlight),
151
+ autocompletion({ override: [complete], activateOnTyping: true }),
152
+ theme,
153
+ ]
154
+ }
155
+
156
+ // Construct an EditorView mounted into `parent` with the SpliceQL extensions and
157
+ // an update listener that calls `onChange(docString)` on every document change.
158
+ // Returns the EditorView so callers can `.destroy()` it on teardown.
159
+ export function mountSpliceEditor(parent, { doc = '', onChange = () => {} } = {}) {
160
+ return new EditorView({
161
+ parent,
162
+ state: EditorState.create({
163
+ doc,
164
+ extensions: [
165
+ ...spliceqlExtensions(),
166
+ EditorView.updateListener.of((u) => {
167
+ if (u.docChanged) onChange(u.state.doc.toString())
168
+ }),
169
+ ],
170
+ }),
171
+ })
172
+ }
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@codonsplice/editor",
3
+ "version": "0.2.4",
4
+ "description": "Reusable CodeMirror 6 SpliceQL editor (syntax highlighting + IntelliSense) for the CodonSplice engine",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "module": "index.js",
8
+ "exports": {
9
+ ".": "./index.js"
10
+ },
11
+ "files": [
12
+ "index.js"
13
+ ],
14
+ "dependencies": {
15
+ "codemirror": "^6.0.2",
16
+ "@codemirror/view": "^6.43.3",
17
+ "@codemirror/state": "^6.7.0",
18
+ "@codemirror/language": "^6.12.4",
19
+ "@codemirror/autocomplete": "^6.20.3",
20
+ "@lezer/highlight": "^1.2.3"
21
+ },
22
+ "keywords": ["spliceql", "codemirror", "editor", "genomics", "bioinformatics", "syntax-highlighting", "autocomplete"],
23
+ "license": "MIT",
24
+ "repository": "github:Pogo-Bash/codonsplice"
25
+ }