From 0d18d5cd4538d04c94a94da7452754f695cfacf9 Mon Sep 17 00:00:00 2001 From: charburgx Date: Sat, 29 Oct 2022 00:57:25 -0500 Subject: [PATCH] perf: switch from quickinfo to completions --- packages/api/src/index.ts | 9 +- packages/api/src/tree.ts | 34 +++++++ packages/api/src/types.ts | 28 +++++- packages/api/src/util.ts | 29 +++++- .../src/state/stateManager.ts | 13 ++- .../typescript-explorer-vscode/src/util.ts | 76 ++++++++++++--- packages/typescript-plugin/src/index.ts | 94 ++++++------------- tests/lib/baselineGenerators.ts | 35 ++----- 8 files changed, 199 insertions(+), 119 deletions(-) diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index f670aae..d14fce0 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -8,12 +8,14 @@ export { getDescendantAtPosition, getDescendantAtRange, isValidType, + getSymbolOrTypeOfNode, } from "./util" export { recursivelyExpandType } from "./merge" export { generateTypeTree, getTypeInfoChildren, getTypeInfoSymbols, + getTypeInfoAtRange, } from "./tree" export { TypeInfo, @@ -25,7 +27,12 @@ export { TypeParameterInfo, SourceFileLocation, SymbolOrType, - ExpandedQuickInfo, + CustomTypeScriptRequest, + CustomTypeScriptRequestId, + CustomTypeScriptResponse, + CustomTypeScriptResponseBody, + CustomTypeScriptRequestOfId, + TextRange, } from "./types" export { LocalizedTypeInfo, diff --git a/packages/api/src/tree.ts b/packages/api/src/tree.ts index 526dbcc..3f3c895 100644 --- a/packages/api/src/tree.ts +++ b/packages/api/src/tree.ts @@ -49,6 +49,8 @@ import { getNodeSymbol, narrowDeclarationForLocation, isPureObjectOrMappedTypeShallow, + getDescendantAtRange, + getSymbolOrTypeOfNode, } from "./util" const maxDepthExceeded: TypeInfo = { kind: "max_depth", id: getEmptyTypeId() } @@ -877,3 +879,35 @@ export function getTypeInfoSymbols(info: TypeInfo): SymbolInfo[] { } } } + +export function getTypeInfoAtRange( + ctx: TypescriptContext, + location: SourceFileLocation, + apiConfig?: APIConfig +) { + const sourceFile = ctx.program.getSourceFile(location.fileName) + if (!sourceFile) return undefined + + const startPos = sourceFile.getPositionOfLineAndCharacter( + location.range.start.line, + location.range.start.character + ) + + // TODO: integrate this + // getDescendantAtRange will probably need to be improved... + // const endPos = sourceFile.getPositionOfLineAndCharacter( + // location.range.end.line, + // location.range.end.character + // ) + + const node = getDescendantAtRange(ctx, sourceFile, [startPos, startPos]) + + if (node === sourceFile || !node.parent) { + return undefined + } + + const symbolOrType = getSymbolOrTypeOfNode(ctx, node) + if (!symbolOrType) return undefined + + return generateTypeTree(symbolOrType, ctx, apiConfig) +} diff --git a/packages/api/src/types.ts b/packages/api/src/types.ts index 96636d4..3ce89a7 100644 --- a/packages/api/src/types.ts +++ b/packages/api/src/types.ts @@ -1,10 +1,34 @@ import type * as ts from "typescript" import { APIConfig } from "./config" -export type ExpandedQuickInfo = ts.QuickInfo & { - __displayTree?: TypeInfo +type FileLocationRequest = { + range: TextRange +} + +export type CustomTypeScriptRequest = { + id: "type-tree" +} & FileLocationRequest + +export type CustomTypeScriptRequestId = CustomTypeScriptRequest["id"] + +export type CustomTypeScriptRequestOfId = + Extract + +type TypeTreeResponseBody = { + typeInfo: TypeInfo | undefined } +type CustomTypeScriptResponseById = { + "type-tree": TypeTreeResponseBody +} + +export type CustomTypeScriptResponse = { + body: CustomTypeScriptResponseById[Id] +} + +export type CustomTypeScriptResponseBody = + CustomTypeScriptResponse["body"] + export type TypescriptContext = { program: ts.Program typeChecker: ts.TypeChecker diff --git a/packages/api/src/util.ts b/packages/api/src/util.ts index 25229af..93b66d5 100644 --- a/packages/api/src/util.ts +++ b/packages/api/src/util.ts @@ -7,6 +7,7 @@ import { SourceFileTypescriptContext, DiscriminatedIndexInfo, ParameterInfo, + SymbolOrType, } from "./types" import { CheckFlags, @@ -700,7 +701,7 @@ export function getSignatureTypeArguments( } export function getDescendantAtPosition( - ctx: SourceFileTypescriptContext, + ctx: TypescriptContext, sourceFile: ts.SourceFile, position: number ) { @@ -795,6 +796,32 @@ export function getSourceFileLocation( } } +export function getSymbolOrTypeOfNode( + ctx: TypescriptContext, + node: ts.Node +): SymbolOrType | undefined { + const { typeChecker } = ctx + + const symbol = + typeChecker.getSymbolAtLocation(node) ?? getNodeSymbol(ctx, node) + + if (symbol) { + const symbolType = getSymbolType(ctx, symbol, node) + + if (isValidType(symbolType)) { + return { symbol, node } + } + } + + const type = getNodeType(ctx, node) + + if (type) { + return { type, node } + } + + return undefined +} + /** * Tries to find subnode that can be retrieved later * by the client diff --git a/packages/typescript-explorer-vscode/src/state/stateManager.ts b/packages/typescript-explorer-vscode/src/state/stateManager.ts index e84afce..cd13420 100644 --- a/packages/typescript-explorer-vscode/src/state/stateManager.ts +++ b/packages/typescript-explorer-vscode/src/state/stateManager.ts @@ -1,7 +1,7 @@ import { TypeInfo } from "@ts-type-explorer/api" import * as vscode from "vscode" import { selectionEnabled } from "../config" -import { getQuickInfoAtPosition, isDocumentSupported, showError } from "../util" +import { getTypeTreeAtRange, isDocumentSupported, showError } from "../util" import { TypeTreeItem, TypeTreeProvider } from "../view/typeTreeView" import { ViewProviders } from "../view/views" @@ -165,14 +165,13 @@ export class StateManager { return } - return getQuickInfoAtPosition(fileName, selections[0].start) - .then((body) => { - const { __displayTree } = body ?? {} - this.setTypeTree(__displayTree) + return getTypeTreeAtRange(fileName, selections[0]) + .then((tree) => { + this.setTypeTree(tree) }) .catch((e) => { - showError("Error getting quick info") - console.error("Quick info error", e) + showError("Error getting type information!") + console.error("TypeTreeRequest error", e) }) } } diff --git a/packages/typescript-explorer-vscode/src/util.ts b/packages/typescript-explorer-vscode/src/util.ts index de7205b..1554c3b 100644 --- a/packages/typescript-explorer-vscode/src/util.ts +++ b/packages/typescript-explorer-vscode/src/util.ts @@ -1,29 +1,37 @@ import * as vscode from "vscode" import type * as ts from "typescript" import { + CustomTypeScriptRequestId, + CustomTypeScriptRequestOfId, + CustomTypeScriptResponse, SourceFileLocation, + TextRange, TypeInfo, - ExpandedQuickInfo, } from "@ts-type-explorer/api" +import type * as Proto from "typescript/lib/protocol" -export const toFileLocationRequestArgs = ( +export const positionToLineAndCharacter = ( + position: vscode.Position +): ts.LineAndCharacter => ({ + line: position.line, + character: position.character, +}) + +export const rangeToTextRange = (range: vscode.Range): TextRange => ({ + start: positionFromLineAndCharacter(range.start), + end: positionFromLineAndCharacter(range.end), +}) + +const toFileLocationRequestArgs = ( file: string, position: vscode.Position -) => ({ +): Proto.FileLocationRequestArgs => ({ file, line: position.line + 1, offset: position.character + 1, }) -export const fromFileLocationRequestArgs = (position: { - line: number - character: number -}) => ({ - line: position.line - 1, - character: position.character - 1, -}) - -export const positionFromLineAndCharacter = ({ +const positionFromLineAndCharacter = ({ line, character, }: ts.LineAndCharacter) => new vscode.Position(line, character) @@ -43,7 +51,7 @@ export function getTypescriptMd(code: string) { return mds } -export async function getQuickInfoAtPosition( +async function getQuickInfoAtPosition( fileName: string, position: vscode.Position ) { @@ -53,7 +61,26 @@ export async function getQuickInfoAtPosition( "quickinfo-full", toFileLocationRequestArgs(fileName, position) ) - .then((r) => (r as { body: ExpandedQuickInfo | undefined }).body) + .then((r) => (r as Proto.QuickInfoResponse).body) +} + +async function customTypescriptRequest( + fileName: string, + position: vscode.Position, + request: CustomTypeScriptRequestOfId +): Promise | undefined> { + return await vscode.commands.executeCommand( + "typescript.tsserverRequest", + "completionInfo", + { + ...toFileLocationRequestArgs(fileName, position), + /** + * We override the "triggerCharacter" property here as a hack so + * that we can send custom commands to TSServer + */ + triggerCharacter: request, + } + ) } export function getQuickInfoAtLocation(location: SourceFileLocation) { @@ -66,7 +93,26 @@ export function getQuickInfoAtLocation(location: SourceFileLocation) { export function getTypeTreeAtLocation( location: SourceFileLocation ): Promise { - return getQuickInfoAtLocation(location).then((data) => data?.__displayTree) + return getTypeTreeAtRange( + location.fileName, + rangeFromLineAndCharacters(location.range.start, location.range.end) + ) +} + +export function getTypeTreeAtRange( + fileName: string, + range: vscode.Range +): Promise { + return customTypescriptRequest( + fileName, + positionFromLineAndCharacter(range.start), + { + id: "type-tree", + range: rangeToTextRange(range), + } + ).then((res) => { + return res?.body.typeInfo + }) } /** diff --git a/packages/typescript-plugin/src/index.ts b/packages/typescript-plugin/src/index.ts index e9c33ab..cd3b953 100644 --- a/packages/typescript-plugin/src/index.ts +++ b/packages/typescript-plugin/src/index.ts @@ -1,15 +1,9 @@ import { - getSymbolType, - generateTypeTree, - getNodeType, - getNodeSymbol, - getDescendantAtPosition, - TypescriptContext, - APIConfig, - SymbolOrType, - ExpandedQuickInfo, + CustomTypeScriptRequest, + CustomTypeScriptResponseBody, + getTypeInfoAtRange, } from "@ts-type-explorer/api" -import { isValidType, SourceFileTypescriptContext } from "@ts-type-explorer/api" +import { SourceFileTypescriptContext } from "@ts-type-explorer/api" // TODO: add config for e.g. max depth @@ -31,57 +25,55 @@ function init(modules: { x.apply(info.languageService, args) } - proxy.getQuickInfoAtPosition = function (fileName, position) { - let prior = info.languageService.getQuickInfoAtPosition( - fileName, - position - ) as ExpandedQuickInfo - + function getContext( + fileName: string + ): SourceFileTypescriptContext | undefined { const program = info.project["program"] as ts.Program | undefined - if (!program) return prior + if (!program) return undefined const typeChecker = program.getTypeChecker() const sourceFile = program.getSourceFile(fileName) - if (!sourceFile) return prior + if (!sourceFile) return undefined - const ctx: SourceFileTypescriptContext = { + return { program, typeChecker, sourceFile, ts: modules.typescript, } + } - const node = getDescendantAtPosition(ctx, ctx.sourceFile, position) + // @ts-expect-error - returning custom response types + proxy.getCompletionsAtPosition = function (...args) { + // triggerCharacter is "hijacked" with custom request information + const { triggerCharacter: possiblePayload } = args[2] ?? {} - if (!node || node === sourceFile) { - // Avoid giving quickInfo for the sourceFile as a whole. - return prior + if (!possiblePayload || typeof possiblePayload === "string") { + return info.languageService.getCompletionsAtPosition(...args) } - const symbolOrType = getSymbolOrType(ctx, node) + const payload = + possiblePayload as unknown as CustomTypeScriptRequest - if (!symbolOrType) { - return prior - } + const [fileName] = args + const ctx = getContext(fileName) - if (!prior) { - prior = {} as ExpandedQuickInfo + if (!ctx) { + return undefined } - const apiConfig = new APIConfig() - apiConfig.referenceDefinedTypes = true + if (payload.id === "type-tree") { + const typeInfo = getTypeInfoAtRange(ctx, { + fileName, + range: payload.range, + }) - if (prior) { - prior.__displayTree = generateTypeTree( - symbolOrType, - ctx, - apiConfig - ) + return { typeInfo } as CustomTypeScriptResponseBody<"type-tree"> } - return prior + return undefined } return proxy @@ -90,30 +82,4 @@ function init(modules: { return { create } } -function getSymbolOrType( - ctx: TypescriptContext, - node: ts.Node -): SymbolOrType | undefined { - const { typeChecker } = ctx - - const symbol = - typeChecker.getSymbolAtLocation(node) ?? getNodeSymbol(ctx, node) - - if (symbol) { - const symbolType = getSymbolType(ctx, symbol, node) - - if (isValidType(symbolType)) { - return { symbol, node } - } - } - - const type = getNodeType(ctx, node) - - if (type) { - return { type, node } - } - - return undefined -} - export = init diff --git a/tests/lib/baselineGenerators.ts b/tests/lib/baselineGenerators.ts index f6ea223..809a10e 100644 --- a/tests/lib/baselineGenerators.ts +++ b/tests/lib/baselineGenerators.ts @@ -2,12 +2,10 @@ import { APIConfig, generateTypeTree, - getDescendantAtPosition, - getNodeSymbol, - getNodeType, SourceFileLocation, - SourceFileTypescriptContext, TypeInfoResolver, + TypescriptContext, + getTypeInfoAtRange, } from "@ts-type-explorer/api" import assert from "assert" import { @@ -49,34 +47,13 @@ const localizedTreeBaselineGenerator = symbolBaselineGenerator( } ) -function getTypeInfoRetriever(ctx: SourceFileTypescriptContext) { +function getTypeInfoRetriever(ctx: TypescriptContext) { return async (location: SourceFileLocation) => { - const sourceFile = ctx.program.getSourceFile(location.fileName) - assert(sourceFile) + const typeTree = getTypeInfoAtRange(ctx, location, apiConfig) - const { line, character } = location.range.start + assert(typeTree, "Symbol/type not found!") - const node = getDescendantAtPosition( - ctx, - sourceFile, - sourceFile.getPositionOfLineAndCharacter(line, character) - ) - - const symbol = getNodeSymbol(ctx, node) - - if (symbol) { - return normalizeTypeTree( - generateTypeTree({ symbol, node }, ctx, apiConfig), - false - ) - } else { - const type = getNodeType(ctx, node) - assert(type, "Symbol/type not found") - return normalizeTypeTree( - generateTypeTree({ type, node }, ctx, apiConfig), - false - ) - } + return normalizeTypeTree(typeTree, false) } }