Skip to content

Commit

Permalink
feat: support going to type definition
Browse files Browse the repository at this point in the history
  • Loading branch information
mxsdev committed Oct 17, 2022
1 parent 770dfba commit e4b6675
Show file tree
Hide file tree
Showing 34 changed files with 303 additions and 149 deletions.
2 changes: 1 addition & 1 deletion packages/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ export { APIConfig } from './config'
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"
export { TypeInfo, SymbolInfo, SignatureInfo, TypeId, IndexInfo, TypeInfoKind, TypeParameterInfo, SourceFileLocation } from "./types"
export { localizeTypeInfo, LocalizedTypeInfo, TypeInfoMap, generateTypeInfoMap, getLocalizedTypeInfoChildren } from "./localizedTree"
32 changes: 29 additions & 3 deletions packages/api/src/localizedTree.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import assert from "assert"
import * as ts from "typescript"
import { getKindText, getPrimitiveKindText, LocalizableKind } from "./localization"
import { IndexInfo, SignatureInfo, SymbolInfo, TypeId, TypeInfo, TypeInfoKind } from "./types"
import { IndexInfo, SignatureInfo, SourceFileLocation, SymbolInfo, TypeId, TypeInfo, TypeInfoKind } from "./types"
import { getTypeInfoChildren } from "./tree"
import { getEmptyTypeId, isEmpty, isNonEmpty, pseudoBigIntToString, wrapSafe } from "./util"
import { unwatchFile } from "fs"

export function localizeTypeInfo(info: TypeInfo, typeInfoMap: TypeInfoMap): LocalizedTypeInfo {
return _localizeTypeInfo(info, { typeInfoMap })
Expand All @@ -26,7 +27,7 @@ export function getLocalizedTypeInfoChildren(info: LocalizedTypeInfo, typeInfoMa
type TypePurpose = 'return'|'index_type'|'index_value_type'|'conditional_check'|'conditional_extends'|'conditional_true'|'conditional_false'|'keyof'|'indexed_access_index'|'indexed_access_base'|'parameter_default'|'parameter_base_constraint'|'class_constructor'|'class_base_type'|'class_implementations'|'object_class'|'type_parameter_list'|'type_argument_list'|'parameter_value'

type ResolvedTypeInfo = Exclude<TypeInfo, {kind: 'reference'}>
type LocalizedSymbolInfo = { name: string, anonymous?: boolean }
type LocalizedSymbolInfo = { name: string, anonymous?: boolean, locations?: SourceFileLocation[] }

type TypeInfoChildren = ({ info?: TypeInfo, localizedInfo?: LocalizedTypeInfo, opts?: LocalizeOpts })[]

Expand All @@ -41,6 +42,7 @@ export type LocalizedTypeInfo = {
dimension?: number,
rest?: boolean,
children?: TypeInfoChildren,
locations?: SourceFileLocation[],
}

export type TypeInfoMap = Map<TypeId, ResolvedTypeInfo>
Expand All @@ -65,6 +67,8 @@ function _localizeTypeInfo(info: TypeInfo, data: LocalizeData, opts: LocalizeOpt
const isOptional = info.symbolMeta?.optional || optional || ((info.symbolMeta?.flags ?? 0) & ts.SymbolFlags.Optional)
const isRest = info.symbolMeta?.rest

const locations = getTypeLocations(info)

const res: LocalizedTypeInfo = {
kindText: getKind(info),
kind: info.kind,
Expand All @@ -74,6 +78,7 @@ function _localizeTypeInfo(info: TypeInfo, data: LocalizeData, opts: LocalizeOpt
...isRest && { rest: true },
...dimension && { dimension },
...(name !== undefined) && { name },
locations,
}

res.children = getChildren(info, opts)
Expand Down Expand Up @@ -282,9 +287,11 @@ function getChildren(info: ResolvedTypeInfo, { typeArguments: contextualTypeArgu
}

function getLocalizedSignature(signature: SignatureInfo, typeArguments?: TypeInfo[]){
const symbol = wrapSafe(localizeSymbol)(signature.symbolMeta)

return createChild({
kindText: "signature",
symbol: wrapSafe(localizeSymbol)(signature.symbolMeta),
symbol, locations: symbol?.locations,
children: getLocalizedSignatureChildren(signature, typeArguments),
})
}
Expand All @@ -299,9 +306,12 @@ function getChildren(info: ResolvedTypeInfo, { typeArguments: contextualTypeArgu
}

function localizeSymbol(symbolInfo: SymbolInfo): LocalizedSymbolInfo {
const locations = getLocations(symbolInfo)

return {
name: symbolInfo.name,
...symbolInfo.anonymous && { anonymous: symbolInfo.anonymous },
locations,
}
}

Expand Down Expand Up @@ -443,4 +453,20 @@ export function generateTypeInfoMap(tree: TypeInfo, cache?: TypeInfoMap): TypeIn
getTypeInfoChildren(tree).forEach(c => generateTypeInfoMap(c, cache))

return cache
}

function getTypeLocations(info: TypeInfo): SourceFileLocation[]|undefined {
const baseLocations = wrapSafe(getLocations)(info.typeSymbolMeta ?? info.aliasSymbolMeta ?? info.symbolMeta)

if(!baseLocations) {
if(info.kind === 'function' && info.signatures.length === 1) {
return wrapSafe(getLocations)(info.signatures[0].symbolMeta)
}
}

return baseLocations
}

function getLocations(info: SymbolInfo): SourceFileLocation[]|undefined {
return info.declarations?.map(({ location }) => location)
}
25 changes: 20 additions & 5 deletions packages/api/src/tree.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import assert from "assert";
import ts, { createProgram, TypeChecker } from "typescript";
import { APIConfig } from "./config";
import { IndexInfo, SignatureInfo, SymbolInfo, TypeId, TypeInfo, TypeInfoNoId } from "./types";
import { getIndexInfos, getIntersectionTypesFlat, getSignaturesOfType, getSymbolType, getTypeId, TSIndexInfoMerged, isPureObject, wrapSafe, isArrayType, getTypeArguments, isTupleType, SignatureInternal, getParameterInfo, IntrinsicTypeInternal, TSSymbol, isClassType, isClassOrInterfaceType, isInterfaceType, getImplementsTypes, filterUndefined, createSymbol, getConstructSignatures, getEmptyTypeId, getTypeParameters, isNonEmpty, arrayContentsEqual, getResolvedSignature, getSignatureTypeArguments, getCallLikeExpression } from "./util";
import { DeclarationInfo, IndexInfo, SignatureInfo, SymbolInfo, TypeId, TypeInfo, TypeInfoNoId } from "./types";
import { getIndexInfos, getIntersectionTypesFlat, getSignaturesOfType, getSymbolType, getTypeId, TSIndexInfoMerged, isPureObject, wrapSafe, isArrayType, getTypeArguments, isTupleType, SignatureInternal, getParameterInfo, IntrinsicTypeInternal, TSSymbol, isClassType, isClassOrInterfaceType, isInterfaceType, getImplementsTypes, filterUndefined, createSymbol, getConstructSignatures, getEmptyTypeId, getTypeParameters, isNonEmpty, arrayContentsEqual, getResolvedSignature, getSignatureTypeArguments, getCallLikeExpression, getSourceFileLocation, getNodeSymbol } from "./util";

const maxDepthExceeded: TypeInfo = {kind: 'max_depth', id: getEmptyTypeId()}

Expand Down Expand Up @@ -47,7 +47,7 @@ function _generateTypeTree({ symbol, type, node }: SymbolOrType, ctx: TypeTreeCo
if(!type) {
type = getSymbolType(typeChecker, symbol!)
}

let isAnonymousSymbol = !symbol

if(!symbol) {
Expand Down Expand Up @@ -281,7 +281,7 @@ function _generateTypeTree({ symbol, type, node }: SymbolOrType, ctx: TypeTreeCo
typeParameters = signature.typeParameters ?? typeParameters

return {
symbolMeta: wrapSafe(getSymbolInfo)(typeChecker.getSymbolAtLocation(signature.getDeclaration())),
symbolMeta: wrapSafe(getSymbolInfo)(getNodeSymbol(typeChecker, signature.getDeclaration())),
parameters: signature.getParameters().map((parameter, index) => getFunctionParameterInfo(parameter, signature, index)),
...includeReturnType && { returnType: parseType(typeChecker.getReturnTypeOfSignature(signature)) },
...typeParameters && { typeParameters: parseTypes(typeParameters) },
Expand Down Expand Up @@ -321,16 +321,31 @@ function _generateTypeTree({ symbol, type, node }: SymbolOrType, ctx: TypeTreeCo
const parent = (symbol as TSSymbol).parent
const insideClassOrInterface = options.insideClassOrInterface ?? (parent && parent.flags & (ts.SymbolFlags.Class | ts.SymbolFlags.Interface))

const declarations = wrapSafe(filterUndefined)(symbol.getDeclarations()?.map(getDeclarationInfo))

return {
name: symbol.getName(),
flags: symbol.getFlags(),
...isAnonymous && { anonymous: true },
...optional && { optional: true },
...rest && { rest: true },
...insideClassOrInterface && { insideClassOrInterface: true }
...insideClassOrInterface && { insideClassOrInterface: true },
...declarations && { declarations },
}
}

function getDeclarationInfo(declaration: ts.Declaration): DeclarationInfo|undefined {
const sourceFile = declaration.getSourceFile()
const location = getSourceFileLocation(sourceFile, declaration)

if(!location) {
return undefined
}

return {
location
}
}
}

function resolveSignature(typeChecker: ts.TypeChecker, type: ts.Type, node?: ts.Node) {
Expand Down
15 changes: 14 additions & 1 deletion packages/api/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
import ts from "typescript"

// TODO: support class instances with generics, like Map<string, number>
export type TextRange = {
start: ts.LineAndCharacter,
end: ts.LineAndCharacter,
}

export type SourceFileLocation = {
range: TextRange,
fileName: string,
}

export type DeclarationInfo = {
location: SourceFileLocation,
}

export type SymbolInfo = {
name: string,
Expand All @@ -9,6 +21,7 @@ export type SymbolInfo = {
anonymous?: boolean,
rest?: boolean,
insideClassOrInterface?: boolean,
declarations?: DeclarationInfo[],
}

export type IndexInfo = {
Expand Down
25 changes: 22 additions & 3 deletions packages/api/src/util.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { deepStrictEqual } from "assert"
import ts from "typescript"
import { TypeId } from "./types"
import { SourceFileLocation, TypeId } from "./types"

export type SymbolName = ts.__String

Expand Down Expand Up @@ -82,8 +82,8 @@ export function getSymbolType(typeChecker: ts.TypeChecker, symbol: ts.Symbol, lo
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 getNodeSymbol(typeChecker: ts.TypeChecker, node?: ts.Node): ts.Symbol|undefined {
return node ? ((node as ts.Node & { symbol?: TSSymbol }).symbol ?? typeChecker.getSymbolAtLocation(node)) : undefined
}

export function getNodeType(typeChecker: ts.TypeChecker, node: ts.Node) {
Expand Down Expand Up @@ -503,6 +503,25 @@ function getStartSafe(node: ts.Node, sourceFile: ts.SourceFile) {
return node.getStart(sourceFile);
}

export function getSourceFileLocation(sourceFile: ts.SourceFile, node: ts.Node): SourceFileLocation|undefined {
const startPos = node.getStart()
const endPos = node.getEnd()

if(startPos < 0 || endPos < 0) {
return undefined
}

const start = sourceFile.getLineAndCharacterOfPosition(startPos)
const end = sourceFile.getLineAndCharacterOfPosition(endPos)

return {
fileName: sourceFile.fileName,
range: {
start, end
}
}
}


export const enum CheckFlags {
Instantiated = 1 << 0, // Instantiated symbol
Expand Down
23 changes: 23 additions & 0 deletions packages/typescript-explorer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@
"title": "Toggle Expanded Type on Hover",
"command": "typescript-explorer.toggleExpandedHover",
"category": "TypeScript Explorer"
},
{
"title": "Refresh Type Tree",
"command": "typescript-explorer.refreshTypeTreeView",
"category": "TypeScript Explorer",
"icon": "$(extensions-refresh)"
},
{
"title": "Go To Type",
"command": "typescript-explorer.goToTypeInTypeTreeView",
"category": "Typescript Explorer"
}
],
"viewsContainers": {
Expand All @@ -51,6 +62,18 @@
}
]
},
"menus": {
"view/title": [
{
"command": "typescript-explorer.refreshTypeTreeView",
"when": "view == type-tree",
"group": "navigation"
}
],
"view/item/context": [

]
},
"typescriptServerPlugins": [
{
"name": "@ts-expand-type/typescript-explorer-tsserver",
Expand Down
3 changes: 2 additions & 1 deletion packages/typescript-explorer/src/commands.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { ViewProviders } from './view/views';
import { TSExplorer } from "./config";
import * as vscode from 'vscode'

export function registerCommands(context: vscode.ExtensionContext) {
export function registerCommands(context: vscode.ExtensionContext, ViewProviders: ViewProviders) {
context.subscriptions.push(
vscode.commands.registerCommand('typescript-explorer.toggleExpandedHover', TSExplorer.Config.toggleExpandedHover)
)
Expand Down
15 changes: 8 additions & 7 deletions packages/typescript-explorer/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@ import { StateManager } from './state/stateManager';
// TODO: add config for e.g. max depth

export function activate(context: vscode.ExtensionContext) {
registerCommands(context)
registerTypeInfoHoverProvider(context)

const stateManager = new StateManager()

const { typeTreeProvider } = createAndRegisterViews(context, stateManager)


const viewProviders = createAndRegisterViews(context, stateManager)

registerCommands(context, viewProviders)
registerTypeInfoHoverProvider(context)

stateManager.init(
typeTreeProvider
context,
viewProviders
)
}

Expand Down
14 changes: 12 additions & 2 deletions packages/typescript-explorer/src/state/stateManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,37 @@ import { TypeInfo } from '@ts-expand-type/api'
import * as vscode from 'vscode'
import { getQuickInfoAtPosition } from '../util'
import { TypeTreeProvider } from '../view/typeTreeView'
import { ViewProviders } from '../view/views'

export class StateManager {
constructor() { }

private typeTree: TypeInfo|undefined
private typeTreeProvider?: TypeTreeProvider

init(typeTreeProvider: TypeTreeProvider) {
init(context: vscode.ExtensionContext, { typeTreeProvider }: ViewProviders) {
this.typeTreeProvider = typeTreeProvider

vscode.window.onDidChangeTextEditorSelection((e) => {
if(e.selections.length === 0) {
if(e.selections.length === 0 || e.kind === vscode.TextEditorSelectionChangeKind.Command || !e.kind) {
return
}

console.log("change selection", e)

getQuickInfoAtPosition(e.textEditor.document.fileName, e.selections[0].start)
.then((body) => {
const { __displayTree } = body ?? {}
this.setTypeTree(__displayTree)
})
})

context.subscriptions.push(
vscode.commands.registerCommand(
"typescript-explorer.refreshTypeTreeView",
() => typeTreeProvider.refresh()
)
)
}

setTypeTree(typeTree: TypeInfo|undefined) {
Expand Down
14 changes: 14 additions & 0 deletions packages/typescript-explorer/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ export const toFileLocationRequestArgs = (file: string, position: vscode.Positio
offset: position.character + 1,
})

type LineAndCharacter = { line: number, character: number }

export const fromFileLocationRequestArgs = (position: { line: number, character: number }) => ({
line: position.line - 1,
character: position.character - 1,
})

export const positionFromLineAndCharacter = ({ line, character }: LineAndCharacter) => new vscode.Position(line, character)

export const rangeFromLineAndCharacters = (start: LineAndCharacter, end: LineAndCharacter) => new vscode.Range(
positionFromLineAndCharacter(start),
positionFromLineAndCharacter(end),
)

export function getTypescriptMd(code: string) {
const mds = new vscode.MarkdownString()
mds.appendCodeblock(code, 'typescript')
Expand Down
Loading

0 comments on commit e4b6675

Please sign in to comment.