03.24.2022 - TypeScript/Source Code Walkthrough: InlayHints

All posts

Posted On 03.24.2022

So, I want to look into the TypeScript source code to understand how things work and maybe contribute to the project as well.

This is a quite big project but I think the code organization is pretty good. And they have loads of documents as well.

The How the TypeScript Compiler Compiles video is a good place to start when you want to understand the compilation process of TypeScript.

Also, there are a lot of notes in TypeScript Compiler Notes repository, and the TypeScript Deep Dive book.

After reading them, what next is to pick up a feature that I want to look at. Since I saw a recently closed ticket about inlay hints, maybe I can just go with it.

Before we start: LSP and InlayHints

Language Server Protocol (LSP) is the communication protocol between the code editors (Neovim, VSCode, IntelliJ IDEA,…) and the language servers (tsserver, gopls, rust-analyzer…) to provide rich editing and code analysis experience.

Inlay Hints is one of the features provided by the language servers, to help the editor render the type annotation next to some tokens of code. For example, the little :Point tag in this screenshot:

The communication between the editor and the language server via language server protocol can be described as the following diagram:

Explore the code

Let’s start looking at TypeScript’s source code to see how the inlay hints feature is implemented. Since it’s a language server’s feature, this has to be something related to tsserver. The best way to start is to look at the test files.

The unit tests for every features in tsserver is defined in the testRunner/unittests/tsserver folder. Look into it, we will see there is an inlayHints.ts file.

Also, there are other tests cases for inlay hints defined in the tests/cases/fourslash/inlayHints(…).ts files, you can look at these files too.

The Execution Path

In the test runner, the inlay hints feature are invoked with the session.executeCommandSeq<protocol.InlayHintsRequest> call:

testRunner/unittests/tsserver/inlayHints.ts

session.executeCommandSeq<protocol.InlayHintsRequest>({  
    command: protocol.CommandTypes.ProvideInlayHints,  
    arguments: {  
        file: app.path,  
        start: 0,  
        length: app.content.length,  
    }  
})

Follow along and look inside this method, which is defined in the src/server/sessions.ts file. What it does is to call the correct handler from the request command (CommandTypes.ProvideInlayHints). All the handlers for the LSP commands are defined as a map:

private handlers = new Map(  
    getEntries<(request: protocol.Request) => HandlerResponse>({  
        ...  
        [CommandNames.ProvideInlayHints]:  
            (request: protocol.InlayHintsRequest) => this.requiredResponse(this.provideInlayHints(request.arguments));  
        }));

Now, we arrive at the provideInlayHints function, what this method does it to call the LanguageService.provideInlayHints() method. Which is located at src/services/inlayHints.ts. And this is where TypeScript implemented the inlay hints feature.

How provideInlayHints works

Now, let’s take a look at the InlayHints.provideInlayHints function: it takes an InlayHintsContext and returns an array of InlayHint.

Some interesting information in the InlayHintsContext:

export interface InlayHintsContext {
    file: SourceFile;
    program: Program;
    cancellationToken: CancellationToken;
    host: LanguageServiceHost;
    span: TextSpan;
    preferences: UserPreferences;
}
  • The compiling context of the source file (where you can access the source, the AST, the type checker,…)
  • The TextSpan (is the position of text that we want to process the inlay hints)
  • The preferences from the user’s editor (In IntelliJ, you can see the options at Preferences -> Editor -> InlayHints -> TypeScript)
  • And lastly, the CancellationToken, I guess it’s for cancel the previous inlay hints requests.

An InlayHint has the structure of:

export interface InlayHint {
    text: string;
    position: number;
    kind: InlayHintKind;
    whitespaceBefore?: boolean;
    whitespaceAfter?: boolean;
}
  • The text content
  • The position where we will add the inlay hints to
  • The kind of inlay hints

You can see the details of the above types at src/services/types.ts.

In the InlayHints.provideInlayHints function, there are 3 methods: addParameterHints, addTypeHints, and addEnumMemberValueHints, which push a new InlayHint object into the result array:

function addParameterHints(text: string, position: number, isFirstVariadicArgument: boolean) {
    result.push({
        ...
        kind: InlayHintKind.Parameter,
        ...
    });
}
 
function addTypeHints(text: string, position: number) {
    result.push({
        ...
        kind: InlayHintKind.Type,
        ...
    });
}
 
function addEnumMemberValueHints(text: string, position: number) {
    result.push({
        ...
        kind: InlayHintKind.Enum,
        ...
    });
}

So we know there are 3 types of inlay hints:

  1. Parameter Hint: Is the hint that appears in function calls parameters

  1. Type Hint: This is the hint that appears in variable or class/struct fields declarations

  1. Enum Member Value Hint: This is the hint for the values of enums

The most important part of InlayHints.provideInlayHints is the visitor() method.

What it does is to visit every node on the AST of the current source file, and only generate the inlay hint if the node is within the requested range from the LSP client, it also skips if the node is a type declaration:

if (!textSpanIntersectsWith(span, node.pos, node.getFullWidth())) {  
    return;  
}  
  
if (isTypeNode(node)) {  
    return;  
}  

Now, for each valid node, more checks will be performed, if the node matches one of the three above inlay hints types, we get the Node’s type and create the InlayHint:

const declarationType = checker.getTypeAtLocation(decl);  
...  
  
const typeDisplayString = printTypeInSingleLine(declarationType);  
if (typeDisplayString) {  
    addTypeHints(typeDisplayString, decl.name.end);  
}  

There are some interesting pieces like the printTypeInSingleLine(type: Type) to format the inlay hints of a type into a single line, for example, with the following type:

{  
    foo: number;  
    bar: string;  
}  

The inlay hint string will be written as:

{ foo: number; bar: string; }  

Lastly, all the InlayHint will be sent back to the code editor to render on the screen:

Closing notes

In case you are lazy and scroll all the way down here from the beginning, here’s a recap:

(You can open the image in a new tab for a better resolution)

At this point, we are pretty clear about how the inlay hints feature works, what we should do next is dig deeper into the AST traversal, messing up with various types of user preferences or the InlayHint structure to see how things change in the tests and maybe in your editor!

All the files in this walkthrough: