@seljs/editor 1.0.0
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/CHANGELOG.md +8 -0
- package/LICENSE.md +190 -0
- package/dist/completion/completion-items.d.ts +8 -0
- package/dist/completion/completion-items.d.ts.map +1 -0
- package/dist/completion/completion-items.js +29 -0
- package/dist/completion/index.d.ts +2 -0
- package/dist/completion/index.d.ts.map +1 -0
- package/dist/completion/index.js +1 -0
- package/dist/completion/schema-completion.d.ts +22 -0
- package/dist/completion/schema-completion.d.ts.map +1 -0
- package/dist/completion/schema-completion.js +220 -0
- package/dist/completion/tree-context.d.ts +23 -0
- package/dist/completion/tree-context.d.ts.map +1 -0
- package/dist/completion/tree-context.js +154 -0
- package/dist/editor/create-editor.d.ts +4 -0
- package/dist/editor/create-editor.d.ts.map +1 -0
- package/dist/editor/create-editor.js +14 -0
- package/dist/editor/editor-config.d.ts +4 -0
- package/dist/editor/editor-config.d.ts.map +1 -0
- package/dist/editor/editor-config.js +64 -0
- package/dist/editor/index.d.ts +6 -0
- package/dist/editor/index.d.ts.map +1 -0
- package/dist/editor/index.js +3 -0
- package/dist/editor/theme.d.ts +3 -0
- package/dist/editor/theme.d.ts.map +1 -0
- package/dist/editor/theme.js +43 -0
- package/dist/editor/type-display.d.ts +4 -0
- package/dist/editor/type-display.d.ts.map +1 -0
- package/dist/editor/type-display.js +75 -0
- package/dist/editor/types.d.ts +28 -0
- package/dist/editor/types.d.ts.map +1 -0
- package/dist/editor/types.js +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/language/index.d.ts +2 -0
- package/dist/language/index.d.ts.map +1 -0
- package/dist/language/index.js +1 -0
- package/dist/language/semantic-highlighter.d.ts +4 -0
- package/dist/language/semantic-highlighter.d.ts.map +1 -0
- package/dist/language/semantic-highlighter.js +76 -0
- package/dist/language/tokenizer-config.d.ts +9 -0
- package/dist/language/tokenizer-config.d.ts.map +1 -0
- package/dist/language/tokenizer-config.js +6 -0
- package/dist/linting/diagnostic-mapper.d.ts +28 -0
- package/dist/linting/diagnostic-mapper.d.ts.map +1 -0
- package/dist/linting/diagnostic-mapper.js +46 -0
- package/dist/linting/index.d.ts +3 -0
- package/dist/linting/index.d.ts.map +1 -0
- package/dist/linting/index.js +2 -0
- package/dist/linting/sel-linter.d.ts +12 -0
- package/dist/linting/sel-linter.d.ts.map +1 -0
- package/dist/linting/sel-linter.js +28 -0
- package/package.json +71 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { syntaxTree } from "@codemirror/language";
|
|
2
|
+
/**
|
|
3
|
+
* Walk up the tree looking for a MemberExpression ancestor.
|
|
4
|
+
* A MemberExpression has two Identifier children (or child nodes) separated by ".".
|
|
5
|
+
* The dot position is: firstChild.to + 1 (the "." character is between them).
|
|
6
|
+
* We are in dot-access context when pos >= dotPos.
|
|
7
|
+
*/
|
|
8
|
+
const findDotAccess = (state, node, pos) => {
|
|
9
|
+
let current = node;
|
|
10
|
+
while (current) {
|
|
11
|
+
/*
|
|
12
|
+
* Stop at ArgList boundary — don't match outer MemberExpressions
|
|
13
|
+
* that wrap the entire call (e.g., `erc20.balanceOf(user)` at `user`)
|
|
14
|
+
*/
|
|
15
|
+
if (current.name === "ArgList") {
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
if (current.name === "MemberExpression" ||
|
|
19
|
+
current.name === "OptionalExpression") {
|
|
20
|
+
const firstChild = current.firstChild;
|
|
21
|
+
if (firstChild) {
|
|
22
|
+
/*
|
|
23
|
+
* The dot is immediately after the first child
|
|
24
|
+
* For MemberExpression: firstChild.to is where the "." starts
|
|
25
|
+
* (the token "." is not a named node, it's between firstChild and secondChild)
|
|
26
|
+
*/
|
|
27
|
+
// position after "."
|
|
28
|
+
const dotPos = firstChild.to + 1;
|
|
29
|
+
if (pos >= dotPos) {
|
|
30
|
+
const receiverText = state.doc.sliceString(current.from, firstChild.to);
|
|
31
|
+
return {
|
|
32
|
+
kind: "dot-access",
|
|
33
|
+
receiverText,
|
|
34
|
+
from: dotPos,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
current = current.parent;
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Count commas in ArgList text that appear before pos.
|
|
45
|
+
* Commas are anonymous tokens not exposed as named children,
|
|
46
|
+
* so we scan the raw source text.
|
|
47
|
+
*/
|
|
48
|
+
const countCommasBefore = (argList, state, pos) => {
|
|
49
|
+
/*
|
|
50
|
+
* Only scan from the opening "(" up to pos
|
|
51
|
+
* skip "("
|
|
52
|
+
*/
|
|
53
|
+
const open = argList.from + 1;
|
|
54
|
+
const end = Math.min(pos, argList.to);
|
|
55
|
+
const text = state.doc.sliceString(open, end);
|
|
56
|
+
let count = 0;
|
|
57
|
+
for (const ch of text) {
|
|
58
|
+
if (ch === ",") {
|
|
59
|
+
count++;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return count;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Extract function name and optional receiver name from a CallExpression node.
|
|
66
|
+
*
|
|
67
|
+
* Tree shapes:
|
|
68
|
+
* foo(...) → CallExpression > Identifier, ArgList
|
|
69
|
+
* erc20.foo(...) → CallExpression > MemberExpression(Identifier "erc20", Identifier "foo"), ArgList
|
|
70
|
+
*/
|
|
71
|
+
const extractCallInfo = (state, callExpr) => {
|
|
72
|
+
// The callee is everything before the ArgList (first child)
|
|
73
|
+
const callee = callExpr.firstChild;
|
|
74
|
+
if (!callee) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
if (callee.name === "Identifier") {
|
|
78
|
+
// Simple function call: foo(...)
|
|
79
|
+
return { functionName: state.doc.sliceString(callee.from, callee.to) };
|
|
80
|
+
}
|
|
81
|
+
if (callee.name === "MemberExpression") {
|
|
82
|
+
/*
|
|
83
|
+
* Method call: receiver.method(...)
|
|
84
|
+
* MemberExpression has two children: receiver and method name (Identifier)
|
|
85
|
+
*/
|
|
86
|
+
const lastChild = callee.lastChild;
|
|
87
|
+
if (lastChild?.name === "Identifier") {
|
|
88
|
+
const functionName = state.doc.sliceString(lastChild.from, lastChild.to);
|
|
89
|
+
// Receiver is first child of MemberExpression
|
|
90
|
+
const receiverNode = callee.firstChild;
|
|
91
|
+
const receiverName = receiverNode?.name === "Identifier"
|
|
92
|
+
? state.doc.sliceString(receiverNode.from, receiverNode.to)
|
|
93
|
+
: undefined;
|
|
94
|
+
return { functionName, receiverName };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
};
|
|
99
|
+
const findWordStart = (state, pos) => {
|
|
100
|
+
const text = state.doc.sliceString(0, pos);
|
|
101
|
+
const match = /\w+$/.exec(text);
|
|
102
|
+
return match ? pos - match[0].length : pos;
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Walk up the tree looking for an ArgList ancestor.
|
|
106
|
+
* When found, count commas before pos to get paramIndex,
|
|
107
|
+
* then extract function name and optional receiver from the CallExpression.
|
|
108
|
+
*/
|
|
109
|
+
const findCallArg = (state, node, pos) => {
|
|
110
|
+
let current = node;
|
|
111
|
+
while (current) {
|
|
112
|
+
if (current.name === "ArgList") {
|
|
113
|
+
const paramIndex = countCommasBefore(current, state, pos);
|
|
114
|
+
const callExpr = current.parent;
|
|
115
|
+
if (callExpr?.name === "CallExpression") {
|
|
116
|
+
const info = extractCallInfo(state, callExpr);
|
|
117
|
+
if (info) {
|
|
118
|
+
return {
|
|
119
|
+
kind: "call-arg",
|
|
120
|
+
functionName: info.functionName,
|
|
121
|
+
receiverName: info.receiverName,
|
|
122
|
+
paramIndex,
|
|
123
|
+
from: pos,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
current = current.parent;
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* Extract completion context from the Lezer syntax tree at the given position.
|
|
134
|
+
*/
|
|
135
|
+
export const getCompletionContext = (state, pos) => {
|
|
136
|
+
const tree = syntaxTree(state);
|
|
137
|
+
const node = tree.resolveInner(pos, -1);
|
|
138
|
+
/*
|
|
139
|
+
* Check for dot-access first: cursor is after a "." in a MemberExpression
|
|
140
|
+
* This must come before call-arg so that dot-access inside parentheses
|
|
141
|
+
* (e.g., `contract.method(contract.)`) is handled correctly.
|
|
142
|
+
*/
|
|
143
|
+
const dotAccess = findDotAccess(state, node, pos);
|
|
144
|
+
if (dotAccess) {
|
|
145
|
+
return dotAccess;
|
|
146
|
+
}
|
|
147
|
+
// Check for call-arg: cursor is inside an ArgList
|
|
148
|
+
const callArg = findCallArg(state, node, pos);
|
|
149
|
+
if (callArg) {
|
|
150
|
+
return callArg;
|
|
151
|
+
}
|
|
152
|
+
// Default: top-level completion
|
|
153
|
+
return { kind: "top-level", from: findWordStart(state, pos) };
|
|
154
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-editor.d.ts","sourceRoot":"","sources":["../../src/editor/create-editor.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAI9C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE/C,wBAAgB,eAAe,CAAC,MAAM,EAAE,eAAe,GAAG,UAAU,CAYnE"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { EditorState } from "@codemirror/state";
|
|
2
|
+
import { EditorView } from "@codemirror/view";
|
|
3
|
+
import { buildExtensions } from "./editor-config";
|
|
4
|
+
export function createSELEditor(config) {
|
|
5
|
+
const extensions = buildExtensions(config);
|
|
6
|
+
const state = EditorState.create({
|
|
7
|
+
doc: config.value ?? "",
|
|
8
|
+
extensions,
|
|
9
|
+
});
|
|
10
|
+
return new EditorView({
|
|
11
|
+
state,
|
|
12
|
+
parent: config.parent,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"editor-config.d.ts","sourceRoot":"","sources":["../../src/editor/editor-config.ts"],"names":[],"mappings":"AAGA,OAAO,EAAe,KAAK,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAYhE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE/C,eAAO,MAAM,eAAe,GAAI,QAAQ,eAAe,KAAG,SAAS,EAqElE,CAAC"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { closeBrackets, closeBracketsKeymap } from "@codemirror/autocomplete";
|
|
2
|
+
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
|
|
3
|
+
import { bracketMatching } from "@codemirror/language";
|
|
4
|
+
import { EditorState } from "@codemirror/state";
|
|
5
|
+
import { EditorView, keymap, placeholder } from "@codemirror/view";
|
|
6
|
+
import { celLanguageSupport } from "@seljs/cel-lezer";
|
|
7
|
+
import { SELChecker, rules } from "@seljs/checker";
|
|
8
|
+
import { selDarkTheme, selLightTheme } from "./theme";
|
|
9
|
+
import { createTypeDisplay } from "./type-display";
|
|
10
|
+
import { createSchemaCompletion } from "../completion/schema-completion";
|
|
11
|
+
import { createSemanticHighlighter } from "../language/semantic-highlighter";
|
|
12
|
+
import { createTokenizerConfig } from "../language/tokenizer-config";
|
|
13
|
+
import { createSELLinter } from "../linting/sel-linter";
|
|
14
|
+
export const buildExtensions = (config) => {
|
|
15
|
+
const checker = new SELChecker(config.schema, { rules: [...rules.builtIn] });
|
|
16
|
+
const extensions = [];
|
|
17
|
+
// Language support (includes syntax highlighting)
|
|
18
|
+
extensions.push(celLanguageSupport(config.dark));
|
|
19
|
+
extensions.push(bracketMatching());
|
|
20
|
+
// Semantic highlighting (schema-aware identifier coloring)
|
|
21
|
+
const tokenizerConfig = createTokenizerConfig(config.schema);
|
|
22
|
+
extensions.push(createSemanticHighlighter(tokenizerConfig, config.dark));
|
|
23
|
+
// Autocomplete (type-aware via checker)
|
|
24
|
+
extensions.push(createSchemaCompletion(config.schema, checker));
|
|
25
|
+
extensions.push(closeBrackets());
|
|
26
|
+
// Keybindings
|
|
27
|
+
extensions.push(keymap.of([...closeBracketsKeymap, ...defaultKeymap, ...historyKeymap]));
|
|
28
|
+
extensions.push(history());
|
|
29
|
+
// Theme
|
|
30
|
+
extensions.push(config.dark ? selDarkTheme : selLightTheme);
|
|
31
|
+
// Validation / linting (built-in checker used when no validate callback provided)
|
|
32
|
+
const validate = config.validate ??
|
|
33
|
+
((expression) => checker.check(expression).diagnostics);
|
|
34
|
+
extensions.push(createSELLinter({
|
|
35
|
+
validate,
|
|
36
|
+
delay: config.validateDelay,
|
|
37
|
+
}));
|
|
38
|
+
// onChange listener
|
|
39
|
+
if (config.onChange) {
|
|
40
|
+
const onChange = config.onChange;
|
|
41
|
+
extensions.push(EditorView.updateListener.of((update) => {
|
|
42
|
+
if (update.docChanged) {
|
|
43
|
+
onChange(update.state.doc.toString());
|
|
44
|
+
}
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
// Read-only
|
|
48
|
+
if (config.readOnly) {
|
|
49
|
+
extensions.push(EditorState.readOnly.of(true));
|
|
50
|
+
}
|
|
51
|
+
// Placeholder
|
|
52
|
+
if (config.placeholder) {
|
|
53
|
+
extensions.push(placeholder(config.placeholder));
|
|
54
|
+
}
|
|
55
|
+
// Type display panel
|
|
56
|
+
if (config.showType) {
|
|
57
|
+
extensions.push(createTypeDisplay(checker, config.dark ?? false));
|
|
58
|
+
}
|
|
59
|
+
// User-provided extensions (last, so they can override)
|
|
60
|
+
if (config.extensions) {
|
|
61
|
+
extensions.push(...config.extensions);
|
|
62
|
+
}
|
|
63
|
+
return extensions;
|
|
64
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { createSELEditor } from "./create-editor";
|
|
2
|
+
export { buildExtensions } from "./editor-config";
|
|
3
|
+
export { selLightTheme, selDarkTheme } from "./theme";
|
|
4
|
+
export type { SELEditorConfig } from "./types";
|
|
5
|
+
export type { EditorView } from "@codemirror/view";
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/editor/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACtD,YAAY,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/C,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../../src/editor/theme.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,aAAa,uCAoBxB,CAAC;AAEH,eAAO,MAAM,YAAY,uCAuBxB,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { EditorView } from "@codemirror/view";
|
|
2
|
+
export const selLightTheme = EditorView.theme({
|
|
3
|
+
"&": {
|
|
4
|
+
fontSize: "14px",
|
|
5
|
+
backgroundColor: "#ffffff",
|
|
6
|
+
color: "#1e1e1e",
|
|
7
|
+
},
|
|
8
|
+
".cm-content": {
|
|
9
|
+
caretColor: "#000000",
|
|
10
|
+
fontFamily: "'JetBrains Mono', 'Fira Code', 'Consolas', monospace",
|
|
11
|
+
padding: "4px 0",
|
|
12
|
+
},
|
|
13
|
+
"&.cm-focused .cm-cursor": {
|
|
14
|
+
borderLeftColor: "#000000",
|
|
15
|
+
},
|
|
16
|
+
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground": {
|
|
17
|
+
backgroundColor: "#d7d4f0",
|
|
18
|
+
},
|
|
19
|
+
".cm-gutters": {
|
|
20
|
+
display: "none",
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
export const selDarkTheme = EditorView.theme({
|
|
24
|
+
"&": {
|
|
25
|
+
fontSize: "14px",
|
|
26
|
+
backgroundColor: "#1e1e1e",
|
|
27
|
+
color: "#d4d4d4",
|
|
28
|
+
},
|
|
29
|
+
".cm-content": {
|
|
30
|
+
caretColor: "#ffffff",
|
|
31
|
+
fontFamily: "'JetBrains Mono', 'Fira Code', 'Consolas', monospace",
|
|
32
|
+
padding: "4px 0",
|
|
33
|
+
},
|
|
34
|
+
"&.cm-focused .cm-cursor": {
|
|
35
|
+
borderLeftColor: "#ffffff",
|
|
36
|
+
},
|
|
37
|
+
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground": {
|
|
38
|
+
backgroundColor: "#264f78",
|
|
39
|
+
},
|
|
40
|
+
".cm-gutters": {
|
|
41
|
+
display: "none",
|
|
42
|
+
},
|
|
43
|
+
}, { dark: true });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"type-display.d.ts","sourceRoot":"","sources":["../../src/editor/type-display.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAA2B,MAAM,mBAAmB,CAAC;AAG5E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AA6DjD,wBAAgB,iBAAiB,CAChC,OAAO,EAAE,UAAU,EACnB,IAAI,EAAE,OAAO,GACX,SAAS,CA4BX"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { StateEffect, StateField } from "@codemirror/state";
|
|
2
|
+
import { EditorView, showPanel } from "@codemirror/view";
|
|
3
|
+
const setType = StateEffect.define();
|
|
4
|
+
const typeField = StateField.define({
|
|
5
|
+
create: () => null,
|
|
6
|
+
update(value, tr) {
|
|
7
|
+
for (const e of tr.effects) {
|
|
8
|
+
if (e.is(setType)) {
|
|
9
|
+
return e.value;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
function createTypePanel(dark) {
|
|
16
|
+
return (view) => {
|
|
17
|
+
const dom = document.createElement("div");
|
|
18
|
+
dom.className = "sel-type-display";
|
|
19
|
+
dom.style.cssText = [
|
|
20
|
+
"display: flex",
|
|
21
|
+
"align-items: center",
|
|
22
|
+
"gap: 6px",
|
|
23
|
+
`padding: 3px 8px`,
|
|
24
|
+
"font-size: 12px",
|
|
25
|
+
`font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace`,
|
|
26
|
+
`color: ${dark ? "#9ca3af" : "#6b7280"}`,
|
|
27
|
+
`background: ${dark ? "#262626" : "#f9fafb"}`,
|
|
28
|
+
`border-top: 1px solid ${dark ? "#374151" : "#e5e7eb"}`,
|
|
29
|
+
].join("; ");
|
|
30
|
+
const update = () => {
|
|
31
|
+
const type = view.state.field(typeField);
|
|
32
|
+
dom.textContent = "";
|
|
33
|
+
if (type) {
|
|
34
|
+
const label = document.createElement("span");
|
|
35
|
+
label.style.color = dark ? "#6b7280" : "#9ca3af";
|
|
36
|
+
label.textContent = "output";
|
|
37
|
+
const typeSpan = document.createElement("span");
|
|
38
|
+
typeSpan.style.color = dark ? "#93c5fd" : "#2563eb";
|
|
39
|
+
typeSpan.style.fontWeight = "500";
|
|
40
|
+
typeSpan.textContent = type;
|
|
41
|
+
dom.append(label, " ", typeSpan);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
update();
|
|
45
|
+
return {
|
|
46
|
+
dom,
|
|
47
|
+
update: () => {
|
|
48
|
+
update();
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function createTypeDisplay(checker, dark) {
|
|
54
|
+
let debounceTimer;
|
|
55
|
+
const plugin = EditorView.updateListener.of((update) => {
|
|
56
|
+
if (!update.docChanged && !update.startState.field(typeField, false)) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (debounceTimer) {
|
|
60
|
+
clearTimeout(debounceTimer);
|
|
61
|
+
}
|
|
62
|
+
debounceTimer = setTimeout(() => {
|
|
63
|
+
const doc = update.state.doc.toString().trim();
|
|
64
|
+
if (!doc) {
|
|
65
|
+
update.view.dispatch({ effects: setType.of(null) });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const result = checker.check(doc);
|
|
69
|
+
update.view.dispatch({
|
|
70
|
+
effects: setType.of(result.valid ? (result.type ?? null) : null),
|
|
71
|
+
});
|
|
72
|
+
}, 200);
|
|
73
|
+
});
|
|
74
|
+
return [typeField, plugin, showPanel.of(createTypePanel(dark))];
|
|
75
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { SELDiagnostic } from "../linting/diagnostic-mapper";
|
|
2
|
+
import type { Extension } from "@codemirror/state";
|
|
3
|
+
import type { SELSchema } from "@seljs/schema";
|
|
4
|
+
export interface SELEditorConfig {
|
|
5
|
+
/** Container element to mount into */
|
|
6
|
+
parent: HTMLElement;
|
|
7
|
+
/** Schema driving autocomplete and syntax highlighting */
|
|
8
|
+
schema: SELSchema;
|
|
9
|
+
/** Initial expression value */
|
|
10
|
+
value?: string;
|
|
11
|
+
/** Called on every expression change */
|
|
12
|
+
onChange?: (value: string) => void;
|
|
13
|
+
/** Validation function for error highlighting */
|
|
14
|
+
validate?: (expression: string) => SELDiagnostic[] | Promise<SELDiagnostic[]>;
|
|
15
|
+
/** Debounce delay for validation (default: 300ms) */
|
|
16
|
+
validateDelay?: number;
|
|
17
|
+
/** Dark mode */
|
|
18
|
+
dark?: boolean;
|
|
19
|
+
/** Whether the editor is read-only */
|
|
20
|
+
readOnly?: boolean;
|
|
21
|
+
/** Placeholder text */
|
|
22
|
+
placeholder?: string;
|
|
23
|
+
/** Show inferred output type below the editor */
|
|
24
|
+
showType?: boolean;
|
|
25
|
+
/** Additional CodeMirror extensions */
|
|
26
|
+
extensions?: Extension[];
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/editor/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE/C,MAAM,WAAW,eAAe;IAC/B,sCAAsC;IACtC,MAAM,EAAE,WAAW,CAAC;IAEpB,0DAA0D;IAC1D,MAAM,EAAE,SAAS,CAAC;IAElB,+BAA+B;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,wCAAwC;IACxC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAEnC,iDAAiD;IACjD,QAAQ,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,aAAa,EAAE,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC;IAE9E,qDAAqD;IACrD,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB,gBAAgB;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;IAEf,sCAAsC;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,iDAAiD;IACjD,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,uCAAuC;IACvC,UAAU,CAAC,EAAE,SAAS,EAAE,CAAC;CACzB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/language/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./tokenizer-config";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"semantic-highlighter.d.ts","sourceRoot":"","sources":["../../src/language/semantic-highlighter.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,SAAS,EAAmB,MAAM,mBAAmB,CAAC;AAQpE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AA0E1D,eAAO,MAAM,yBAAyB,GACrC,QAAQ,eAAe,EACvB,cAAY,KACV,SAgBF,CAAC"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { syntaxTree } from "@codemirror/language";
|
|
2
|
+
import { RangeSetBuilder } from "@codemirror/state";
|
|
3
|
+
import { Decoration, ViewPlugin, } from "@codemirror/view";
|
|
4
|
+
const LIGHT_COLORS = {
|
|
5
|
+
contract: "#00695c",
|
|
6
|
+
function: "#1565c0",
|
|
7
|
+
macro: "#6a1b9a",
|
|
8
|
+
variable: "#37474f",
|
|
9
|
+
};
|
|
10
|
+
const DARK_COLORS = {
|
|
11
|
+
contract: "#4db6ac",
|
|
12
|
+
function: "#64b5f6",
|
|
13
|
+
macro: "#ce93d8",
|
|
14
|
+
variable: "#b0bec5",
|
|
15
|
+
};
|
|
16
|
+
const createDecorations = (dark) => {
|
|
17
|
+
const colors = dark ? DARK_COLORS : LIGHT_COLORS;
|
|
18
|
+
return {
|
|
19
|
+
contract: Decoration.mark({
|
|
20
|
+
attributes: { style: `color: ${colors.contract}` },
|
|
21
|
+
}),
|
|
22
|
+
function: Decoration.mark({
|
|
23
|
+
attributes: { style: `color: ${colors.function}` },
|
|
24
|
+
}),
|
|
25
|
+
macro: Decoration.mark({ attributes: { style: `color: ${colors.macro}` } }),
|
|
26
|
+
variable: Decoration.mark({
|
|
27
|
+
attributes: { style: `color: ${colors.variable}` },
|
|
28
|
+
}),
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
const buildDecorations = (view, config, decos) => {
|
|
32
|
+
const builder = new RangeSetBuilder();
|
|
33
|
+
const tree = syntaxTree(view.state);
|
|
34
|
+
for (const { from, to } of view.visibleRanges) {
|
|
35
|
+
tree.iterate({
|
|
36
|
+
from,
|
|
37
|
+
to,
|
|
38
|
+
enter(node) {
|
|
39
|
+
if (node.name !== "Identifier") {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const name = view.state.doc.sliceString(node.from, node.to);
|
|
43
|
+
let deco;
|
|
44
|
+
if (config.contractNames.has(name)) {
|
|
45
|
+
deco = decos.contract;
|
|
46
|
+
}
|
|
47
|
+
else if (config.functionNames.has(name)) {
|
|
48
|
+
deco = decos.function;
|
|
49
|
+
}
|
|
50
|
+
else if (config.macroNames.has(name)) {
|
|
51
|
+
deco = decos.macro;
|
|
52
|
+
}
|
|
53
|
+
else if (config.variableNames.has(name)) {
|
|
54
|
+
deco = decos.variable;
|
|
55
|
+
}
|
|
56
|
+
if (deco) {
|
|
57
|
+
builder.add(node.from, node.to, deco);
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return builder.finish();
|
|
63
|
+
};
|
|
64
|
+
export const createSemanticHighlighter = function (config, dark = false) {
|
|
65
|
+
const decos = createDecorations(dark);
|
|
66
|
+
return ViewPlugin.define((view) => ({
|
|
67
|
+
decorations: buildDecorations(view, config, decos),
|
|
68
|
+
update(update) {
|
|
69
|
+
if (update.docChanged || update.viewportChanged) {
|
|
70
|
+
this.decorations = buildDecorations(update.view, config, decos);
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
}), {
|
|
74
|
+
decorations: (v) => v.decorations,
|
|
75
|
+
});
|
|
76
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SELSchema } from "@seljs/schema";
|
|
2
|
+
export interface TokenizerConfig {
|
|
3
|
+
contractNames: Set<string>;
|
|
4
|
+
functionNames: Set<string>;
|
|
5
|
+
macroNames: Set<string>;
|
|
6
|
+
variableNames: Set<string>;
|
|
7
|
+
}
|
|
8
|
+
export declare const createTokenizerConfig: (schema: SELSchema) => TokenizerConfig;
|
|
9
|
+
//# sourceMappingURL=tokenizer-config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokenizer-config.d.ts","sourceRoot":"","sources":["../../src/language/tokenizer-config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE/C,MAAM,WAAW,eAAe;IAC/B,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACxB,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAC3B;AAED,eAAO,MAAM,qBAAqB,GAAI,QAAQ,SAAS,KAAG,eAKxD,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const createTokenizerConfig = (schema) => ({
|
|
2
|
+
contractNames: new Set(schema.contracts.map((c) => c.name)),
|
|
3
|
+
functionNames: new Set(schema.functions.map((f) => f.name)),
|
|
4
|
+
macroNames: new Set(schema.macros.map((m) => m.name)),
|
|
5
|
+
variableNames: new Set(schema.variables.map((v) => v.name)),
|
|
6
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { SELDiagnostic } from "@seljs/checker";
|
|
2
|
+
interface CheckResult {
|
|
3
|
+
valid: boolean;
|
|
4
|
+
error?: Error;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Maps a TypeCheckResult-shaped object or a caught Error to SELDiagnostic[].
|
|
8
|
+
*
|
|
9
|
+
* Handles both failure modes from `@seljs/runtime`'s `env.check()`:
|
|
10
|
+
* 1. Returned result: `{ valid: false, error: TypeError }`
|
|
11
|
+
* 2. Thrown exception: `SELParseError` or `SELTypeError`
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* ```ts
|
|
15
|
+
* const validate = (expression: string): SELDiagnostic[] => {
|
|
16
|
+
* try {
|
|
17
|
+
* const result = env.check(expression);
|
|
18
|
+
* return mapCheckResult(result, expression.length);
|
|
19
|
+
* } catch (error) {
|
|
20
|
+
* return mapCheckResult(error as Error, expression.length);
|
|
21
|
+
* }
|
|
22
|
+
* };
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
declare const mapCheckResult: (resultOrError: CheckResult | Error, docLength: number) => SELDiagnostic[];
|
|
26
|
+
export { mapCheckResult };
|
|
27
|
+
export type { SELDiagnostic };
|
|
28
|
+
//# sourceMappingURL=diagnostic-mapper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diagnostic-mapper.d.ts","sourceRoot":"","sources":["../../src/linting/diagnostic-mapper.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEpD,UAAU,WAAW;IACpB,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,KAAK,CAAC;CACd;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,QAAA,MAAM,cAAc,GACnB,eAAe,WAAW,GAAG,KAAK,EAClC,WAAW,MAAM,KACf,aAAa,EA4Bf,CAAC;AAEF,OAAO,EAAE,cAAc,EAAE,CAAC;AAC1B,YAAY,EAAE,aAAa,EAAE,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps a TypeCheckResult-shaped object or a caught Error to SELDiagnostic[].
|
|
3
|
+
*
|
|
4
|
+
* Handles both failure modes from `@seljs/runtime`'s `env.check()`:
|
|
5
|
+
* 1. Returned result: `{ valid: false, error: TypeError }`
|
|
6
|
+
* 2. Thrown exception: `SELParseError` or `SELTypeError`
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* ```ts
|
|
10
|
+
* const validate = (expression: string): SELDiagnostic[] => {
|
|
11
|
+
* try {
|
|
12
|
+
* const result = env.check(expression);
|
|
13
|
+
* return mapCheckResult(result, expression.length);
|
|
14
|
+
* } catch (error) {
|
|
15
|
+
* return mapCheckResult(error as Error, expression.length);
|
|
16
|
+
* }
|
|
17
|
+
* };
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
const mapCheckResult = (resultOrError, docLength) => {
|
|
21
|
+
// Thrown error path
|
|
22
|
+
if (resultOrError instanceof Error) {
|
|
23
|
+
return [
|
|
24
|
+
{
|
|
25
|
+
message: resultOrError.message,
|
|
26
|
+
severity: "error",
|
|
27
|
+
from: 0,
|
|
28
|
+
to: Math.max(0, docLength),
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
// Returned result path
|
|
33
|
+
if (resultOrError.valid) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
const message = resultOrError.error?.message ?? "Invalid expression";
|
|
37
|
+
return [
|
|
38
|
+
{
|
|
39
|
+
message,
|
|
40
|
+
severity: "error",
|
|
41
|
+
from: 0,
|
|
42
|
+
to: Math.max(0, docLength),
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
};
|
|
46
|
+
export { mapCheckResult };
|