Skip to content

Commit

Permalink
feat(vscode): add type tree view
Browse files Browse the repository at this point in the history
  • Loading branch information
mxsdev committed Oct 8, 2022
1 parent c307328 commit 9d18f22
Show file tree
Hide file tree
Showing 16 changed files with 401 additions and 27 deletions.
4 changes: 2 additions & 2 deletions packages/api/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { getSymbolType, multilineTypeToString } from "./util"
export { recursivelyExpandType } from "./merge"
export { generateTypeTree } from "./tree"
export { TypeInfo, SymbolInfo, TypeInfoNode, SignatureInfo, TypeId, IndexInfo, TypeInfoKind, TypeParameterInfo } from "./types"
export { generateTypeTree, getTypeInfoChildren } from "./tree"
export { TypeInfo, SymbolInfo, SignatureInfo, TypeId, IndexInfo, TypeInfoKind, TypeParameterInfo } from "./types"
2 changes: 2 additions & 0 deletions packages/api/src/merge.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import ts from "typescript"
import { createUnionType, createIntersectionType, createObjectType, TSSymbol, createSymbol, getSymbolType, SymbolName, ObjectType, getSignaturesOfType, getIndexInfos, getIntersectionTypesFlat } from "./util"

// TODO: need to add max depth

export function recursivelyExpandType(typeChecker: ts.TypeChecker, type: ts.Type) {
return _recursivelyExpandType(typeChecker, [type], new WeakMap())
}
Expand Down
57 changes: 53 additions & 4 deletions packages/api/src/tree.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import assert from "assert";
import ts from "typescript";
import { IndexInfo, SignatureInfo, SymbolInfo, TypeId, TypeInfo, TypeInfoNode, TypeParameterInfo } from "./types";
import { IndexInfo, SignatureInfo, SymbolInfo, TypeId, TypeInfo, TypeInfoNoId, TypeParameterInfo } from "./types";
import { getIntersectionTypesFlat, getSignaturesOfType, getSymbolType, getTypeId, isPureObject, wrapSafe } from "./util";

// TODO: need to add max depth

