@seljs/editor 1.0.0 → 1.0.1
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 +7 -0
- package/dist/completion/completion-items.cjs +36 -0
- package/dist/completion/completion-items.mjs +32 -0
- package/dist/completion/index.cjs +1 -0
- package/dist/completion/index.d.mts +1 -0
- package/dist/completion/index.mjs +2 -0
- package/dist/completion/schema-completion.cjs +182 -0
- package/dist/completion/schema-completion.d.cts +25 -0
- package/dist/completion/schema-completion.d.mts +25 -0
- package/dist/completion/schema-completion.mjs +181 -0
- package/dist/completion/tree-context.cjs +108 -0
- package/dist/completion/tree-context.mjs +108 -0
- package/dist/editor/create-editor.cjs +16 -0
- package/dist/editor/create-editor.d.cts +7 -0
- package/dist/editor/create-editor.d.mts +7 -0
- package/dist/editor/create-editor.mjs +16 -0
- package/dist/editor/editor-config.cjs +49 -0
- package/dist/editor/editor-config.d.cts +7 -0
- package/dist/editor/editor-config.d.mts +7 -0
- package/dist/editor/editor-config.mjs +49 -0
- package/dist/editor/index.cjs +3 -0
- package/dist/editor/index.d.cts +6 -0
- package/dist/editor/index.d.mts +6 -0
- package/dist/editor/index.mjs +4 -0
- package/dist/editor/theme.cjs +35 -0
- package/dist/editor/theme.d.cts +7 -0
- package/dist/editor/theme.d.mts +7 -0
- package/dist/editor/theme.mjs +34 -0
- package/dist/editor/type-display.cjs +71 -0
- package/dist/editor/type-display.mjs +71 -0
- package/dist/editor/types.d.cts +31 -0
- package/dist/editor/types.d.mts +31 -0
- package/dist/index.cjs +20 -0
- package/dist/index.d.cts +10 -0
- package/dist/index.d.mts +10 -0
- package/dist/index.mjs +11 -0
- package/dist/language/semantic-highlighter.cjs +55 -0
- package/dist/language/semantic-highlighter.mjs +55 -0
- package/dist/language/tokenizer-config.cjs +9 -0
- package/dist/language/tokenizer-config.d.cts +12 -0
- package/dist/language/tokenizer-config.d.mts +12 -0
- package/dist/language/tokenizer-config.mjs +9 -0
- package/dist/linting/diagnostic-mapper.cjs +37 -0
- package/dist/linting/{diagnostic-mapper.d.ts → diagnostic-mapper.d.cts} +7 -6
- package/dist/linting/diagnostic-mapper.d.mts +29 -0
- package/dist/linting/diagnostic-mapper.mjs +37 -0
- package/dist/linting/index.cjs +2 -0
- package/dist/linting/index.d.mts +2 -0
- package/dist/linting/index.mjs +3 -0
- package/dist/linting/sel-linter.cjs +28 -0
- package/dist/linting/sel-linter.d.cts +13 -0
- package/dist/linting/sel-linter.d.mts +13 -0
- package/dist/linting/sel-linter.mjs +28 -0
- package/package.json +29 -22
- package/dist/completion/completion-items.d.ts +0 -8
- package/dist/completion/completion-items.d.ts.map +0 -1
- package/dist/completion/completion-items.js +0 -29
- package/dist/completion/index.d.ts +0 -2
- package/dist/completion/index.d.ts.map +0 -1
- package/dist/completion/index.js +0 -1
- package/dist/completion/schema-completion.d.ts +0 -22
- package/dist/completion/schema-completion.d.ts.map +0 -1
- package/dist/completion/schema-completion.js +0 -220
- package/dist/completion/tree-context.d.ts +0 -23
- package/dist/completion/tree-context.d.ts.map +0 -1
- package/dist/completion/tree-context.js +0 -154
- package/dist/editor/create-editor.d.ts +0 -4
- package/dist/editor/create-editor.d.ts.map +0 -1
- package/dist/editor/create-editor.js +0 -14
- package/dist/editor/editor-config.d.ts +0 -4
- package/dist/editor/editor-config.d.ts.map +0 -1
- package/dist/editor/editor-config.js +0 -64
- package/dist/editor/index.d.ts +0 -6
- package/dist/editor/index.d.ts.map +0 -1
- package/dist/editor/index.js +0 -3
- package/dist/editor/theme.d.ts +0 -3
- package/dist/editor/theme.d.ts.map +0 -1
- package/dist/editor/theme.js +0 -43
- package/dist/editor/type-display.d.ts +0 -4
- package/dist/editor/type-display.d.ts.map +0 -1
- package/dist/editor/type-display.js +0 -75
- package/dist/editor/types.d.ts +0 -28
- package/dist/editor/types.d.ts.map +0 -1
- package/dist/editor/types.js +0 -1
- package/dist/index.d.ts +0 -5
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -4
- package/dist/language/index.d.ts +0 -2
- package/dist/language/index.d.ts.map +0 -1
- package/dist/language/index.js +0 -1
- package/dist/language/semantic-highlighter.d.ts +0 -4
- package/dist/language/semantic-highlighter.d.ts.map +0 -1
- package/dist/language/semantic-highlighter.js +0 -76
- package/dist/language/tokenizer-config.d.ts +0 -9
- package/dist/language/tokenizer-config.d.ts.map +0 -1
- package/dist/language/tokenizer-config.js +0 -6
- package/dist/linting/diagnostic-mapper.d.ts.map +0 -1
- package/dist/linting/diagnostic-mapper.js +0 -46
- package/dist/linting/index.d.ts +0 -3
- package/dist/linting/index.d.ts.map +0 -1
- package/dist/linting/index.js +0 -2
- package/dist/linting/sel-linter.d.ts +0 -12
- package/dist/linting/sel-linter.d.ts.map +0 -1
- package/dist/linting/sel-linter.js +0 -28
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
import { autocompletion, } from "@codemirror/autocomplete";
|
|
2
|
-
import { isTypeCompatible } from "@seljs/checker";
|
|
3
|
-
import { createContractCompletions, createFunctionCompletions, createMacroCompletions, createMethodCompletions, createVariableCompletions, } from "./completion-items";
|
|
4
|
-
import { getCompletionContext } from "./tree-context";
|
|
5
|
-
/**
|
|
6
|
-
* Determine the CodeMirror completion type from a checker CompletionItem.
|
|
7
|
-
* Method details start with "(" (e.g., "(owner: sol_address): sol_int"),
|
|
8
|
-
* while struct field details are plain type names (e.g., "sol_int").
|
|
9
|
-
*/
|
|
10
|
-
const completionKind = (item) => item.detail?.startsWith("(") ? "method" : "property";
|
|
11
|
-
export class SchemaCompletionProvider {
|
|
12
|
-
topLevelCompletions;
|
|
13
|
-
allDotCompletions;
|
|
14
|
-
checker;
|
|
15
|
-
constructor(schema, checker) {
|
|
16
|
-
this.checker = checker;
|
|
17
|
-
const contractCompletions = createContractCompletions(schema.contracts);
|
|
18
|
-
const variableCompletions = createVariableCompletions(schema.variables);
|
|
19
|
-
const freeFunctions = schema.functions.filter((f) => !f.receiverType);
|
|
20
|
-
const functionCompletions = createFunctionCompletions(freeFunctions);
|
|
21
|
-
const macroCompletions = createMacroCompletions(schema.macros);
|
|
22
|
-
const atomCompletions = [
|
|
23
|
-
{ label: "true", type: "keyword" },
|
|
24
|
-
{ label: "false", type: "keyword" },
|
|
25
|
-
{ label: "null", type: "keyword" },
|
|
26
|
-
];
|
|
27
|
-
this.topLevelCompletions = [
|
|
28
|
-
...contractCompletions,
|
|
29
|
-
...variableCompletions,
|
|
30
|
-
...functionCompletions,
|
|
31
|
-
...atomCompletions,
|
|
32
|
-
];
|
|
33
|
-
// Broad matching: all methods + macros + struct fields + receiver methods
|
|
34
|
-
const allMethods = [];
|
|
35
|
-
for (const contract of schema.contracts) {
|
|
36
|
-
allMethods.push(...createMethodCompletions(contract));
|
|
37
|
-
}
|
|
38
|
-
const structFields = [];
|
|
39
|
-
for (const type of schema.types) {
|
|
40
|
-
if (type.kind === "struct" && type.fields) {
|
|
41
|
-
for (const field of type.fields) {
|
|
42
|
-
structFields.push({
|
|
43
|
-
label: field.name,
|
|
44
|
-
type: "property",
|
|
45
|
-
detail: field.type,
|
|
46
|
-
info: field.description,
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
const receiverMethods = [];
|
|
52
|
-
for (const fn of schema.functions) {
|
|
53
|
-
if (fn.receiverType) {
|
|
54
|
-
receiverMethods.push({
|
|
55
|
-
label: fn.name,
|
|
56
|
-
type: "method",
|
|
57
|
-
detail: fn.signature,
|
|
58
|
-
info: fn.description,
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
this.allDotCompletions = [
|
|
63
|
-
...allMethods,
|
|
64
|
-
...macroCompletions,
|
|
65
|
-
...structFields,
|
|
66
|
-
...receiverMethods,
|
|
67
|
-
];
|
|
68
|
-
}
|
|
69
|
-
completionSource = (context) => {
|
|
70
|
-
const treeCtx = getCompletionContext(context.state, context.pos);
|
|
71
|
-
switch (treeCtx.kind) {
|
|
72
|
-
case "dot-access":
|
|
73
|
-
return this.handleDotAccess(context, treeCtx);
|
|
74
|
-
case "call-arg":
|
|
75
|
-
return this.handleCallArg(context, treeCtx);
|
|
76
|
-
case "top-level":
|
|
77
|
-
return this.handleTopLevel(context, treeCtx);
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
handleDotAccess(context, treeCtx) {
|
|
81
|
-
const { receiverText, from } = treeCtx;
|
|
82
|
-
// Skip numeric identifiers (e.g., 3.14)
|
|
83
|
-
if (/^\d+$/.test(receiverText)) {
|
|
84
|
-
return this.handleTopLevel(context, { kind: "top-level", from });
|
|
85
|
-
}
|
|
86
|
-
// Use checker's direct API — tree-context already provides receiverText
|
|
87
|
-
const info = this.checker.dotCompletions(receiverText);
|
|
88
|
-
// Checker resolved the type and found members
|
|
89
|
-
if (info.items.length > 0) {
|
|
90
|
-
return {
|
|
91
|
-
from,
|
|
92
|
-
options: info.items.map((item) => ({
|
|
93
|
-
label: item.label,
|
|
94
|
-
type: completionKind(item),
|
|
95
|
-
detail: item.detail,
|
|
96
|
-
info: item.description,
|
|
97
|
-
})),
|
|
98
|
-
filter: true,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
/*
|
|
102
|
-
* Checker resolved to a known type but no members — no completions
|
|
103
|
-
* When receiverType !== receiverText, the checker actually resolved the type
|
|
104
|
-
*/
|
|
105
|
-
if (info.receiverType !== receiverText) {
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
// Unresolved receiver (checker echoes identifier back) — broad fallback
|
|
109
|
-
return this.allDotCompletions.length
|
|
110
|
-
? { from, options: this.allDotCompletions, filter: true }
|
|
111
|
-
: null;
|
|
112
|
-
}
|
|
113
|
-
handleCallArg(context, treeCtx) {
|
|
114
|
-
// Use checker's direct API with structured context from tree
|
|
115
|
-
const expectedType = this.checker.expectedTypeFor({
|
|
116
|
-
kind: "function-arg",
|
|
117
|
-
functionName: treeCtx.functionName,
|
|
118
|
-
receiverName: treeCtx.receiverName,
|
|
119
|
-
paramIndex: treeCtx.paramIndex,
|
|
120
|
-
});
|
|
121
|
-
if (expectedType) {
|
|
122
|
-
// Filter pre-built top-level completions by type compatibility
|
|
123
|
-
const filtered = this.topLevelCompletions.filter((c) => {
|
|
124
|
-
if (c.type === "keyword") {
|
|
125
|
-
// null is dyn — always compatible
|
|
126
|
-
if (c.label === "null") {
|
|
127
|
-
return true;
|
|
128
|
-
}
|
|
129
|
-
if (c.label === "true" || c.label === "false") {
|
|
130
|
-
return isTypeCompatible("bool", expectedType);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
// Variables have their type in detail
|
|
134
|
-
if (c.type === "variable" && c.detail) {
|
|
135
|
-
return isTypeCompatible(c.detail, expectedType);
|
|
136
|
-
}
|
|
137
|
-
// Contracts: check contract type name compatibility
|
|
138
|
-
if (c.type === "class" && c.label) {
|
|
139
|
-
return isTypeCompatible(`SEL_Contract_${c.label}`, expectedType);
|
|
140
|
-
}
|
|
141
|
-
// Functions — include (return type not easily available)
|
|
142
|
-
return true;
|
|
143
|
-
});
|
|
144
|
-
if (filtered.length > 0) {
|
|
145
|
-
const wordMatch = context.matchBefore(/\w+/);
|
|
146
|
-
const from = wordMatch?.from ?? treeCtx.from;
|
|
147
|
-
return { from, options: filtered, filter: true };
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
// No expected type or no compatible items — fall back to top-level
|
|
151
|
-
return this.handleTopLevel(context, {
|
|
152
|
-
kind: "top-level",
|
|
153
|
-
from: treeCtx.from,
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
handleTopLevel(context, treeCtx) {
|
|
157
|
-
const wordMatch = context.matchBefore(/\w+/);
|
|
158
|
-
const from = wordMatch?.from ?? treeCtx.from;
|
|
159
|
-
const hasText = !!wordMatch;
|
|
160
|
-
// Type-aware narrowing
|
|
161
|
-
const narrowed = this.narrowByExpectedType(context, from);
|
|
162
|
-
if (narrowed && narrowed.length > 0) {
|
|
163
|
-
return { from, options: narrowed, filter: true };
|
|
164
|
-
}
|
|
165
|
-
// Unfiltered top-level
|
|
166
|
-
if (hasText) {
|
|
167
|
-
return this.topLevelCompletions.length
|
|
168
|
-
? { from, options: this.topLevelCompletions, filter: true }
|
|
169
|
-
: null;
|
|
170
|
-
}
|
|
171
|
-
// Explicit activation (Ctrl+Space) with no text
|
|
172
|
-
if (context.explicit && this.topLevelCompletions.length) {
|
|
173
|
-
return {
|
|
174
|
-
from: context.pos,
|
|
175
|
-
options: this.topLevelCompletions,
|
|
176
|
-
filter: true,
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
return null;
|
|
180
|
-
}
|
|
181
|
-
/**
|
|
182
|
-
* Use the checker to infer the expected type at cursor position
|
|
183
|
-
* and filter completions to only type-compatible items.
|
|
184
|
-
* Returns undefined when narrowing is not possible.
|
|
185
|
-
*/
|
|
186
|
-
narrowByExpectedType(context, from) {
|
|
187
|
-
const doc = context.state.doc.toString();
|
|
188
|
-
const info = this.checker.completionsAt(doc, from);
|
|
189
|
-
if (!info.expectedType) {
|
|
190
|
-
return undefined;
|
|
191
|
-
}
|
|
192
|
-
const expectedType = info.expectedType;
|
|
193
|
-
// Filter checker items by type compatibility
|
|
194
|
-
const filtered = info.items
|
|
195
|
-
.filter((item) => isTypeCompatible(item.type, expectedType))
|
|
196
|
-
.map((item) => ({
|
|
197
|
-
label: item.label,
|
|
198
|
-
type: item.type.startsWith("SEL_Contract_")
|
|
199
|
-
? "class"
|
|
200
|
-
: item.detail
|
|
201
|
-
? "function"
|
|
202
|
-
: "variable",
|
|
203
|
-
detail: item.detail ?? item.type,
|
|
204
|
-
info: item.description,
|
|
205
|
-
}));
|
|
206
|
-
// Also include keywords when compatible
|
|
207
|
-
if (isTypeCompatible("bool", expectedType)) {
|
|
208
|
-
filtered.push({ label: "true", type: "keyword" }, { label: "false", type: "keyword" });
|
|
209
|
-
}
|
|
210
|
-
// null is dyn — always compatible
|
|
211
|
-
filtered.push({ label: "null", type: "keyword" });
|
|
212
|
-
return filtered.length > 0 ? filtered : undefined;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
export const createSchemaCompletion = (schema, checker) => {
|
|
216
|
-
const provider = new SchemaCompletionProvider(schema, checker);
|
|
217
|
-
return autocompletion({
|
|
218
|
-
override: [provider.completionSource],
|
|
219
|
-
});
|
|
220
|
-
};
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import type { EditorState } from "@codemirror/state";
|
|
2
|
-
/**
|
|
3
|
-
* Completion context extracted from the syntax tree at the cursor position.
|
|
4
|
-
*/
|
|
5
|
-
export type TreeCompletionContext = {
|
|
6
|
-
kind: "dot-access";
|
|
7
|
-
receiverText: string;
|
|
8
|
-
from: number;
|
|
9
|
-
} | {
|
|
10
|
-
kind: "call-arg";
|
|
11
|
-
functionName: string;
|
|
12
|
-
receiverName?: string;
|
|
13
|
-
paramIndex: number;
|
|
14
|
-
from: number;
|
|
15
|
-
} | {
|
|
16
|
-
kind: "top-level";
|
|
17
|
-
from: number;
|
|
18
|
-
};
|
|
19
|
-
/**
|
|
20
|
-
* Extract completion context from the Lezer syntax tree at the given position.
|
|
21
|
-
*/
|
|
22
|
-
export declare const getCompletionContext: (state: EditorState, pos: number) => TreeCompletionContext;
|
|
23
|
-
//# sourceMappingURL=tree-context.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tree-context.d.ts","sourceRoot":"","sources":["../../src/completion/tree-context.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AA+KrD;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAC9B;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC1D;IACA,IAAI,EAAE,UAAU,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACZ,GACD;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvC;;GAEG;AACH,eAAO,MAAM,oBAAoB,GAChC,OAAO,WAAW,EAClB,KAAK,MAAM,KACT,qBAsBF,CAAC"}
|
|
@@ -1,154 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
|
@@ -1,14 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
|
@@ -1,64 +0,0 @@
|
|
|
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
|
-
};
|
package/dist/editor/index.d.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
package/dist/editor/index.js
DELETED
package/dist/editor/theme.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
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"}
|
package/dist/editor/theme.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
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 });
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
|
@@ -1,75 +0,0 @@
|
|
|
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
|
-
}
|