Skip to content

Commit

Permalink
chore(website): error viewer in playground (#5061)
Browse files Browse the repository at this point in the history
* chore(website): add POC for error viewer in playground

* chore(website): correct colors

* chore(website): correct issue with optional fixers

* chore(website): expose fixers to errors-viewer

* chore(website): add simple locking system for fixers

* chore(website): change label apply to fix

* fix: correct small issue with compilerOptions not updating in eslint

* fix: apply changes requested in code review

* fix: apply changes from code review

* fix: add useMemo to cache model

* Update packages/website/src/components/ErrorsViewer.tsx

Co-authored-by: Josh Goldberg <me@joshuakgoldberg.com>

Co-authored-by: Josh Goldberg <me@joshuakgoldberg.com>
  • Loading branch information
armano2 and JoshuaKGoldberg committed May 26, 2022
1 parent 3cd54b7 commit eaa5d7b
Show file tree
Hide file tree
Showing 11 changed files with 238 additions and 55 deletions.
21 changes: 21 additions & 0 deletions packages/website/src/components/ErrorsViewer.module.css
@@ -0,0 +1,21 @@
.list {
font-family: var(--ifm-font-family-monospace);
background: transparent;
border: none;
padding-left: 1.5rem;
padding-right: 1.5rem;
font-size: 13px;
line-height: 18px;
letter-spacing: 0;
font-feature-settings: 'liga' 0, 'calt' 0;
box-sizing: border-box;
white-space: break-spaces;
margin: 0;
}

.fixer {
margin: 0.5rem 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
}
111 changes: 111 additions & 0 deletions packages/website/src/components/ErrorsViewer.tsx
@@ -0,0 +1,111 @@
import React, { useEffect, useMemo, useState } from 'react';
import type Monaco from 'monaco-editor';
import type { ErrorItem } from './types';

import styles from './ErrorsViewer.module.css';

export interface ErrorsViewerProps {
readonly value?: ErrorItem[];
}

export interface ErrorBlockProps {
readonly item: ErrorItem;
readonly setIsLocked: (value: boolean) => void;
readonly isLocked: boolean;
}

function severityClass(severity: Monaco.MarkerSeverity): string {
switch (severity) {
case 8:
return 'danger';
case 4:
return 'caution';
case 2:
return 'note';
}
return 'info';
}

function groupErrorItems(items: ErrorItem[]): [string, ErrorItem[]][] {
return Object.entries(
items.reduce<Record<string, ErrorItem[]>>((acc, obj) => {
if (!acc[obj.group]) {
acc[obj.group] = [];
}
acc[obj.group].push(obj);
return acc;
}, {}),
).sort(([a], [b]) => a.localeCompare(b));
}

function ErrorBlock({
item,
setIsLocked,
isLocked,
}: ErrorBlockProps): JSX.Element {
return (
<div className={`admonition alert alert--${severityClass(item.severity)}`}>
<div className="admonition-content">
<div className="row row--no-gutters">
<div className="col col--12">
{item.message} {item.location}
</div>
{item.hasFixers && (
<div className="col col--12">
{item.fixers.map((fixer, index) => (
<div key={index} className={styles.fixer}>
<span>&gt; {fixer.message}</span>
<button
className="button button--primary button--sm"
disabled={isLocked}
onClick={(): void => {
fixer.fix();
setIsLocked(true);
}}
>
fix
</button>
</div>
))}
</div>
)}
</div>
</div>
</div>
);
}

export default function ErrorsViewer({
value,
}: ErrorsViewerProps): JSX.Element {
const model = useMemo(
() => (value ? groupErrorItems(value) : undefined),
[value],
);

const [isLocked, setIsLocked] = useState(false);

useEffect(() => {
setIsLocked(false);
}, [value]);

return (
<div className={styles.list}>
{model?.map(([group, data]) => {
return (
<div className="margin-top--sm" key={group}>
<h4>{group}</h4>
{data.map((item, index) => (
<ErrorBlock
isLocked={isLocked}
setIsLocked={setIsLocked}
item={item}
key={index}
/>
))}
</div>
);
})}
</div>
);
}
4 changes: 0 additions & 4 deletions packages/website/src/components/Playground.module.css
Expand Up @@ -10,10 +10,6 @@
border: 1px solid var(--ifm-color-emphasis-100);
}

.sourceCodeStandalone {
width: 100%;
}

