diff --git a/packages/website/src/components/Playground.module.css b/packages/website/src/components/Playground.module.css index e61a35bad03c..b824d441fafc 100644 --- a/packages/website/src/components/Playground.module.css +++ b/packages/website/src/components/Playground.module.css @@ -52,6 +52,7 @@ .tabCode { height: calc(100% - 41px); + overflow: auto; } .hidden { diff --git a/packages/website/src/components/Playground.tsx b/packages/website/src/components/Playground.tsx index 4f3a74bd5255..ef45fecab317 100644 --- a/packages/website/src/components/Playground.tsx +++ b/packages/website/src/components/Playground.tsx @@ -1,8 +1,6 @@ -import type { TSESTree } from '@typescript-eslint/utils'; import clsx from 'clsx'; import type * as ESQuery from 'esquery'; import React, { useCallback, useState } from 'react'; -import type { SourceFile } from 'typescript'; import ASTViewer from './ast/ASTViewer'; import ConfigEslint from './config/ConfigEslint'; @@ -14,17 +12,17 @@ import { ESQueryFilter } from './ESQueryFilter'; import useHashState from './hooks/useHashState'; import EditorTabs from './layout/EditorTabs'; import Loader from './layout/Loader'; +import type { UpdateModel } from './linter/types'; import { defaultConfig, detailTabs } from './options'; import OptionsSelector from './OptionsSelector'; import styles from './Playground.module.css'; import ConditionalSplitPane from './SplitPane/ConditionalSplitPane'; +import { TypesDetails } from './typeDetails/TypesDetails'; import type { ErrorGroup, RuleDetails, SelectedRange, TabType } from './types'; function Playground(): React.JSX.Element { const [state, setState] = useHashState(defaultConfig); - const [esAst, setEsAst] = useState(); - const [tsAst, setTsAST] = useState(); - const [scope, setScope] = useState | null>(); + const [astModel, setAstModel] = useState(); const [markers, setMarkers] = useState(); const [ruleNames, setRuleNames] = useState([]); const [isLoading, setIsLoading] = useState(true); @@ -62,15 +60,6 @@ function Playground(): React.JSX.Element { } }, []); - const astToShow = - state.showAST === 'ts' - ? tsAst - : state.showAST === 'scope' - ? scope - : state.showAST === 'es' - ? esAst - : undefined; - return (
@@ -137,9 +126,7 @@ function Playground(): React.JSX.Element { eslintrc={state.eslintrc} sourceType={state.sourceType} showAST={state.showAST} - onEsASTChange={setEsAst} - onTsASTChange={setTsAST} - onScopeChange={setScope} + onASTChange={setAstModel} onMarkersChange={setMarkers} selectedRange={selectedRange} onChange={setState} @@ -169,11 +156,27 @@ function Playground(): React.JSX.Element { value={esQueryError} /> )) || - (state.showAST && astToShow && ( + (state.showAST === 'types' && astModel?.storedTsAST && ( + + )) || + (state.showAST && astModel && ( = ({ eslintrc, selectedRange, fileType, - onEsASTChange, - onScopeChange, - onTsASTChange, + onASTChange, onMarkersChange, onChange, onSelect, @@ -140,12 +138,10 @@ export const LoadedEditor: React.FC = ({ useEffect(() => { const disposable = webLinter.onParse((uri, model) => { - onEsASTChange(model.storedAST); - onScopeChange(model.storedScope as Record | undefined); - onTsASTChange(model.storedTsAST); + onASTChange(model); }); return () => disposable(); - }, [webLinter, onEsASTChange, onScopeChange, onTsASTChange]); + }, [webLinter, onASTChange]); useEffect(() => { const createRuleUri = (name: string): string => diff --git a/packages/website/src/components/editor/types.ts b/packages/website/src/components/editor/types.ts index bc7b886f535f..e8933ce19f42 100644 --- a/packages/website/src/components/editor/types.ts +++ b/packages/website/src/components/editor/types.ts @@ -1,15 +1,11 @@ -import type { TSESTree } from '@typescript-eslint/utils'; -import type { SourceFile } from 'typescript'; - +import type { UpdateModel } from '../linter/types'; import type { ConfigModel, ErrorGroup, SelectedRange, TabType } from '../types'; export interface CommonEditorProps extends ConfigModel { readonly activeTab: TabType; readonly selectedRange?: SelectedRange; readonly onChange: (cfg: Partial) => void; - readonly onTsASTChange: (value: SourceFile | undefined) => void; - readonly onEsASTChange: (value: TSESTree.Program | undefined) => void; - readonly onScopeChange: (value: Record | undefined) => void; + readonly onASTChange: (value: undefined | UpdateModel) => void; readonly onMarkersChange: (value: ErrorGroup[]) => void; readonly onSelect: (position?: number) => void; } diff --git a/packages/website/src/components/hooks/useHashState.ts b/packages/website/src/components/hooks/useHashState.ts index 70293527f188..95573ba375b4 100644 --- a/packages/website/src/components/hooks/useHashState.ts +++ b/packages/website/src/components/hooks/useHashState.ts @@ -21,6 +21,7 @@ function readShowAST(value: string | null): ConfigShowAst { case 'es': case 'ts': case 'scope': + case 'types': return value; } return value ? 'es' : false; diff --git a/packages/website/src/components/linter/types.ts b/packages/website/src/components/linter/types.ts index 540ad0e45218..12e4c9654d41 100644 --- a/packages/website/src/components/linter/types.ts +++ b/packages/website/src/components/linter/types.ts @@ -8,7 +8,7 @@ export type { ParseSettings } from '@typescript-eslint/typescript-estree/use-at- export interface UpdateModel { storedAST?: TSESTree.Program; - storedTsAST?: ts.SourceFile; + storedTsAST?: ts.Node; storedScope?: ScopeManager; typeChecker?: ts.TypeChecker; } diff --git a/packages/website/src/components/options.ts b/packages/website/src/components/options.ts index 07e50240fb3d..f52e7784bc37 100644 --- a/packages/website/src/components/options.ts +++ b/packages/website/src/components/options.ts @@ -6,6 +6,7 @@ export const detailTabs: { value: ConfigShowAst; label: string }[] = [ { value: 'es', label: 'ESTree' }, { value: 'ts', label: 'TypeScript' }, { value: 'scope', label: 'Scope' }, + { value: 'types', label: 'Types' }, ]; /** diff --git a/packages/website/src/components/typeDetails/SimplifiedTreeView.tsx b/packages/website/src/components/typeDetails/SimplifiedTreeView.tsx new file mode 100644 index 000000000000..c5dc1b37926d --- /dev/null +++ b/packages/website/src/components/typeDetails/SimplifiedTreeView.tsx @@ -0,0 +1,77 @@ +import clsx from 'clsx'; +import React, { useCallback, useMemo } from 'react'; +import type * as ts from 'typescript'; + +import styles from '../ast/ASTViewer.module.css'; +import PropertyName from '../ast/PropertyName'; +import { tsEnumToString } from '../ast/tsUtils'; +import type { OnHoverNodeFn } from '../ast/types'; +import { getRange, isTSNode } from '../ast/utils'; + +export interface SimplifiedTreeViewProps { + readonly value: ts.Node; + readonly selectedNode: ts.Node | undefined; + readonly onSelect: (value: ts.Node) => void; + readonly onHoverNode?: OnHoverNodeFn; +} + +function SimplifiedItem({ + value, + onSelect, + selectedNode, + onHoverNode, +}: SimplifiedTreeViewProps): React.JSX.Element { + const items = useMemo(() => { + const result: ts.Node[] = []; + value.forEachChild(child => { + result.push(child); + }); + return result; + }, [value]); + + const onHover = useCallback( + (v: boolean) => { + if (isTSNode(value) && onHoverNode) { + return onHoverNode(v ? getRange(value, 'tsNode') : undefined); + } + }, + [onHoverNode, value], + ); + + return ( +
+ + { + onSelect(value); + }} + /> + + +
+ {items.map((item, index) => ( + + ))} +
+
+ ); +} + +export function SimplifiedTreeView( + params: SimplifiedTreeViewProps, +): React.JSX.Element { + return ( +
+ +
+ ); +} diff --git a/packages/website/src/components/typeDetails/TypeInfo.tsx b/packages/website/src/components/typeDetails/TypeInfo.tsx new file mode 100644 index 000000000000..87084650d124 --- /dev/null +++ b/packages/website/src/components/typeDetails/TypeInfo.tsx @@ -0,0 +1,141 @@ +import React, { useMemo } from 'react'; +import type * as ts from 'typescript'; + +import ASTViewer from '../ast/ASTViewer'; +import astStyles from '../ast/ASTViewer.module.css'; +import type { OnHoverNodeFn } from '../ast/types'; + +export interface TypeInfoProps { + readonly value: ts.Node; + readonly typeChecker?: ts.TypeChecker; + readonly onHoverNode?: OnHoverNodeFn; +} + +interface InfoModel { + type?: unknown; + typeString?: string; + contextualType?: unknown; + contextualTypeString?: string; + symbol?: unknown; + signature?: unknown; + flowNode?: unknown; +} + +interface SimpleFieldProps { + readonly value: string | undefined; + readonly label: string; +} + +interface TypeGroupProps { + readonly label: string; + readonly type?: unknown; + readonly string?: string; + readonly onHoverNode?: OnHoverNodeFn; +} + +function SimpleField(props: SimpleFieldProps): React.JSX.Element { + return ( +
+ {props.label} + : + {String(props.value)} +
+ ); +} + +function TypeGroup(props: TypeGroupProps): React.JSX.Element { + return ( + <> +

{props.label}

+ {props.type ? ( + <> + {props.string && ( + + )} + + + ) : ( +
None
+ )} + + ); +} + +export function TypeInfo({ + value, + typeChecker, + onHoverNode, +}: TypeInfoProps): React.JSX.Element { + const computed = useMemo(() => { + if (!typeChecker || !value) { + return undefined; + } + const info: InfoModel = {}; + try { + const type = typeChecker.getTypeAtLocation(value); + info.type = type; + info.typeString = typeChecker.typeToString(type); + info.symbol = type.getSymbol(); + let signature = type.getCallSignatures(); + if (signature.length === 0) { + signature = type.getConstructSignatures(); + } + info.signature = signature.length > 0 ? signature : undefined; + // @ts-expect-error not part of public api + info.flowNode = value.flowNode ?? value.endFlowNode ?? undefined; + } catch (e: unknown) { + info.type = e; + } + try { + // @ts-expect-error just fail if a node type is not correct + const contextualType = typeChecker.getContextualType(value); + info.contextualType = contextualType; + if (contextualType) { + info.contextualTypeString = typeChecker.typeToString(contextualType); + } + } catch { + info.contextualType = undefined; + } + return info; + }, [value, typeChecker]); + + if (!typeChecker || !computed) { + return
TypeChecker not available
; + } + + return ( +
+ <> +

Node

+ + + + + + + +
+ ); +} diff --git a/packages/website/src/components/typeDetails/TypesDetails.tsx b/packages/website/src/components/typeDetails/TypesDetails.tsx new file mode 100644 index 000000000000..28dd2f824deb --- /dev/null +++ b/packages/website/src/components/typeDetails/TypesDetails.tsx @@ -0,0 +1,62 @@ +import React, { useEffect, useState } from 'react'; +import type * as ts from 'typescript'; + +import { findSelectionPath } from '../ast/selectedRange'; +import type { OnHoverNodeFn } from '../ast/types'; +import { isTSNode } from '../ast/utils'; +import styles from '../Playground.module.css'; +import ConditionalSplitPane from '../SplitPane/ConditionalSplitPane'; +import { SimplifiedTreeView } from './SimplifiedTreeView'; +import { TypeInfo } from './TypeInfo'; + +export interface TypesDetailsProps { + readonly value: ts.Node; + readonly typeChecker?: ts.TypeChecker; + readonly cursorPosition?: number; + readonly onHoverNode?: OnHoverNodeFn; +} + +export function TypesDetails({ + cursorPosition, + value, + typeChecker, + onHoverNode, +}: TypesDetailsProps): React.JSX.Element { + const [selectedNode, setSelectedNode] = useState(value); + + useEffect(() => { + if (cursorPosition) { + const item = findSelectionPath(value, cursorPosition); + if (item.node && isTSNode(item.node)) { + setSelectedNode(item.node); + } + } + }, [cursorPosition, value]); + + return ( + +
+ +
+ {selectedNode && ( +
+ +
+ )} +
+ ); +} diff --git a/packages/website/src/components/types.ts b/packages/website/src/components/types.ts index 219443b6b952..4b99af65c887 100644 --- a/packages/website/src/components/types.ts +++ b/packages/website/src/components/types.ts @@ -17,7 +17,7 @@ export type TabType = 'code' | 'eslintrc' | 'tsconfig'; export type ConfigFileType = `${ts.Extension}`; -export type ConfigShowAst = 'es' | 'scope' | 'ts' | false; +export type ConfigShowAst = 'es' | 'scope' | 'ts' | 'types' | false; export interface ConfigModel { fileType?: ConfigFileType; pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy