@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.
- package/index.js +172 -0
- 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
|
+
}
|