.codeBlocks {
display: flex;
flex-direction: row;
Expand Down
50 changes: 23 additions & 27 deletions packages/website/src/components/Playground.tsx
Expand Up @@ -20,6 +20,7 @@ import type { RuleDetails, SelectedRange } from './types';
import type { TSESTree } from '@typescript-eslint/utils';
import type { SourceFile } from 'typescript';
import ASTViewerScope from '@site/src/components/ASTViewerScope';
import ErrorsViewer from '@site/src/components/ErrorsViewer';

function rangeReducer<T extends SelectedRange | null>(
prevState: T,
Expand Down Expand Up @@ -52,6 +53,7 @@ function Playground(): JSX.Element {
const [esAst, setEsAst] = useState<TSESTree.Program | string | null>();
const [tsAst, setTsAST] = useState<SourceFile | string | null>();
const [scope, setScope] = useState<Record<string, unknown> | string | null>();
const [markers, setMarkers] = useState<Monaco.editor.IMarker[]>();
const [ruleNames, setRuleNames] = useState<RuleDetails[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [tsVersions, setTSVersion] = useState<readonly string[]>([]);
Expand All @@ -70,12 +72,7 @@ function Playground(): JSX.Element {
/>
</div>
<div className={styles.codeBlocks}>
<div
className={clsx(
styles.sourceCode,
state.showAST ? '' : styles.sourceCodeStandalone,
)}
>
<div className={clsx(styles.sourceCode)}>
{isLoading && <Loader />}
<EditorEmbed />
<LoadingEditor
Expand All @@ -90,6 +87,7 @@ function Playground(): JSX.Element {
onEsASTChange={setEsAst}
onTsASTChange={setTsAST}
onScopeChange={setScope}
onMarkersChange={setMarkers}
decoration={selectedRange}
onChange={(code): void => setState({ code: code })}
onLoaded={(ruleNames, tsVersions): void => {
Expand All @@ -100,31 +98,29 @@ function Playground(): JSX.Element {
onSelect={setPosition}
/>
</div>
{state.showAST && (
<div className={styles.astViewer}>
{(tsAst && state.showAST === 'ts' && (
<ASTViewerTS
value={tsAst}
<div className={styles.astViewer}>
{(tsAst && state.showAST === 'ts' && (
<ASTViewerTS
value={tsAst}
position={position}
onSelectNode={setSelectedRange}
/>
)) ||
(state.showAST === 'scope' && scope && (
<ASTViewerScope
value={scope}
position={position}
onSelectNode={setSelectedRange}
/>
)) ||
(state.showAST === 'scope' && scope && (
<ASTViewerScope
value={scope}
position={position}
onSelectNode={setSelectedRange}
/>
)) ||
(esAst && (
<ASTViewerESTree
value={esAst}
position={position}
onSelectNode={setSelectedRange}
/>
))}
</div>
)}
(state.showAST === 'es' && esAst && (
<ASTViewerESTree
value={esAst}
position={position}
onSelectNode={setSelectedRange}
/>
)) || <ErrorsViewer value={markers} />}
</div>
</div>
</div>
);
Expand Down
8 changes: 7 additions & 1 deletion packages/website/src/components/editor/LoadedEditor.tsx
Expand Up @@ -8,6 +8,7 @@ import type { WebLinter } from '../linter/WebLinter';
import { debounce } from '../lib/debounce';
import { lintCode, LintCodeAction } from '../linter/lintCode';
import { createProvideCodeActions } from './createProvideCodeActions';
import { parseMarkers } from '../linter/utils';

export interface LoadedEditorProps extends CommonEditorProps {
readonly main: typeof Monaco;
Expand All @@ -24,6 +25,7 @@ export const LoadedEditor: React.FC<LoadedEditorProps> = ({
onEsASTChange,
onScopeChange,
onTsASTChange,
onMarkersChange,
onChange,
onSelect,
rules,
Expand All @@ -34,7 +36,7 @@ export const LoadedEditor: React.FC<LoadedEditorProps> = ({
webLinter,
}) => {
const [decorations, setDecorations] = useState<string[]>([]);
const fixes = useRef(new Map<string, LintCodeAction>()).current;
const fixes = useRef(new Map<string, LintCodeAction[]>()).current;

useEffect(() => {
const config = {
Expand Down Expand Up @@ -112,6 +114,10 @@ export const LoadedEditor: React.FC<LoadedEditorProps> = ({
onChange(sandboxInstance.getModel().getValue());
}, 500),
),
sandboxInstance.monaco.editor.onDidChangeMarkers(() => {
const markers = sandboxInstance.monaco.editor.getModelMarkers({});
onMarkersChange(parseMarkers(markers, fixes, sandboxInstance.editor));
}),
];

return (): void => {
Expand Down
Expand Up @@ -3,7 +3,7 @@ import type { LintCodeAction } from '../linter/lintCode';
import { createURI } from '../linter/utils';

export function createProvideCodeActions(
fixes: Map<string, LintCodeAction>,
fixes: Map<string, LintCodeAction[]>,
): Monaco.languages.CodeActionProvider {
return {
provideCodeActions(
Expand All @@ -22,8 +22,8 @@ export function createProvideCodeActions(
}
const actions: Monaco.languages.CodeAction[] = [];
for (const marker of context.markers) {
const message = fixes.get(createURI(marker));
if (message) {
const messages = fixes.get(createURI(marker)) ?? [];
for (const message of messages) {
const start = model.getPositionAt(message.fix.range[0]);
const end = model.getPositionAt(message.fix.range[1]);
actions.push({
Expand Down
3 changes: 2 additions & 1 deletion packages/website/src/components/editor/types.ts
@@ -1,5 +1,5 @@
import type Monaco from 'monaco-editor';
import type { ConfigModel, SelectedRange } from '../types';
import type { ConfigModel, SelectedRange, ErrorItem } from '../types';
import type { TSESTree } from '@typescript-eslint/utils';
import type { SourceFile } from 'typescript';

Expand All @@ -10,5 +10,6 @@ export interface CommonEditorProps extends ConfigModel {
readonly onTsASTChange: (value: string | SourceFile) => void;
readonly onEsASTChange: (value: string | TSESTree.Program) => void;
readonly onScopeChange: (value: string | Record<string, unknown>) => void;
readonly onMarkersChange: (value: ErrorItem[]) => void;
readonly onSelect: (position: Monaco.Position | null) => void;
}
5 changes: 2 additions & 3 deletions packages/website/src/components/linter/WebLinter.ts
Expand Up @@ -40,7 +40,7 @@ export class WebLinter {

this.linter.defineParser(PARSER_NAME, {
parseForESLint: (text, options?: ParserOptions) => {
return this.eslintParse(text, compilerOptions, options);
return this.eslintParse(text, options);
},
});

Expand Down Expand Up @@ -70,7 +70,6 @@ export class WebLinter {

eslintParse(
code: string,
compilerOptions: CompilerOptions,
eslintOptions: ParserOptions = {},
): TSESLint.Linter.ESLintParseResult {
const isJsx = eslintOptions?.ecmaFeatures?.jsx ?? false;
Expand All @@ -80,7 +79,7 @@ export class WebLinter {

const program = window.ts.createProgram(
[fileName],
compilerOptions,
this.compilerOptions,
this.host,
);
const tsAst = program.getSourceFile(fileName)!;
Expand Down
29 changes: 13 additions & 16 deletions packages/website/src/components/linter/lintCode.ts
Expand Up @@ -11,7 +11,7 @@ export interface LintCodeAction {
};
}

export type LintCodeActionGroup = [string, LintCodeAction];
export type LintCodeActionGroup = [string, LintCodeAction[]];

export function lintCode(
linter: WebLinter,
Expand Down Expand Up @@ -59,27 +59,24 @@ export function lintCode(
};
const markerUri = createURI(marker);

const fixes: LintCodeAction[] = [];
if (message.fix) {
codeActions.push([
markerUri,
{
message: `Fix this ${message.ruleId ?? 'unknown'} problem`,
fix: message.fix,
},
]);
fixes.push({
message: `Fix this ${message.ruleId ?? 'unknown'} problem`,
fix: message.fix,
});
}
if (message.suggestions) {
for (const suggestion of message.suggestions) {
codeActions.push([
markerUri,
{
message: `${suggestion.desc} (${message.ruleId ?? 'unknown'})`,
fix: suggestion.fix,
},
]);
fixes.push({
message: `${suggestion.desc} (${message.ruleId ?? 'unknown'})`,
fix: suggestion.fix,
});
}
}

if (fixes.length > 0) {
codeActions.push([markerUri, fixes]);
}
markers.push(marker);
}

Expand Down

0 comments on commit eaa5d7b

Please sign in to comment.