Skip to content

Commit f407323

Browse files
authored
feat: Add esquery selector textfield & highlighting of matched code (#80)
1 parent cb1bc5c commit f407323

25 files changed

+575
-132
lines changed

package-lock.json

+12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
"eslint-plugin-react-hooks": "^4.6.2",
6565
"eslint-scope": "^8.1.0",
6666
"espree": "^10.1.0",
67+
"esquery": "^1.6.0",
6768
"global": "^4.4.0",
6869
"graphviz-react": "^1.2.5",
6970
"lucide-react": "^0.407.0",
@@ -77,6 +78,7 @@
7778
"devDependencies": {
7879
"@types/eslint-scope": "^3.7.7",
7980
"@types/espree": "^10.0.0",
81+
"@types/esquery": "^1.5.4",
8082
"@types/node": "^18.19.44",
8183
"@types/react": "^18.3.3",
8284
"@types/react-dom": "^18.3.0",

src/App.css

+4
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242
--scrollbar-track: 0deg 0% 93.33%;
4343
--scrollbar-thumb: 0deg 0% 53.33%;
4444
--scrollbar-thumb-hover: 0deg 0% 33.33%;
45+
46+
--nonmatching-esquery-selector: var(--color-rose-200);
4547
}
4648

4749
.dark {
@@ -79,6 +81,8 @@
7981
--scrollbar-track: 0deg 0% 18.04%;
8082
--scrollbar-thumb: 0deg 0% 53.33%;
8183
--scrollbar-thumb-hover: 0deg 0% 33.33%;
84+
85+
--nonmatching-esquery-selector: var(--color-rose-900);
8286
}
8387
}
8488

src/App.tsx

+14
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,18 @@ import { Navbar } from "./components/navbar";
33
import { useExplorer } from "./hooks/use-explorer";
44
import { tools } from "./lib/tools";
55
import { Editor } from "./components/editor";
6+
import { EsquerySelectorInput } from "./components/esquery-selector-input";
67
import { ToolSelector } from "./components/tool-selector";
78
import { ThemeProvider } from "./components/theme-provider";
89
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
10+
import { useAST } from "@/hooks/use-ast";
11+
import { convertNodesToRanges } from "@/lib/convert-nodes-to-ranges";
912

