Skip to content

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

Merged
merged 17 commits into from
Sep 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/website/src/components/Playground.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@

.tabCode {
height: calc(100% - 41px);
overflow: auto;
}

.hidden {
Expand Down
41 changes: 22 additions & 19 deletions packages/website/src/components/Playground.tsx
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';
Expand All @@ -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);
Expand Down Expand Up @@ -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}>
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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
Copy link
Collaborator Author

@armano2 armano2 Apr 4, 2023

Choose a reason for hiding this comment

The 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 and/or condition chain is getting complex

Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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 good first issue for a potential contributor?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filed #7630. On second thought I think it's probably not a good first issue given it's touching a bunch of code.

<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}
Expand Down
10 changes: 3 additions & 7 deletions packages/website/src/components/editor/LoadedEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,7 @@ export const LoadedEditor: React.FC<LoadedEditorProps> = ({
eslintrc,
selectedRange,
fileType,
onEsASTChange,
onScopeChange,
onTsASTChange,
onASTChange,
onMarkersChange,
onChange,
onSelect,
Expand Down Expand Up @@ -140,12 +138,10 @@ export const LoadedEditor: React.FC<LoadedEditorProps> = ({

useEffect(() => {
const disposable = webLinter.onParse((uri, model) => {
onEsASTChange(model.storedAST);
onScopeChange(model.storedScope as Record<string, unknown> | undefined);
onTsASTChange(model.storedTsAST);
onASTChange(model);
});
return () => disposable();
}, [webLinter, onEsASTChange, onScopeChange, onTsASTChange]);
}, [webLinter, onASTChange]);

useEffect(() => {
const createRuleUri = (name: string): string =>
Expand Down
8 changes: 2 additions & 6 deletions packages/website/src/components/editor/types.ts
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;
}
1 change: 1 addition & 0 deletions packages/website/src/components/hooks/useHashState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ function readShowAST(value: string | null): ConfigShowAst {
case 'es':
case 'ts':
case 'scope':
case 'types':
return value;
}
return value ? 'es' : false;
Expand Down
2 changes: 1 addition & 1 deletion packages/website/src/components/linter/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
1 change: 1 addition & 0 deletions packages/website/src/components/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
];

/**
Expand Down
77 changes: 77 additions & 0 deletions packages/website/src/components/typeDetails/SimplifiedTreeView.tsx
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>
);
}
141 changes: 141 additions & 0 deletions packages/website/src/components/typeDetails/TypeInfo.tsx
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;
} 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>
);
}
Loading