diff --git a/packages/website/src/components/ErrorsViewer.module.css b/packages/website/src/components/ErrorsViewer.module.css index fab012b69e88..22178ae72acd 100644 --- a/packages/website/src/components/ErrorsViewer.module.css +++ b/packages/website/src/components/ErrorsViewer.module.css @@ -13,9 +13,12 @@ margin: 0; } -.fixer { - margin: 0.5rem 1.5rem; +.fixerContainer { display: flex; justify-content: space-between; align-items: center; } + +.fixer { + margin: 0.5rem 0 0.5rem 1.5rem; +} diff --git a/packages/website/src/components/ErrorsViewer.tsx b/packages/website/src/components/ErrorsViewer.tsx index 566f1b83fdc4..6d7621d096aa 100644 --- a/packages/website/src/components/ErrorsViewer.tsx +++ b/packages/website/src/components/ErrorsViewer.tsx @@ -1,11 +1,13 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import type Monaco from 'monaco-editor'; -import type { ErrorItem } from './types'; +import clsx from 'clsx'; +import type { ErrorItem, ErrorGroup } from './types'; +import IconExternalLink from '@theme/IconExternalLink'; import styles from './ErrorsViewer.module.css'; export interface ErrorsViewerProps { - readonly value?: ErrorItem[]; + readonly value?: ErrorGroup[]; } export interface ErrorBlockProps { @@ -14,6 +16,12 @@ export interface ErrorBlockProps { readonly isLocked: boolean; } +export interface FixButtonProps { + readonly fix: () => void; + readonly setIsLocked: (value: boolean) => void; + readonly disabled: boolean; +} + function severityClass(severity: Monaco.MarkerSeverity): string { switch (severity) { case 8: @@ -26,16 +34,19 @@ function severityClass(severity: Monaco.MarkerSeverity): string { return 'info'; } -function groupErrorItems(items: ErrorItem[]): [string, ErrorItem[]][] { - return Object.entries( - items.reduce>((acc, obj) => { - if (!acc[obj.group]) { - acc[obj.group] = []; - } - acc[obj.group].push(obj); - return acc; - }, {}), - ).sort(([a], [b]) => a.localeCompare(b)); +function FixButton(props: FixButtonProps): JSX.Element { + return ( + + ); } function ErrorBlock({ @@ -46,30 +57,35 @@ function ErrorBlock({ return (
-
-
+
+
{item.message} {item.location}
- {item.hasFixers && ( -
- {item.fixers.map((fixer, index) => ( -
- > {fixer.message} - -
- ))} -
+ {item.fixer && ( + )}
+ {item.suggestions.length > 0 && ( +
+ {item.suggestions.map((fixer, index) => ( +
+ > {fixer.message} + +
+ ))} +
+ )}
); @@ -78,11 +94,6 @@ function ErrorBlock({ export default function ErrorsViewer({ value, }: ErrorsViewerProps): JSX.Element { - const model = useMemo( - () => (value ? groupErrorItems(value) : undefined), - [value], - ); - const [isLocked, setIsLocked] = useState(false); useEffect(() => { @@ -91,11 +102,21 @@ export default function ErrorsViewer({ return (
- {model?.map(([group, data]) => { + {value?.map(({ group, uri, items }) => { return (
-

{group}

- {data.map((item, index) => ( +

+ {group} + {uri && ( + <> + {' - '} + + docs + + + )} +

+ {items.map((item, index) => ( (); const [tsAst, setTsAST] = useState(); const [scope, setScope] = useState | null>(); - const [markers, setMarkers] = useState(); + const [markers, setMarkers] = useState(); const [ruleNames, setRuleNames] = useState([]); const [isLoading, setIsLoading] = useState(true); const [tsVersions, setTSVersion] = useState([]); diff --git a/packages/website/src/components/editor/LoadedEditor.tsx b/packages/website/src/components/editor/LoadedEditor.tsx index bdc0138f7ac2..f3ad36cab84b 100644 --- a/packages/website/src/components/editor/LoadedEditor.tsx +++ b/packages/website/src/components/editor/LoadedEditor.tsx @@ -129,7 +129,9 @@ export const LoadedEditor: React.FC = ({ const messages = webLinter.lint(code); - const markers = parseLintResults(messages, codeActions); + const markers = parseLintResults(messages, codeActions, ruleId => + sandboxInstance.monaco.Uri.parse(webLinter.rulesUrl.get(ruleId) ?? ''), + ); sandboxInstance.monaco.editor.setModelMarkers( tabs.code, diff --git a/packages/website/src/components/editor/createProvideCodeActions.ts b/packages/website/src/components/editor/createProvideCodeActions.ts index 2239a00459b6..26045a467939 100644 --- a/packages/website/src/components/editor/createProvideCodeActions.ts +++ b/packages/website/src/components/editor/createProvideCodeActions.ts @@ -28,9 +28,10 @@ export function createProvideCodeActions( const messages = fixes.get(createURI(marker)) ?? []; for (const message of messages) { actions.push({ - title: message.message, + title: message.message + (message.code ? ` (${message.code})` : ''), diagnostics: [marker], kind: 'quickfix', + isPreferred: message.isPreferred, edit: { edits: [ { diff --git a/packages/website/src/components/editor/types.ts b/packages/website/src/components/editor/types.ts index 578ef28d00b4..7b904b4bf5f8 100644 --- a/packages/website/src/components/editor/types.ts +++ b/packages/website/src/components/editor/types.ts @@ -1,5 +1,5 @@ import type Monaco from 'monaco-editor'; -import type { ConfigModel, SelectedRange, ErrorItem, TabType } from '../types'; +import type { ConfigModel, SelectedRange, ErrorGroup, TabType } from '../types'; import type { TSESTree } from '@typescript-eslint/utils'; import type { SourceFile } from 'typescript'; @@ -11,6 +11,6 @@ export interface CommonEditorProps extends ConfigModel { readonly onTsASTChange: (value: undefined | SourceFile) => void; readonly onEsASTChange: (value: undefined | TSESTree.Program) => void; readonly onScopeChange: (value: undefined | Record) => void; - readonly onMarkersChange: (value: ErrorItem[]) => void; + readonly onMarkersChange: (value: ErrorGroup[]) => void; readonly onSelect: (position: Monaco.Position | null) => void; } diff --git a/packages/website/src/components/linter/WebLinter.ts b/packages/website/src/components/linter/WebLinter.ts index de29ed72502c..eaece6e236f0 100644 --- a/packages/website/src/components/linter/WebLinter.ts +++ b/packages/website/src/components/linter/WebLinter.ts @@ -35,7 +35,8 @@ export class WebLinter { private lintUtils: LintUtils; private rules: TSESLint.Linter.RulesRecord = {}; - public ruleNames: { name: string; description?: string }[]; + public readonly ruleNames: { name: string; description?: string }[] = []; + public readonly rulesUrl = new Map(); constructor( system: System, @@ -54,11 +55,12 @@ export class WebLinter { }, }); - this.ruleNames = Array.from(this.linter.getRules()).map(value => { - return { - name: value[0], - description: value[1]?.meta?.docs?.description, - }; + this.linter.getRules().forEach((item, name) => { + this.ruleNames.push({ + name: name, + description: item.meta?.docs?.description, + }); + this.rulesUrl.set(name, item.meta?.docs?.url); }); } diff --git a/packages/website/src/components/linter/utils.ts b/packages/website/src/components/linter/utils.ts index d51d86b0867a..8f8d0f23fbdd 100644 --- a/packages/website/src/components/linter/utils.ts +++ b/packages/website/src/components/linter/utils.ts @@ -1,9 +1,11 @@ import type Monaco from 'monaco-editor'; -import type { ErrorItem } from '../types'; +import type { ErrorGroup } from '../types'; import type { TSESLint } from '@typescript-eslint/utils'; export interface LintCodeAction { message: string; + code?: string | null; + isPreferred: boolean; fix: { range: Readonly<[number, number]>; text: string; @@ -45,48 +47,76 @@ export function createEditOperation( }; } +function normalizeCode(code: Monaco.editor.IMarker['code']): { + value: string; + target?: string; +} { + if (!code) { + return { value: '' }; + } + if (typeof code === 'string') { + return { value: code }; + } + return { + value: code.value, + target: code.target.toString(), + }; +} + export function parseMarkers( markers: Monaco.editor.IMarker[], fixes: Map, editor: Monaco.editor.IStandaloneCodeEditor, -): ErrorItem[] { - return markers.map(marker => { - const code = - typeof marker.code === 'string' ? marker.code : marker.code?.value ?? ''; +): ErrorGroup[] { + const result: Record = {}; + for (const marker of markers) { + const code = normalizeCode(marker.code); const uri = createURI(marker); const fixers = - fixes.get(uri)?.map(item => { - return { - message: item.message, - fix(): void { - editor.executeEdits('eslint', [ - createEditOperation(editor.getModel()!, item), - ]); - }, - }; - }) ?? []; - - return { - group: - marker.owner === 'eslint' - ? code - : marker.owner === 'typescript' - ? 'TypeScript' - : marker.owner, + fixes.get(uri)?.map(item => ({ + message: item.message, + isPreferred: item.isPreferred, + fix(): void { + editor.executeEdits('eslint', [ + createEditOperation(editor.getModel()!, item), + ]); + }, + })) ?? []; + + const group = + marker.owner === 'eslint' + ? code.value + : marker.owner === 'typescript' + ? 'TypeScript' + : marker.owner; + + if (!result[group]) { + result[group] = { + group: group, + uri: code.target, + items: [], + }; + } + + result[group].items.push({ message: - (marker.owner !== 'eslint' && code ? `${code}: ` : '') + marker.message, + (marker.owner !== 'eslint' && code ? `${code.value}: ` : '') + + marker.message, location: `${marker.startLineNumber}:${marker.startColumn} - ${marker.endLineNumber}:${marker.endColumn}`, severity: marker.severity, - hasFixers: fixers.length > 0, - fixers: fixers, - }; - }); + fixer: fixers.find(item => item.isPreferred), + suggestions: fixers.filter(item => !item.isPreferred), + }); + } + + return Object.values(result).sort((a, b) => a.group.localeCompare(b.group)); } export function parseLintResults( messages: TSESLint.Linter.LintMessage[], codeActions: Map, + ruleUri: (ruleId: string) => Monaco.Uri, ): Monaco.editor.IMarkerData[] { const markers: Monaco.editor.IMarkerData[] = []; @@ -99,7 +129,12 @@ export function parseLintResults( const endColumn = ensurePositiveInt(message.endColumn, startColumn + 1); const marker: Monaco.editor.IMarkerData = { - code: message.ruleId ?? 'FATAL', + code: message.ruleId + ? { + value: message.ruleId, + target: ruleUri(message.ruleId), + } + : 'FATAL', severity: message.severity === 2 ? 8 // MarkerSeverity.Error @@ -118,13 +153,16 @@ export function parseLintResults( fixes.push({ message: `Fix this ${message.ruleId ?? 'unknown'} problem`, fix: message.fix, + isPreferred: true, }); } if (message.suggestions) { for (const suggestion of message.suggestions) { fixes.push({ - message: `${suggestion.desc} (${message.ruleId ?? 'unknown'})`, + message: suggestion.desc, + code: message.ruleId, fix: suggestion.fix, + isPreferred: false, }); } } diff --git a/packages/website/src/components/types.ts b/packages/website/src/components/types.ts index ef6cf70dcad4..b63d9bca307b 100644 --- a/packages/website/src/components/types.ts +++ b/packages/website/src/components/types.ts @@ -35,12 +35,17 @@ export interface SelectedRange { } export interface ErrorItem { - group: string; message: string; location: string; severity: number; - hasFixers: boolean; - fixers: { message: string; fix(): void }[]; + suggestions: { message: string; fix(): void }[]; + fixer?: { message: string; fix(): void }; +} + +export interface ErrorGroup { + group: string; + uri?: string; + items: ErrorItem[]; } export type EslintRC = Record & { rules: RulesRecord };