Skip to content

Commit 3c65d55

Browse files
committed
feat: simplify editor config API
1 parent 52c4adf commit 3c65d55

File tree

9 files changed

+119
-87
lines changed

9 files changed

+119
-87
lines changed

packages/sel-editor-react/src/sel-editor.spec.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,13 @@ describe("src/sel-editor.tsx", () => {
5757
expect(container.querySelector(".cm-editor")).toBeTruthy();
5858
});
5959

60-
it("accepts validate prop for error highlighting", () => {
61-
const validate = vi.fn().mockReturnValue([]);
60+
it("accepts checkerOptions prop", () => {
6261
const { container } = render(
63-
<SELEditor schema={testSchema} value="test" validate={validate} />,
62+
<SELEditor
63+
schema={testSchema}
64+
value="test"
65+
checkerOptions={{ rules: [] }}
66+
/>,
6467
);
6568
expect(container.querySelector(".cm-editor")).toBeTruthy();
6669
});

packages/sel-editor-react/src/sel-editor.stories.tsx

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { SELChecker, rules } from "@seljs/checker";
1+
import { rules } from "@seljs/checker";
22
import { buildSchema } from "@seljs/env";
33
import { parseAbi } from "viem";
44

@@ -93,56 +93,43 @@ const WithValidation: Story = {
9393
},
9494
};
9595

96-
const checkerWithRules = new SELChecker(erc20Schema, {
97-
rules: rules.builtIn,
98-
});
99-
100-
const conditionChecker = new SELChecker(erc20Schema, {
101-
rules: [rules.requireType("bool"), ...rules.builtIn],
102-
});
103-
10496
const WithLintRules: Story = {
10597
name: "Lint Rules (Redundant Bool)",
10698
args: {
10799
value: "erc20.balanceOf(user) > 0 == true",
108-
validate: (expression: string) =>
109-
checkerWithRules.check(expression).diagnostics,
100+
checkerOptions: { rules: rules.builtIn },
110101
},
111102
};
112103

113104
const WithLintSelfComparison: Story = {
114105
name: "Lint Rules (Self Comparison)",
115106
args: {
116107
value: "erc20.totalSupply() == erc20.totalSupply()",
117-
validate: (expression: string) =>
118-
checkerWithRules.check(expression).diagnostics,
108+
checkerOptions: { rules: rules.builtIn },
119109
},
120110
};
121111

122112
const WithLintConstantCondition: Story = {
123113
name: "Lint Rules (Constant Condition)",
124114
args: {
125115
value: "true && erc20.balanceOf(user) > 0",
126-
validate: (expression: string) =>
127-
checkerWithRules.check(expression).diagnostics,
116+
checkerOptions: { rules: rules.builtIn },
128117
},
129118
};
130119

131120
const RequireTypeBoolPass: Story = {
132121
name: "Require Type Bool (Pass)",
133122
args: {
134123
value: "erc20.balanceOf(user) > 0",
135-
validate: (expression: string) =>
136-
conditionChecker.check(expression).diagnostics,
124+
checkerOptions: { rules: [rules.requireType("bool"), ...rules.builtIn] },
137125
},
138126
};
139127

140128
const RequireTypeBoolFail: Story = {
141129
name: "Require Type Bool (Fail)",
142130
args: {
143131
value: "erc20.balanceOf(user)",
144-
validate: (expression: string) =>
145-
conditionChecker.check(expression).diagnostics,
132+
checkerOptions: { rules: [rules.requireType("bool"), ...rules.builtIn] },
146133
},
147134
};
148135

@@ -197,15 +184,15 @@ const WithTypeDisplay: Story = {
197184
name: "Type Display",
198185
args: {
199186
value: "erc20.balanceOf(user) > 0",
200-
showType: true,
187+
features: { typeDisplay: true },
201188
},
202189
};
203190

204191
const TypeDisplayDark: Story = {
205192
name: "Type Display (Dark)",
206193
args: {
207194
value: "erc20.balanceOf(user)",
208-
showType: true,
195+
features: { typeDisplay: true },
209196
dark: true,
210197
},
211198
decorators: [

packages/sel-editor-react/src/use-sel-editor.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ describe("src/use-sel-editor.ts", () => {
104104
});
105105
});
106106

107-
expect(onChange).toHaveBeenCalledWith("hello");
107+
expect(onChange).toHaveBeenCalledWith("hello", expect.any(Boolean));
108108

109109
document.body.removeChild(container);
110110
});

packages/sel-editor-react/src/use-sel-editor.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export interface UseSELEditorResult {
1111
ref: (node: HTMLElement | null) => void;
1212
view: EditorView | null;
1313
value: string;
14+
valid: boolean;
1415
}
1516

1617
export function useSELEditor(
@@ -19,6 +20,7 @@ export function useSELEditor(
1920
const viewRef = useRef<EditorView | null>(null);
2021
const [view, setView] = useState<EditorView | null>(null);
2122
const [value, setValue] = useState(config.value ?? "");
23+
const [valid, setValid] = useState(true);
2224
const configRef = useRef(config);
2325
configRef.current = config;
2426

@@ -38,9 +40,10 @@ export function useSELEditor(
3840
const editor = createSELEditor({
3941
...currentConfig,
4042
parent: node,
41-
onChange: (newValue) => {
43+
onChange: (newValue, isValid) => {
4244
setValue(newValue);
43-
currentConfig.onChange?.(newValue);
45+
setValid(isValid);
46+
currentConfig.onChange?.(newValue, isValid);
4447
},
4548
});
4649
viewRef.current = editor;
@@ -56,5 +59,5 @@ export function useSELEditor(
5659
};
5760
}, []);
5861

59-
return { ref, view, value };
62+
return { ref, view, value, valid };
6063
}

packages/sel-editor-react/src/whatsabi-explorer.stories.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -334,8 +334,10 @@ function WhatsAbiExplorer({
334334
schema={status.schema}
335335
value={expression}
336336
placeholder={`Try ${contractName.trim() || "contract"}.<method>(...)`}
337-
onChange={setExpression}
338-
showType
337+
onChange={(value) => {
338+
setExpression(value);
339+
}}
340+
features={{ typeDisplay: true }}
339341
/>
340342

341343
<div style={{ marginTop: 12 }}>

packages/sel-editor/src/editor/create-editor.spec.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ describe("src/editor/create-editor.ts", () => {
8989
view.destroy();
9090
});
9191

92-
it("onChange fires with current value on document changes", () => {
92+
it("onChange fires with current value and validity on document changes", () => {
9393
const onChange = vi.fn();
9494
const parent = createParent();
9595
const view = createSELEditor({
@@ -103,7 +103,7 @@ describe("src/editor/create-editor.ts", () => {
103103
changes: { from: 0, insert: "test" },
104104
});
105105

106-
expect(onChange).toHaveBeenCalledWith("test");
106+
expect(onChange).toHaveBeenCalledWith("test", expect.any(Boolean));
107107
view.destroy();
108108
});
109109

@@ -123,17 +123,15 @@ describe("src/editor/create-editor.ts", () => {
123123
view.destroy();
124124
});
125125

126-
it("passing validate enables the lint extension", () => {
127-
const validate = vi.fn().mockReturnValue([]);
126+
it("passing checkerOptions configures the internal checker", () => {
128127
const parent = createParent();
129128
const view = createSELEditor({
130129
parent,
131130
schema: testSchema,
132131
value: "test",
133-
validate,
132+
checkerOptions: { rules: [] },
134133
});
135134

136-
// Linter extension should be present (no errors thrown)
137135
expect(view.state).toBeDefined();
138136
view.destroy();
139137
});
@@ -195,14 +193,25 @@ describe("src/editor/create-editor.ts", () => {
195193
view2.destroy();
196194
});
197195

198-
it("accepts additional extensions", () => {
196+
it("features can disable linting", () => {
199197
const parent = createParent();
198+
const view = createSELEditor({
199+
parent,
200+
schema: testSchema,
201+
features: { linting: false },
202+
});
203+
204+
expect(view).toBeDefined();
205+
view.destroy();
206+
});
200207

201-
// Pass an empty array of extensions (no-op but shouldn't break)
208+
it("features can enable type display", () => {
209+
const parent = createParent();
202210
const view = createSELEditor({
203211
parent,
204212
schema: testSchema,
205-
extensions: [],
213+
value: "erc20.balanceOf(user)",
214+
features: { typeDisplay: true },
206215
});
207216

208217
expect(view).toBeDefined();

packages/sel-editor/src/editor/editor-config.ts

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { bracketMatching } from "@codemirror/language";
44
import { EditorState, type Extension } from "@codemirror/state";
55
import { EditorView, keymap, placeholder } from "@codemirror/view";
66
import { celLanguageSupport } from "@seljs/cel-lezer";
7-
import { SELChecker, rules } from "@seljs/checker";
7+
import { SELChecker } from "@seljs/checker";
88

99
import { selDarkTheme, selLightTheme } from "./theme";
1010
import { createTypeDisplay } from "./type-display";
@@ -13,23 +13,35 @@ import { createTokenizerConfig } from "../language";
1313
import { createSemanticHighlighter } from "../language/semantic-highlighter";
1414
import { createSELLinter } from "../linting";
1515

16-
import type { SELEditorConfig } from "./types";
16+
import type { SELEditorConfig, SELEditorFeatures } from "./types";
17+
18+
const resolveFeatures = (features?: SELEditorFeatures) => ({
19+
linting: features?.linting ?? true,
20+
autocomplete: features?.autocomplete ?? true,
21+
semanticHighlighting: features?.semanticHighlighting ?? true,
22+
typeDisplay: features?.typeDisplay ?? false,
23+
});
1724

1825
export const buildExtensions = (config: SELEditorConfig): Extension[] => {
19-
const checker = new SELChecker(config.schema, { rules: [...rules.builtIn] });
26+
const checker = new SELChecker(config.schema, config.checkerOptions);
27+
const resolved = resolveFeatures(config.features);
2028
const extensions: Extension[] = [];
2129

2230
// Language support (includes syntax highlighting)
2331
extensions.push(celLanguageSupport(config.dark));
2432
extensions.push(bracketMatching());
2533

2634
// Semantic highlighting (schema-aware identifier coloring)
27-
const tokenizerConfig = createTokenizerConfig(config.schema);
28-
extensions.push(createSemanticHighlighter(tokenizerConfig, config.dark));
35+
if (resolved.semanticHighlighting) {
36+
const tokenizerConfig = createTokenizerConfig(config.schema);
37+
extensions.push(createSemanticHighlighter(tokenizerConfig, config.dark));
38+
}
2939

3040
// Autocomplete (type-aware via checker)
31-
extensions.push(createSchemaCompletion(config.schema, checker));
32-
extensions.push(closeBrackets());
41+
if (resolved.autocomplete) {
42+
extensions.push(createSchemaCompletion(config.schema, checker));
43+
extensions.push(closeBrackets());
44+
}
3345

3446
// Keybindings
3547
extensions.push(
@@ -40,24 +52,24 @@ export const buildExtensions = (config: SELEditorConfig): Extension[] => {
4052
// Theme
4153
extensions.push(config.dark ? selDarkTheme : selLightTheme);
4254

43-
// Validation / linting (built-in checker used when no validate callback provided)
44-
const validate =
45-
config.validate ??
46-
((expression: string) => checker.check(expression).diagnostics);
47-
extensions.push(
48-
createSELLinter({
49-
validate,
50-
delay: config.validateDelay,
51-
}),
52-
);
55+
// Validation / linting
56+
if (resolved.linting) {
57+
extensions.push(
58+
createSELLinter({
59+
validate: (expression: string) => checker.check(expression).diagnostics,
60+
}),
61+
);
62+
}
5363

54-
// onChange listener
64+
// onChange listener with validity
5565
if (config.onChange) {
5666
const onChange = config.onChange;
5767
extensions.push(
5868
EditorView.updateListener.of((update) => {
5969
if (update.docChanged) {
60-
onChange(update.state.doc.toString());
70+
const value = update.state.doc.toString();
71+
const result = checker.check(value);
72+
onChange(value, result.valid);
6173
}
6274
}),
6375
);
@@ -74,14 +86,9 @@ export const buildExtensions = (config: SELEditorConfig): Extension[] => {
7486
}
7587

7688
// Type display panel
77-
if (config.showType) {
89+
if (resolved.typeDisplay) {
7890
extensions.push(createTypeDisplay(checker, config.dark ?? false));
7991
}
8092

81-
// User-provided extensions (last, so they can override)
82-
if (config.extensions) {
83-
extensions.push(...config.extensions);
84-
}
85-
8693
return extensions;
8794
};
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
export { createSELEditor } from "./create-editor";
2-
export { buildExtensions } from "./editor-config";
32
export { selLightTheme, selDarkTheme } from "./theme";
4-
export type { SELEditorConfig } from "./types";
3+
export type { SELEditorConfig, SELEditorFeatures } from "./types";
54
export type { EditorView } from "@codemirror/view";

0 commit comments

Comments
 (0)