type TypeTreeContext = {
typeChecker: ts.TypeChecker,
seen?: Set<TypeId>
Expand All @@ -22,23 +24,23 @@ export function generateTypeTree({ symbol, type }: {symbol: ts.Symbol, type?: un
symbol = type.getSymbol()
}

let typeInfo: TypeInfoNode
let typeInfo: TypeInfoNoId
if(!ctx.seen?.has(getTypeId(type))) {
ctx.seen?.add(getTypeId(type))
typeInfo = _generateTypeTree(typeChecker, type, ctx)
} else {
typeInfo = { kind: 'reference' }
}

const typeInfoId = typeInfo as TypeInfoNode & { id: number }
const typeInfoId = typeInfo as TypeInfo

typeInfoId.symbolMeta = wrapSafe(getSymbolInfo)(symbol)
typeInfoId.id = getTypeId(type)

return typeInfoId
}

function _generateTypeTree(typeChecker: ts.TypeChecker, type: ts.Type, ctx: TypeTreeContext): TypeInfoNode {
function _generateTypeTree(typeChecker: ts.TypeChecker, type: ts.Type, ctx: TypeTreeContext): TypeInfoNoId {
const flags = type.getFlags()

if(flags & ts.TypeFlags.Any) { return { kind: 'primitive', primitive: 'any' }}
Expand Down Expand Up @@ -157,4 +159,51 @@ function getSymbolInfo(symbol: ts.Symbol): SymbolInfo {
return {
name: symbol.getName(),
}
}

export function getTypeInfoChildren(info: TypeInfo): TypeInfo[] {
switch(info.kind) {
case 'object': {
return [
...info.properties,
...info.signatures?.flatMap(s => [...s.parameters, s.returnType]) ?? [],
...info.indexInfos?.map(x => x.type) ?? [],
// TODO: array
]
}

case "intersection": {
return [...info.types, ...info.properties]
}

case "union": {
return info.types
}

case "index": {
return [info.indexOf]
}

case "indexed_access": {
return [info.indexType, info.objectType]
}

case "conditional": {
return [
info.checkType, info.extendsType,
...info.falseType ? [info.falseType] : [],
...info.trueType ? [info.trueType] : [],
]
}

case "substitution": {
return [info.baseType, info.substitute]
}

case "template_literal": {
return info.types
}
}

return []
}
7 changes: 4 additions & 3 deletions packages/api/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ export type TypeParameterInfo = {
name: string,
}

export type TypeInfoNode =
export type TypeInfo = TypeInfoNoId & { id: TypeId }

export type TypeInfoNoId =
({
symbolMeta?: SymbolInfo,
} & (
Expand Down Expand Up @@ -89,7 +91,6 @@ export type TypeInfoNode =
}
))

export type TypeInfo = TypeInfoNode & {id: number} | TypeId
export type TypeId = number

export type TypeInfoKind<K extends Exclude<TypeInfo, TypeId>['kind']> = Extract<TypeInfo, { kind: K }>
export type TypeInfoKind<K extends TypeInfo['kind']> = Extract<TypeInfo, { kind: K }>
13 changes: 12 additions & 1 deletion packages/typescript-explorer-tsserver/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { multilineTypeToString, getSymbolType, recursivelyExpandType } from "@ts-expand-type/api";
import { multilineTypeToString, getSymbolType, recursivelyExpandType, generateTypeTree } from "@ts-expand-type/api";
import type { ExpandedQuickInfo } from "./types";
import * as ts_orig from "typescript"

Expand Down Expand Up @@ -36,6 +36,7 @@ function init(modules: { typescript: typeof import("typescript/lib/tsserverlibra
if(prior) {
prior.__displayString = prior.displayParts?.map(({ text }) => text).join("")
prior.__displayType = getDisplayType(typeChecker, sourceFile, node)
prior.__displayTree = getDisplayTree(typeChecker, node)

prior.displayParts = undefined
}
Expand All @@ -49,6 +50,16 @@ function init(modules: { typescript: typeof import("typescript/lib/tsserverlibra
return { create }
}

function getDisplayTree(typeChecker: ts.TypeChecker, node: ts.Node) {
const symbol = typeChecker.getSymbolAtLocation(node)

if(symbol) {
return generateTypeTree({ symbol }, { typeChecker })
}

return undefined
}

function getDisplayType(typeChecker: ts.TypeChecker, sourceFile: ts.SourceFile, node: ts.Node): string|undefined {
const symbol = typeChecker.getSymbolAtLocation(node)
if(!symbol) return undefined
Expand Down
2 changes: 2 additions & 0 deletions packages/typescript-explorer-tsserver/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { TypeInfo } from "@ts-expand-type/api"
import type * as ts from "typescript"

export type ExpandedQuickInfo = ts.QuickInfo & {
__displayString?: string,
__displayType?: string,
__displayTree?: TypeInfo,
}
17 changes: 17 additions & 0 deletions packages/typescript-explorer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,23 @@
"category": "TypeScript Explorer"
}
],
"viewsContainers": {
"activitybar": [
{
"id": "typescript-explorer",
"title": "TypeScript Explorer",
"icon": "resources/typescript.svg"
}
]
},
"views": {
"typescript-explorer": [
{
"id": "type-tree",
"name": "Type Tree"
}
]
},
"typescriptServerPlugins": [
{
"name": "@ts-expand-type/typescript-explorer-tsserver",
Expand Down
19 changes: 19 additions & 0 deletions packages/typescript-explorer/resources/typescript.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions packages/typescript-explorer/src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
import * as vscode from 'vscode';
import { registerTypeInfoHoverProvider } from "./hover";
import { registerCommands } from "./commands";
import { createAndRegisterViews } from './view/views';
import { StateManager } from './state/stateManager';

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

const stateManager = new StateManager()

const { typeTreeProvider } = createAndRegisterViews(context, stateManager)

stateManager.init(
typeTreeProvider
)
}

export function deactivate() {}
29 changes: 13 additions & 16 deletions packages/typescript-explorer/src/hover.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ExpandedQuickInfo } from '@ts-expand-type/typescript-explorer-tsserver/dist/types';
import { TSExplorer } from './config';
import * as vscode from 'vscode';
import { getTypescriptMd, toFileLocationRequestArgs } from './util';
import { getQuickInfoAtPosition, getTypescriptMd, toFileLocationRequestArgs } from './util';

export function registerTypeInfoHoverProvider(context: vscode.ExtensionContext) {
context.subscriptions.push(
Expand All @@ -12,24 +12,21 @@ export function registerTypeInfoHoverProvider(context: vscode.ExtensionContext)
function getTypeInfoHoverProvider(): vscode.HoverProvider {
return {
async provideHover(document, position, token) {
return await vscode.commands.executeCommand("typescript.tsserverRequest", "quickinfo-full", toFileLocationRequestArgs(document.fileName, position))
.then((r) => {
const { body } = (r as { body: ExpandedQuickInfo })
const body = await getQuickInfoAtPosition(document.fileName, position)

const {__displayType, __displayString} = body ?? {}
const {__displayType, __displayString} = body ?? {}

if(!TSExplorer.Config.isExpandedHover()) {
if(__displayString) {
return new vscode.Hover(getTypescriptMd(__displayString))
}
} else {
if(__displayType) {
return new vscode.Hover(getTypescriptMd(__displayType))
}
}
if(!TSExplorer.Config.isExpandedHover()) {
if(__displayString) {
return new vscode.Hover(getTypescriptMd(__displayString))
}
} else {
if(__displayType) {
return new vscode.Hover(getTypescriptMd(__displayType))
}
}

return null
})
return null
}
}
}
50 changes: 50 additions & 0 deletions packages/typescript-explorer/src/localization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { TypeInfoKind, TypeInfo } from "@ts-expand-type/api"

type PrimitiveKind = TypeInfoKind<'primitive'>['primitive']

export const PrimitiveKindText: Record<PrimitiveKind, string> = {
"any": "any",
"bigint": "BigInt",
"boolean": "boolean",
"enum": "enum",
"essymbol": "ESSymbol",
"unique_symbol": "ESSymbol",
"never": "never",
"null": "null",
"number": "number",
"undefined": "undefined",
"void": "void",
"string": "string",
"unknown": "unknown"
}

type Kind = Exclude<TypeInfo['kind'], 'reference'>
export const KindText: Record<Kind, string> = {
"bigint_literal": "$1L",
"boolean_literal": "$1",
"enum_literal": "$1",
"number_literal": "$1",
"conditional": "conditional",
"index": "Index",
"indexed_access": "Indexed Access",
"intersection": "intersection",
"union": "union",
"non_primitive": "Non-Primitive",
"object": "object",
"string_literal": "\"$1\"",
"string_mapping": "String Mapping",
"type_parameter": "Type Parameter",
"primitive": "Primitive",
"substitution": "Substitution",
"template_literal": "Template Literal"
}

export function getKindText(kind: Kind, ...args: {toString(): string}[]) {
return args.reduce<string>((prev, curr, i) => {
return prev.replace(new RegExp(`\\\$${i+1}`, "g"), curr.toString())
}, KindText[kind])
}

export function getPrimitiveKindText(kind: PrimitiveKind) {
return PrimitiveKindText[kind]
}
36 changes: 36 additions & 0 deletions packages/typescript-explorer/src/state/stateManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { TypeInfo } from '@ts-expand-type/api'
import * as vscode from 'vscode'
import { getQuickInfoAtPosition } from '../util'
import { TypeTreeProvider } from '../view/typeTreeView'

export class StateManager {
constructor() { }

private typeTree: TypeInfo|undefined
private typeTreeProvider?: TypeTreeProvider

init(typeTreeProvider: TypeTreeProvider) {
this.typeTreeProvider = typeTreeProvider

vscode.window.onDidChangeTextEditorSelection((e) => {
if(e.selections.length === 0) {
return
}

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

setTypeTree(typeTree: TypeInfo|undefined) {
this.typeTree = typeTree
this.typeTreeProvider?.refresh()
}

getTypeTree() {
return this.typeTree
}
}
6 changes: 6 additions & 0 deletions packages/typescript-explorer/src/util.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ExpandedQuickInfo } from '@ts-expand-type/typescript-explorer-tsserver/dist/types'
import * as vscode from 'vscode'

export const toFileLocationRequestArgs = (file: string, position: vscode.Position) => ({
Expand All @@ -10,4 +11,9 @@ export function getTypescriptMd(code: string) {
const mds = new vscode.MarkdownString()
mds.appendCodeblock(code, 'typescript')
return mds
}

export async function getQuickInfoAtPosition(fileName: string, position: vscode.Position) {
return await vscode.commands.executeCommand("typescript.tsserverRequest", "quickinfo-full", toFileLocationRequestArgs(fileName, position))
.then((r) => (r as { body: ExpandedQuickInfo|undefined }).body)
}
Loading

0 comments on commit 9d18f22

Please sign in to comment.