Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(website): error viewer in playground #5061

Merged
merged 14 commits into from
May 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/website/data/sponsors.json
Original file line number Diff line number Diff line change
Expand Up @@ -229,4 +229,4 @@
"totalDonations": 1500,
"website": "https://www.mysportsinjury.co.uk"
}
]
]
bradzacher marked this conversation as resolved.
Show resolved Hide resolved
21 changes: 21 additions & 0 deletions packages/website/src/components/ErrorsViewer.module.css
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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]);
armano2 marked this conversation as resolved.
Show resolved Hide resolved

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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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