1013
function App() {
1114
const { language, tool, code, setCode } = useExplorer();
15+
16+
const astParseResult = useAST();
17+
1218
const activeTool = tools.find(({ value }) => value === tool) ?? tools[0];
1319
return (
1420
<ThemeProvider>
@@ -22,8 +28,16 @@ function App() {
2228
className="border-t h-full"
2329
>
2430
<Panel defaultSize={50} minSize={25}>
31+
<EsquerySelectorInput />
2532
<Editor
2633
value={code[language]}
34+
highlightedRanges={
35+
astParseResult.ok
36+
? convertNodesToRanges(
37+
astParseResult.esqueryMatchedNodes,
38+
)
39+
: undefined
40+
}
2741
onChange={value => {
2842
setCode({
2943
...code,

src/codemirror-themes.css

+2
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
--editor-bracket-match-outline-color: var(--color-neutral-200);
8181
--editor-bracket-match-background-color: var(--color-neutral-100);
8282
--editor-bracket-match-color: none;
83+
--editor-highlighted-range-color: 209deg 54% 81%;
8384
}
8485

8586
.dark {
@@ -94,4 +95,5 @@
9495
--editor-bracket-match-outline-color: var(--color-neutral-600);
9596
--editor-bracket-match-background-color: var(--color-neutral-700);
9697
--editor-bracket-match-color: var(--color-neutral-25);
98+
--editor-highlighted-range-color: 209deg 54% 31%;
9799
}

src/components/ast/css-ast-tree-item.tsx

+32-18
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,48 @@ import {
55
} from "@/components/ui/accordion";
66
import { TreeEntry } from "../tree-entry";
77
import type { FC } from "react";
8+
import { cn } from "@/lib/utils";
89

910
type ASTNode = {
1011
readonly type: string;
1112
readonly [key: string]: unknown;
1213
};
1314

14-
type CssAstTreeItemProperties = {
15+
export type CssAstTreeItemProperties = {
1516
readonly index: number;
1617
readonly data: ASTNode;
18+
readonly esqueryMatchedNodes: ASTNode[];
1719
};
1820

1921
export const CssAstTreeItem: FC<CssAstTreeItemProperties> = ({
2022
data,
2123
index,
22-
}) => (
23-
<AccordionItem
24-
value={`${index}-${data.type}`}
25-
className="border border-card rounded-lg overflow-hidden"
26-
>
27-
<AccordionTrigger className="text-sm bg-card px-4 py-3 capitalize">
28-
{data.type}
29-
</AccordionTrigger>
30-
<AccordionContent className="p-4 border-t">
31-
<div className="space-y-1">
32-
{Object.entries(data).map(item => (
33-
<TreeEntry key={item[0]} data={item} />
34-
))}
35-
</div>
36-
</AccordionContent>
37-
</AccordionItem>
38-
);
24+
esqueryMatchedNodes,
25+
}) => {
26+
const isEsqueryMatchedNode = esqueryMatchedNodes.includes(data);
27+
28+
return (
29+
<AccordionItem
30+
value={`${index}-${data.type}`}
31+
className={cn(
32+
"border border-card rounded-lg overflow-hidden",
33+
isEsqueryMatchedNode && "border-primary border-4",
34+
)}
35+
>
36+
<AccordionTrigger className="text-sm bg-card px-4 py-3 capitalize">
37+
{data.type}
38+
</AccordionTrigger>
39+
<AccordionContent className="p-4 border-t">
40+
<div className="space-y-1">
41+
{Object.entries(data).map(item => (
42+
<TreeEntry
43+
key={item[0]}
44+
data={item}
45+
esqueryMatchedNodes={esqueryMatchedNodes}
46+
/>
47+
))}
48+
</div>
49+
</AccordionContent>
50+
</AccordionItem>
51+
);
52+
};

src/components/ast/css-ast.tsx

+14-12
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,19 @@
1-
import css from "@eslint/css";
21
import { Accordion } from "@/components/ui/accordion";
32
import { Editor } from "@/components/editor";
3+
import { useAST } from "@/hooks/use-ast";
44
import { useExplorer } from "@/hooks/use-explorer";
5-
import { CssAstTreeItem } from "./css-ast-tree-item";
5+
import {
6+
CssAstTreeItem,
7+
type CssAstTreeItemProperties,
8+
} from "./css-ast-tree-item";
69
import type { FC } from "react";
710
import { parseError } from "@/lib/parse-error";
811
import { ErrorState } from "../error-boundary";
912

1013
export const CssAst: FC = () => {
11-
const { code, cssOptions, viewModes } = useExplorer();
14+
const result = useAST();
15+
const { viewModes } = useExplorer();
1216
const { astView } = viewModes;
13-
const { cssMode, tolerant } = cssOptions;
14-
const language = css.languages[cssMode];
15-
const result = language.parse(
16-
{ body: code.css },
17-
{
18-
languageOptions: { tolerant },
19-
},
20-
);
2117

2218
if (!result.ok) {
2319
const message = parseError(result.errors[0]);
@@ -33,7 +29,13 @@ export const CssAst: FC = () => {
3329
className="px-8 font-mono space-y-3"
3430
defaultValue={["0-StyleSheet"]}
3531
>
36-
<CssAstTreeItem data={result.ast} index={0} />
32+
<CssAstTreeItem
33+
data={result.ast as CssAstTreeItemProperties["data"]}
34+
index={0}
35+
esqueryMatchedNodes={
36+
result.esqueryMatchedNodes as CssAstTreeItemProperties["esqueryMatchedNodes"]
37+
}
38+
/>
3739
</Accordion>
3840
);
3941
}

src/components/ast/javascript-ast-tree-item.tsx

+32-18
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,45 @@ import {
66
import { TreeEntry } from "../tree-entry";
77
import type { FC } from "react";
88
import type * as espree from "espree";
9+
import { cn } from "@/lib/utils";
910

10-
type JavascriptAstTreeItemProperties = {
11+
export type JavascriptAstTreeItemProperties = {
1112
readonly index: number;
1213
readonly data:
1314
| ReturnType<typeof espree.parse>
1415
| ReturnType<typeof espree.parse>["body"][number];
16+
readonly esqueryMatchedNodes: unknown[];
1517
};
1618

1719
export const JavascriptAstTreeItem: FC<JavascriptAstTreeItemProperties> = ({
1820
data,
1921
index,
20-
}) => (
21-
<AccordionItem
22-
value={`${index}-${data.type}`}
23-
className="border border-card rounded-lg overflow-hidden"
24-
>
25-
<AccordionTrigger className="text-sm bg-card px-4 py-3 capitalize">
26-
{data.type}
27-
</AccordionTrigger>
28-
<AccordionContent className="p-4 border-t">
29-
<div className="space-y-1">
30-
{Object.entries(data).map(item => (
31-
<TreeEntry key={item[0]} data={item} />
32-
))}
33-
</div>
34-
</AccordionContent>
35-
</AccordionItem>
36-
);
22+
esqueryMatchedNodes,
23+
}) => {
24+
const isEsqueryMatchedNode = esqueryMatchedNodes.includes(data);
25+
26+
return (
27+
<AccordionItem
28+
value={`${index}-${data.type}`}
29+
className={cn(
30+
"border border-card rounded-lg overflow-hidden",
31+
isEsqueryMatchedNode && "border-primary border-4",
32+
)}
33+
>
34+
<AccordionTrigger className="text-sm bg-card px-4 py-3 capitalize">
35+
{data.type}
36+
</AccordionTrigger>
37+
<AccordionContent className="p-4 border-t">
38+
<div className="space-y-1">
39+
{Object.entries(data).map(item => (
40+
<TreeEntry
41+
key={item[0]}
42+
data={item}
43+
esqueryMatchedNodes={esqueryMatchedNodes}
44+
/>
45+
))}
46+
</div>
47+
</AccordionContent>
48+
</AccordionItem>
49+
);
50+
};

src/components/ast/javascript-ast.tsx

+19-18
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,30 @@
1-
import * as espree from "espree";
21
import { Accordion } from "@/components/ui/accordion";
32
import { Editor } from "@/components/editor";
43
import { useExplorer } from "@/hooks/use-explorer";
5-
import { JavascriptAstTreeItem } from "./javascript-ast-tree-item";
4+
import { useAST } from "@/hooks/use-ast";
5+
import {
6+
JavascriptAstTreeItem,
7+
type JavascriptAstTreeItemProperties,
8+
} from "./javascript-ast-tree-item";
69
import type { FC } from "react";
710
import { parseError } from "@/lib/parse-error";
811
import { ErrorState } from "../error-boundary";
912

1013
export const JavascriptAst: FC = () => {
14+
const result = useAST();
1115
const explorer = useExplorer();
1216
const { viewModes } = explorer;
1317
const { astView } = viewModes;
14-
let ast = "";
15-
let tree: ReturnType<typeof espree.parse> | null = null;
1618

17-
try {
18-
tree = espree.parse(explorer.code.javascript, {
19-
ecmaVersion: explorer.jsOptions.esVersion,
20-
sourceType: explorer.jsOptions.sourceType,
21-
ecmaFeatures: {
22-
jsx: explorer.jsOptions.isJSX,
23-
},
24-
});
25-
26-
ast = JSON.stringify(tree, null, 2);
27-
} catch (error) {
28-
const message = parseError(error);
19+
if (!result.ok) {
20+
const message = parseError(result.errors[0]);
2921
return <ErrorState message={message} />;
3022
}
23+
24+
const ast = JSON.stringify(result.ast, null, 2);
25+
3126
if (astView === "tree") {
32-
if (tree === null) {
27+
if (result.ast === null) {
3328
return null;
3429
}
3530

@@ -39,7 +34,13 @@ export const JavascriptAst: FC = () => {
3934
className="px-8 font-mono space-y-3"
4035
defaultValue={["0-Program"]}
4136
>
42-
<JavascriptAstTreeItem data={tree} index={0} />
37+
<JavascriptAstTreeItem
38+
data={result.ast as JavascriptAstTreeItemProperties["data"]}
39+
index={0}
40+
esqueryMatchedNodes={
41+
result.esqueryMatchedNodes as JavascriptAstTreeItemProperties["esqueryMatchedNodes"]
42+
}
43+
/>
4344
</Accordion>
4445
);
4546
}

0 commit comments

Comments
 (0)