Skip to content

Commit

Permalink
feat: show type info for type literals
Browse files Browse the repository at this point in the history
  • Loading branch information
mxsdev committed Oct 16, 2022
1 parent 9eecac9 commit 770dfba
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 10 deletions.
2 changes: 1 addition & 1 deletion packages/api/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export { APIConfig } from './config'
export { getSymbolType, multilineTypeToString, pseudoBigIntToString, getNodeType, getNodeSymbol } from "./util"
export { getSymbolType, multilineTypeToString, pseudoBigIntToString, getNodeType, getNodeSymbol, getDescendantAtPosition, getDescendantAtRange } from "./util"
export { recursivelyExpandType } from "./merge"
export { generateTypeTree, getTypeInfoChildren } from "./tree"
export { TypeInfo, SymbolInfo, SignatureInfo, TypeId, IndexInfo, TypeInfoKind, TypeParameterInfo } from "./types"
Expand Down
70 changes: 66 additions & 4 deletions packages/api/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,23 +78,29 @@ export function getSymbolType(typeChecker: ts.TypeChecker, symbol: ts.Symbol, lo
return symbolType;
}

return typeChecker.getTypeOfSymbolAtLocation(symbol, { parent: {} } as unknown as ts.Node)
const fallbackType = typeChecker.getTypeOfSymbolAtLocation(symbol, { parent: {} } as unknown as ts.Node)
return fallbackType
}

export function getNodeSymbol(typeChecker: ts.TypeChecker, node: ts.Node): ts.Symbol|undefined {
return (node as ts.Node & { symbol?: TSSymbol }).symbol ?? typeChecker.getSymbolAtLocation(node)
}

export function getNodeType(typeChecker: ts.TypeChecker, node: ts.Node) {
const nodeType = typeChecker.getTypeAtLocation(node)
if(isValidType(nodeType)) return nodeType

const symbolType = wrapSafe((symbol: ts.Symbol) => getSymbolType(typeChecker, symbol, node))(getNodeSymbol(typeChecker, node))
if(symbolType && isValidType(symbolType)) return symbolType

if(ts.isTypeNode(node)) {
const typeNodeType = typeChecker.getTypeFromTypeNode(node)
if(ts.isTypeNode(node) || ts.isTypeNode(node.parent)) {
const typeNode = ts.isTypeNode(node) ? node : ts.isTypeNode(node.parent) ? node.parent : undefined as never

const typeNodeType = getTypeFromTypeNode(typeChecker, typeNode)
if(isValidType(typeNodeType)) return typeNodeType
}

if(ts.isExpressionStatement(node)) {
if((ts as (typeof ts & { isExpression(node: ts.Node): node is ts.Expression })).isExpression(node)) {
const expressionType = checkExpression(typeChecker, node)
if(expressionType && isValidType(expressionType)) return expressionType
}
Expand Down Expand Up @@ -442,6 +448,62 @@ export function getSignatureTypeArguments(typeChecker: ts.TypeChecker, signature
?.typeArguments?.map(t => getTypeFromTypeNode(typeChecker, t))
}

export function getDescendantAtPosition(sourceFile: ts.SourceFile, position: number) {
return getDescendantAtRange(sourceFile, [position, position])
}

export function getDescendantAtRange(sourceFile: ts.SourceFile, range: [number, number]) {
let bestMatch: { node: ts.Node; start: number } = { node: sourceFile, start: sourceFile.getStart(sourceFile) };
searchDescendants(sourceFile);
return bestMatch.node;

function searchDescendants(node: ts.Node) {
const children: ts.Node[] = []
node.forEachChild(child => { children.push(child); return undefined })
// const children = node.getChildren(sourcefile)

for (const child of children) {
if (child.kind !== ts.SyntaxKind.SyntaxList) {
if (isBeforeRange(child.end)) {
continue;
}

const childStart = getStartSafe(child, sourceFile);

if (isAfterRange(childStart)) {
return;
}

const isEndOfFileToken = child.kind === ts.SyntaxKind.EndOfFileToken;
const hasSameStart = bestMatch.start === childStart && range[0] === childStart;
if (!isEndOfFileToken && !hasSameStart) {
bestMatch = { node: child, start: childStart };
}
}

searchDescendants(child);
}
}

function isBeforeRange(pos: number) {
return pos < range[0];
}

function isAfterRange(nodeEnd: number) {
return nodeEnd >= range[0] && nodeEnd > range[1];
}
}

function getStartSafe(node: ts.Node, sourceFile: ts.SourceFile) {
// workaround for compiler api bug with getStart(sourceFile, true) (see PR #35029 in typescript repo)
const jsDocs = ((node as any).jsDoc) as ts.Node[] | undefined;
if (jsDocs && jsDocs.length > 0) {
return jsDocs[0].getStart(sourceFile);
}
return node.getStart(sourceFile);
}


export const enum CheckFlags {
Instantiated = 1 << 0, // Instantiated symbol
SyntheticProperty = 1 << 1, // Property in union or intersection type
Expand Down
19 changes: 14 additions & 5 deletions packages/typescript-explorer-tsserver/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { multilineTypeToString, getSymbolType, recursivelyExpandType, generateTypeTree, getNodeType, getNodeSymbol } from "@ts-expand-type/api";
import { multilineTypeToString, getSymbolType, recursivelyExpandType, generateTypeTree, getNodeType, getNodeSymbol, getDescendantAtPosition } from "@ts-expand-type/api";
import type { ExpandedQuickInfo } from "./types";
import * as ts_orig from "typescript"
import { TypeChecker, Node } from "typescript/lib/tsserverlibrary";
import { isValidType } from "@ts-expand-type/api/dist/util";

// TODO: add config for e.g. max depth

Expand All @@ -18,7 +19,7 @@ function init(modules: { typescript: typeof import("typescript/lib/tsserverlibra
}

proxy.getQuickInfoAtPosition = function (fileName, position) {
const prior = info.languageService.getQuickInfoAtPosition(fileName, position) as ExpandedQuickInfo
let prior = info.languageService.getQuickInfoAtPosition(fileName, position) as ExpandedQuickInfo

const program = info.project['program'] as ts.Program|undefined

Expand All @@ -29,13 +30,17 @@ function init(modules: { typescript: typeof import("typescript/lib/tsserverlibra

if(!sourceFile) return prior

// @ts-expect-error
const node: ts.Node = ts.getTouchingPropertyName(sourceFile, position);
// const node: ts.Node = ts.getTouchingPropertyName(sourceFile, position);
const node = getDescendantAtPosition(sourceFile, position)
if (!node || node === sourceFile) {
// Avoid giving quickInfo for the sourceFile as a whole.
return prior
}

if(!prior) {
prior = { } as ExpandedQuickInfo
}

if(prior) {
prior.__displayString = prior.displayParts?.map(({ text }) => text).join("")
prior.__displayType = getDisplayType(typeChecker, sourceFile, node)
Expand All @@ -57,7 +62,11 @@ function getDisplayTree(typeChecker: ts.TypeChecker, node: ts.Node) {
const symbol = typeChecker.getSymbolAtLocation(node) ?? getNodeSymbol(typeChecker, node)

if(symbol) {
return generateTypeTree({ symbol, node }, typeChecker)
const symbolType = getSymbolType(typeChecker, symbol, node)

if(isValidType(symbolType)) {
return generateTypeTree({ symbol, node }, typeChecker)
}
}

const type = getNodeType(typeChecker, node)
Expand Down

0 comments on commit 770dfba

Please sign in to comment.