Skip to content

Commit 1102a4b

Browse files
committed
feat: Add esquery selector textfield & highlighting of matched code
Fixes eslint#79
1 parent 89a913c commit 1102a4b

16 files changed

+192
-9
lines changed

package-lock.json

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,7 @@
126126
.cm-tooltip.cm-tooltip-autocomplete > ul {
127127
border-radius: 0.125rem;
128128
}
129+
130+
.eslint-code-explorer_highlight-range {
131+
@apply bg-editorRangeHighlightColor;
132+
}

src/App.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,29 @@
11
import "./App.css";
2+
23
import { Navbar } from "./components/navbar";
34
import { useExplorer } from "./hooks/use-explorer";
5+
import { useHighlightRanges } from "./hooks/use-highlight-ranges";
46
import { tools } from "./lib/tools";
57
import { Editor } from "./components/editor";
8+
import { EsquerySelectorInput } from "./components/esquery-selector-input";
69
import { ToolSelector } from "./components/tool-selector";
710
import { ThemeProvider } from "./components/theme-provider";
811
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
912

1013
function App() {
11-
const { language, tool, code, setCode } = useExplorer();
14+
const { language, tool, code, setCode, jsOptions } = useExplorer();
15+
16+
const highlightRanges = useHighlightRanges();
17+
1218
const activeTool = tools.find(({ value }) => value === tool) ?? tools[0];
1319
return (
1420
<ThemeProvider>
1521
<div className="antialiased touch-manipulation font-sans">
1622
<div className="flex flex-col h-screen">
1723
<Navbar />
24+
{jsOptions.esquerySelectorEnabled && (
25+
<EsquerySelectorInput />
26+
)}
1827
<div className="h-full overflow-hidden">
1928
<div className="border-t h-full">
2029
<PanelGroup
@@ -24,6 +33,7 @@ function App() {
2433
<Panel defaultSize={50} minSize={25}>
2534
<Editor
2635
value={code[language]}
36+
highlightRanges={highlightRanges}
2737
onChange={value => {
2838
setCode({
2939
...code,

src/codemirror-themes.css

Lines changed: 2 additions & 0 deletions
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-range-highlight-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-range-highlight-color: 209deg 54% 31%;
9799
}

src/components/ast/javascript-ast.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { JavascriptAstTreeItem } from "./javascript-ast-tree-item";
66
import type { FC } from "react";
77
import { parseError } from "@/lib/parse-error";
88
import { ErrorState } from "../error-boundary";
9+
import { parseJavascriptAST } from "@/lib/parse-javascript-ast";
910

1011
export const JavascriptAst: FC = () => {
1112
const explorer = useExplorer();
@@ -15,12 +16,9 @@ export const JavascriptAst: FC = () => {
1516
let tree: ReturnType<typeof espree.parse> | null = null;
1617

1718
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-
},
19+
tree = parseJavascriptAST({
20+
code: explorer.code.javascript,
21+
jsOptions: explorer.jsOptions,
2422
});
2523

2624
ast = JSON.stringify(tree, null, 2);

src/components/editor.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ import {
1616
ESLintPlaygroundTheme,
1717
ESLintPlaygroundHighlightStyle,
1818
} from "@/utils/codemirror-themes";
19+
import {
20+
highlightRangesExtension,
21+
type HighlightRange,
22+
} from "@/utils/highlight-ranges";
1923

2024
const languageExtensions: Record<string, (isJSX?: boolean) => LanguageSupport> =
2125
{
@@ -28,10 +32,16 @@ const languageExtensions: Record<string, (isJSX?: boolean) => LanguageSupport> =
2832
type EditorProperties = {
2933
readOnly?: boolean;
3034
value?: string;
35+
highlightRanges?: HighlightRange[];
3136
onChange?: (value: string) => void;
3237
};
3338

34-
export const Editor: FC<EditorProperties> = ({ readOnly, value, onChange }) => {
39+
export const Editor: FC<EditorProperties> = ({
40+
readOnly,
41+
value,
42+
highlightRanges = [],
43+
onChange,
44+
}) => {
3545
const { wrap, language, jsOptions } = useExplorer();
3646
const { isJSX } = jsOptions;
3747
const [isDragOver, setIsDragOver] = useState<boolean>(false);
@@ -50,6 +60,7 @@ export const Editor: FC<EditorProperties> = ({ readOnly, value, onChange }) => {
5060
readOnly ? EditorState.readOnly.of(true) : [],
5161
ESLintPlaygroundTheme,
5262
ESLintPlaygroundHighlightStyle,
63+
highlightRangesExtension(highlightRanges),
5364
];
5465

5566
const debouncedOnChange = useCallback(
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { useId, type FC } from "react";
2+
import { Label } from "@/components/ui/label";
3+
import { TextField } from "@/components/ui/text-field";
4+
import { useExplorer } from "@/hooks/use-explorer";
5+
6+
export const EsquerySelectorInput: FC = () => {
7+
const { esquerySelector, setEsquerySelector } = useExplorer();
8+
const htmlId = useId();
9+
10+
return (
11+
<div className="space-y-1.5 m-2">
12+
<Label htmlFor={htmlId}>esquery Selector</Label>
13+
<TextField
14+
id={htmlId}
15+
className="w-full"
16+
value={esquerySelector}
17+
onChange={e => setEsquerySelector(e.target.value)}
18+
/>
19+
</div>
20+
);
21+
};

src/components/options.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ const CssPanel: React.FC = () => {
8787
const JavaScriptPanel = () => {
8888
const explorer = useExplorer();
8989
const { jsOptions, setJsOptions } = explorer;
90-
const { parser, sourceType, esVersion, isJSX } = jsOptions;
90+
const { parser, sourceType, esVersion, isJSX, esquerySelectorEnabled } =
91+
jsOptions;
9192
return (
9293
<>
9394
<LabeledSelect
@@ -137,6 +138,20 @@ const JavaScriptPanel = () => {
137138
/>
138139
<Label htmlFor="jsx">JSX</Label>
139140
</div>
141+
142+
<div className="flex items-center gap-1.5">
143+
<Switch
144+
id="esquerySelector"
145+
checked={esquerySelectorEnabled}
146+
onCheckedChange={(value: boolean) => {
147+
setJsOptions({
148+
...jsOptions,
149+
esquerySelectorEnabled: value,
150+
});
151+
}}
152+
/>
153+
<Label htmlFor="esquerySelector">esquery Selector</Label>
154+
</div>
140155
</>
141156
);
142157
};

src/components/ui/text-field.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import * as React from "react";
2+
3+
import { cn } from "@/lib/utils";
4+
5+
export type TextFieldProps = Exclude<
6+
React.InputHTMLAttributes<HTMLInputElement>,
7+
"type"
8+
>;
9+
10+
const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
11+
({ className, ...props }, ref) => {
12+
return (
13+
<input
14+
type="text"
15+
className={cn(
16+
"flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2",
17+
className,
18+
)}
19+
ref={ref}
20+
{...props}
21+
/>
22+
);
23+
},
24+
);
25+
TextField.displayName = "TextField";
26+
27+
export { TextField };

0 commit comments

Comments
 (0)