-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
chore(website): [playground] add types tab to playground #6843
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
Changes from all commits
5cf6bcd
5a74892
45a6b97
d721e16
14e4323
bec2c64
808b578
969800c
af69a7e
ed72ba3
dc87da0
f1c0cdb
df49214
fc44aa2
70fc82c
266f8d1
f36c655
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -52,6 +52,7 @@ | |
|
||
.tabCode { | ||
height: calc(100% - 41px); | ||
overflow: auto; | ||
} | ||
|
||
.hidden { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<TSESTree.Program | null>(); | ||
const [tsAst, setTsAST] = useState<SourceFile | null>(); | ||
const [scope, setScope] = useState<Record<string, unknown> | null>(); | ||
const [astModel, setAstModel] = useState<UpdateModel>(); | ||
const [markers, setMarkers] = useState<ErrorGroup[]>(); | ||
const [ruleNames, setRuleNames] = useState<RuleDetails[]>([]); | ||
const [isLoading, setIsLoading] = useState<boolean>(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 ( | ||
<div className={styles.codeContainer}> | ||
<div className={styles.codeBlocks}> | ||
|
@@ -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 && ( | ||
<TypesDetails | ||
typeChecker={astModel.typeChecker} | ||
value={astModel.storedTsAST} | ||
onHoverNode={setSelectedRange} | ||
cursorPosition={position} | ||
/> | ||
)) || | ||
(state.showAST && astModel && ( | ||
Comment on lines
+159
to
+167
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i have a feeling that i should extract this to separate component, this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah this is probably getting to the point where we might want to extract the condition into a bit of state machine logic which outputs an enum value so that we can simplify the render logic. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed. I don't personally think of it as a blocker but it would be a nice to have. Maybe a good follow-up There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Filed #7630. On second thought I think it's probably not a |
||
<ASTViewer | ||
key={String(state.showAST)} | ||
filter={state.showAST === 'es' ? esQueryFilter : undefined} | ||
value={astToShow} | ||
value={ | ||
state.showAST === 'ts' | ||
? astModel.storedTsAST | ||
: state.showAST === 'scope' | ||
? astModel.storedScope | ||
: state.showAST === 'es' | ||
? astModel.storedAST | ||
: undefined | ||
} | ||
showTokens={state.showTokens} | ||
enableScrolling={state.scroll} | ||
cursorPosition={position} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<ConfigModel>) => void; | ||
readonly onTsASTChange: (value: SourceFile | undefined) => void; | ||
readonly onEsASTChange: (value: TSESTree.Program | undefined) => void; | ||
readonly onScopeChange: (value: Record<string, unknown> | undefined) => void; | ||
readonly onASTChange: (value: undefined | UpdateModel) => void; | ||
readonly onMarkersChange: (value: ErrorGroup[]) => void; | ||
readonly onSelect: (position?: number) => void; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<div className={styles.nonExpand}> | ||
<span className={selectedNode === value ? styles.selected : ''}> | ||
<PropertyName | ||
value={tsEnumToString('SyntaxKind', value.kind)} | ||
className={styles.propName} | ||
onHover={onHover} | ||
onClick={(): void => { | ||
onSelect(value); | ||
}} | ||
/> | ||
</span> | ||
|
||
<div className={clsx(styles.subList, 'padding-left--md')}> | ||
{items.map((item, index) => ( | ||
<SimplifiedItem | ||
onHoverNode={onHoverNode} | ||
selectedNode={selectedNode} | ||
value={item} | ||
onSelect={onSelect} | ||
key={index.toString()} | ||
/> | ||
))} | ||
</div> | ||
</div> | ||
); | ||
} | ||
|
||
export function SimplifiedTreeView( | ||
params: SimplifiedTreeViewProps, | ||
): React.JSX.Element { | ||
return ( | ||
<div className={clsx(styles.list, 'padding-left--sm')}> | ||
<SimplifiedItem {...params} /> | ||
</div> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<div className={astStyles.list}> | ||
<span className={astStyles.propClass}>{props.label}</span> | ||
<span>: </span> | ||
<span className={astStyles.propString}>{String(props.value)}</span> | ||
</div> | ||
); | ||
} | ||
|
||
function TypeGroup(props: TypeGroupProps): React.JSX.Element { | ||
return ( | ||
<> | ||
<h4 className="padding--sm margin--none">{props.label}</h4> | ||
{props.type ? ( | ||
<> | ||
{props.string && ( | ||
<SimpleField value={props.string} label="typeToString()" /> | ||
)} | ||
<ASTViewer onHoverNode={props.onHoverNode} value={props.type} /> | ||
</> | ||
) : ( | ||
<div className={astStyles.list}>None</div> | ||
)} | ||
</> | ||
); | ||
} | ||
|
||
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; | ||
armano2 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} 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 <div>TypeChecker not available</div>; | ||
} | ||
|
||
return ( | ||
<div> | ||
<> | ||
<h4 className="padding--sm margin--none">Node</h4> | ||
<ASTViewer onHoverNode={onHoverNode} value={value} /> | ||
<TypeGroup | ||
label="Type" | ||
type={computed.type} | ||
string={computed.typeString} | ||
onHoverNode={onHoverNode} | ||
/> | ||
<TypeGroup | ||
label="Contextual Type" | ||
type={computed.contextualType} | ||
string={computed.contextualTypeString} | ||
onHoverNode={onHoverNode} | ||
/> | ||
<TypeGroup | ||
label="Symbol" | ||
type={computed.symbol} | ||
onHoverNode={onHoverNode} | ||
/> | ||
<TypeGroup | ||
label="Signature" | ||
type={computed.signature} | ||
onHoverNode={onHoverNode} | ||
/> | ||
<TypeGroup | ||
label="FlowNode" | ||
type={computed.flowNode} | ||
onHoverNode={onHoverNode} | ||
/> | ||
</> | ||
</div> | ||
); | ||
} |
Uh oh!
There was an error while loading. Please reload this page.