Skip to content

Commit a37148f

Browse files
committed
Use editor markers to show syntax error
1 parent 95604fd commit a37148f

File tree

3 files changed

+74
-14
lines changed

3 files changed

+74
-14
lines changed

src/App.tsx

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,8 @@ export default function App() {
1919
const [highlightedIntervals, setHighlightedIntervals] = useState<HighlightedIntervals>([]);
2020
const [query, setQuery] = useState<string>('');
2121
const [code, setCode] = useState<string>('');
22-
const [syntaxError, setSyntaxError] = useState<Error>();
2322

2423
const handleQueryChange = useCallback((value: string | undefined) => {
25-
setSyntaxError(undefined);
2624
if (value) {
2725
setQuery(value);
2826
}
@@ -38,16 +36,14 @@ export default function App() {
3836
if (!query) {
3937
return;
4038
}
41-
setSyntaxError(undefined);
4239
setHighlightedIntervals([]);
4340

4441
try {
4542
const nodes = queryCode(code, query);
4643
const highlightedIntervals = nodes.map((node) => mapNodeToHighlightInterval(node));
4744
setHighlightedIntervals(highlightedIntervals);
4845
} catch (error) {
49-
if (isSyntaxError(error)) {
50-
setSyntaxError(error);
46+
if (error instanceof SyntaxError) {
5147
return;
5248
}
5349
throw error;
@@ -66,17 +62,12 @@ export default function App() {
6662
</header>
6763
<h2>Query</h2>
6864
<QueryEditor onChange={handleQueryChange} />
69-
{syntaxError && syntaxError.toString()}
7065
<h2>Code</h2>
7166
<Code highlighted={highlightedIntervals} onChange={handleCodeChange} />
7267
</div>
7368
);
7469
}
7570

76-
function isSyntaxError(error: unknown): error is Error & { name: 'SyntaxError' } {
77-
return (error as { name: string }).name === 'SyntaxError';
78-
}
79-
8071
function mapNodeToHighlightInterval(node: Node): HighlightedInterval {
8172
const fullText = node.getFullText();
8273
const leadingWhitespaceOffset = getFirstMatchLengthOrZero(fullText, REG_EXPS.LeadingWhitespace);

src/QueryEditor.tsx

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,96 @@
1-
import { FC, useCallback } from 'react';
1+
import { FC, useCallback, useRef } from 'react';
22
import Editor, { EditorProps } from '@monaco-editor/react';
33
import type * as Monaco from 'monaco-editor';
4+
import { tsquery } from '@phenomnomnominal/tsquery';
5+
import { sanitizeQuery } from './engine';
46

5-
const QueryEditor: FC<Omit<EditorProps, 'defaultLanguage' | 'theme' | 'options' | 'onMount'>> = (props) => {
7+
const MONACO_MODEL_MARKER_OWNER = 'tsquery';
8+
9+
const QueryEditor: FC<Omit<EditorProps, 'defaultLanguage' | 'theme' | 'options' | 'onMount'>> = ({
10+
onChange,
11+
...rest
12+
}) => {
13+
const monacoRef = useRef<typeof Monaco>();
14+
const editorRef = useRef<Monaco.editor.IStandaloneCodeEditor>();
615
const handleQueryMount = useCallback((editor: Monaco.editor.IStandaloneCodeEditor, monaco: typeof Monaco) => {
16+
monacoRef.current = monaco;
17+
editorRef.current = editor;
718
monaco.languages.css.cssDefaults.setOptions({
819
validate: false,
920
});
1021
}, []);
1122

23+
const handleQueryChange = useCallback(
24+
(value: string | undefined, event: Monaco.editor.IModelContentChangedEvent) => {
25+
const monaco = monacoRef.current;
26+
const editor = editorRef.current;
27+
const model = editor?.getModel();
28+
if (monaco && model && typeof value === 'string') {
29+
const sanitizedQuery = sanitizeQuery(value);
30+
const error = getSyntaxError(sanitizedQuery);
31+
if (!error) {
32+
monaco.editor.setModelMarkers(model, MONACO_MODEL_MARKER_OWNER, []);
33+
} else {
34+
if (model) {
35+
monaco.editor.setModelMarkers(model, MONACO_MODEL_MARKER_OWNER, [
36+
{
37+
...getFullRange(model),
38+
severity: monaco.MarkerSeverity.Error,
39+
message: error.message,
40+
},
41+
]);
42+
}
43+
}
44+
}
45+
46+
if (onChange) {
47+
onChange(value, event);
48+
}
49+
},
50+
[onChange],
51+
);
52+
1253
return (
1354
<Editor
1455
defaultLanguage="css"
1556
theme="vs-dark"
1657
onMount={handleQueryMount}
58+
onChange={handleQueryChange}
1759
options={{
1860
minimap: {
1961
enabled: false,
2062
},
2163
}}
22-
{...props}
64+
{...rest}
2365
/>
2466
);
2567
};
2668

2769
export default QueryEditor;
70+
71+
function getSyntaxError(query: string): Error | null {
72+
try {
73+
tsquery.parse(query);
74+
return null;
75+
} catch (error) {
76+
if (error instanceof SyntaxError) {
77+
return error;
78+
}
79+
throw error;
80+
}
81+
}
82+
83+
function getFullRange(model: Monaco.editor.ITextModel): {
84+
startLineNumber: number;
85+
startColumn: number;
86+
endLineNumber: number;
87+
endColumn: number;
88+
} {
89+
const lastLine = model.getLineCount();
90+
return {
91+
startLineNumber: 0,
92+
startColumn: 0,
93+
endLineNumber: lastLine,
94+
endColumn: model.getLineMaxColumn(lastLine),
95+
};
96+
}

src/engine.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export function queryCode(code: string, query: string): Node[] {
2424
return nonEmptyNodes;
2525
}
2626

27-
function sanitizeQuery(query: string): string {
27+
export function sanitizeQuery(query: string): string {
2828
return query.replace(REG_EXPS.AllLineBreaks, ' ').replace(REG_EXPS.TrailingCommaAndWhitespace, '').trim();
2929
}
3030

0 commit comments

Comments
 (0)