美文网首页Other
[IDE] typescript-language-server

[IDE] typescript-language-server

作者: 何幻 | 来源:发表于2019-07-23 16:50 被阅读15次

    背景

    基于 LSP 的 TypeScript Language Servers,有两个开源实现,

    其中,theia-ide 支持更多的 ServerCapabilities

    ServerCapabilities theia-ide sourcegraph
    textDocumentSync
    hoverProvider
    completionProvider
    signatureHelpProvider
    definitionProvider
    referencesProvider
    documentHighlightProvider
    documentSymbolProvider
    workspaceSymbolProvider
    codeActionProvider
    codeLensProvider
    documentFormattingProvider
    documentRangeFormattingProvider
    documentOnTypeFormattingProvider
    renameProvider
    documentLinkProvider
    executeCommandProvider
    experimental
    implementationProvider
    typeDefinitionProvider
    workspace.workspaceFolders
    colorProvider
    foldingRangeProvider
    declarationProvider
    selectionRangeProvider

    下文我们简单阅读以下 theia-ide/typescript-language-server 的源码,
    看看以上这些 ServerCapabilities 是怎样实现的。


    1. language-server

    (1)克隆源码 & 安装依赖

    $ git clone https://github.com/theia-ide/typescript-language-server.git
    $ cd typescript-language-server
    $ yarn
    

    (2)启动调试

    修改单测 server/src/file-lsp-server.spec.ts,添加 .only 只跑这一条单测,
    然后在 createServer第19行)打个断点,

    F5 开始调试,
    (注意,要在 file-lsp-server.spec.ts 文件中按 F5

    (3)lspServer 初始化

    Step Into 单步调试,进入 createServer 函数中,

    server.initialize第72行)打个断点,按 F5 运行到断点,

    Step Into 进入 initialize 函数中,
    (如果无法进入这个函数,就在函数中打断点,重新启动调试进入)

    (4)启动 tspClient 与 tsserver 通信

    initialize 函数中,会先 new TspClient,然后执行 tspClient.start()
    TspClient 构造函数只初始化了 logger,

    主要逻辑在 start 中,

    cp.spawn 位置(第106行)打个断点,按 F5 运行到断点位置,



    可以看到这里,会衍生出一个子进程,相当于执行以下 shell 命令,

    $ tsserver --cancellationPipeName /private/var/folders/df/57vsznhs05qb8h15mw5qxg3r0000gn/T/6145afdd56d84e3d8a83d87bca78382a/tscancellation*
    

    它会启动一个 tsserver,这个 tsserver 是全局安装 typescript 时添加的,
    tsserver 相关的可详见本文第二节)

    $ which tsserver
    /Users/thzt/.nvm/versions/node/v10.15.0/bin/tsserver
    

    启动 tsserver 后,使用 node 内置的 readline 模块 createInterface

    然后监听 readlineInterfaceline 事件,
    (类似于 child.stdout.on('data', ...)

    this.readlineInterface.on('line', line => this.processMessage(line));
    

    (5)通信示例

    先结束本次调试,把所有的断点都删掉,
    (Debug - Remove All Breakpoints)


    然后,把 line 事件监听代码改成两行,在 第116行 打个断点,

    this.readlineInterface.on('line', line => {
      return this.processMessage(line);    // <- 在这一行打断点
    });
    

    然后打开 lsp-server.tsinitialize 函数,this.tspClient.request第125行)打个断点,

    重新启动调试,
    (打开 file-lsp-server.spec.tsF5

    tspClient 会发送一个 configure 命令给 tsserver,按 F5 运行到下一个断点,

    读到了子进程 tsserver stdout 的第一行,

    Content-Length: 76
    

    再按 F5,读到了第二行,是个空行,

    再按 F5,独到了最后一行,

    {"seq":0,"type":"event","event":"typingsInstallerPid","body":{"pid":81542}}
    

    这就是一个完整的通信过程了。

    2. tsserver

    The TypeScript standalone server (aka tsserver) is a node executable that encapsulates the TypeScript compiler and language services, and exposes them through a JSON protocol. tsserver is well suited for editors and IDE support.

    (1)安装 & 运行

    安装 tsserver

    $ tnpm init -f
    $ tnpm i -S typescript
    

    会在 node_modules/.bin 目录安装两个可执行文件,

    ./node_modules/.bin/tsc
    ./node_modules/.bin/tsserver
    

    启动 tsserver ,(二选一)

    $ node node_modules/.bin/tsserver
    $ npx tsserver
    

    (2)父子进程通过 stdio 通信

    tsserver listens on stdin and writes messages back to stdout.

    index.js

    const path = require('path');
    const cp = require('child_process');
    
    const child = cp.spawn('npx', ['tsserver']);
    
    child.stdout.on('data', data => {
      console.log('child.stdout.on:data');
      console.log(data.toString());
    });
    
    child.on('close', code => {
      console.log('child.stdout.on:close');
      console.log(code);
    });
    
    child.stdin.write(JSON.stringify({
      seq: 1,
      type: 'quickinfo',
      command: 'open',
      arguments: {
        file: path.resolve('./index.js'),
      }
    }));
    
    $ node index.js
    child.stdout.on:data
    Content-Length: 76
    
    {"seq":0,"type":"event","event":"typingsInstallerPid","body":{"pid":80170}}
    

    3. typescript

    (1)克隆 & build

    克隆 typescript 仓库,并 resetv3.5.3 时的代码,

    $ git clone https://github.com/microsoft/TypeScript.git
    $ cd TypeScript
    $ git reset --hard aff7ef12305de00cca7ac405fdaf8402ba0e6973    # v3.5.3 的 commit id
    

    安装依赖,

    $ tnpm i -g gulp
    $ tnpm i
    

    构建到 built/local/,

    $ gulp local
    

    注:阅读源码之前,要先 gulp local,不然有些定义无法直接跳转过去。

    其他命令,

    gulp local            # Build the compiler into built/local
    gulp clean            # Delete the built compiler
    gulp LKG              # Replace the last known good with the built one.
                          # Bootstrapping step to be executed when the built compiler reaches a stable state.
    gulp tests            # Build the test infrastructure using the built compiler.
    gulp runtests         # Run tests using the built compiler and test infrastructure.
                          # You can override the host or specify a test for this command.
                          # Use --host=<hostName> or --tests=<testPath>.
    gulp baseline-accept  # This replaces the baseline test results with the results obtained from gulp runtests.
    gulp lint             # Runs tslint on the TypeScript source.
    gulp help             # List the above commands.
    

    (2)listener

    src/tsserver/server.ts#964
    tsserver 启动后开始 listen

    ioSession.listen();
    

    listen 位于 src/tsserver/server.ts#562
    接到 stdin 之后,会调用 this.onMessage

    listen() {
        rl.on("line", (input: string) => {
            ...
            this.onMessage(message);
        });
        ...
    }
    

    onMessage 的实现位于 src/server/session.ts#2489

    public onMessage(message: string) {
        ...
        try {
            ...
            const { response, responseRequired } = this.executeCommand(request);
            ...
        }
        catch (err) {
            ...
        }
    }
    

    其中,#2504 行,调用了 this.executeCommand

    executeCommand 的实现在 src/server/session.ts#2477

    public executeCommand(request: protocol.Request): HandlerResponse {
        const handler = this.handlers.get(request.command);
        if (handler) {
            return this.executeWithRequestId(request.seq, () => handler(request));
        }
        else {
            ...
        }
    }
    

    这里根据不同的 request.command,获取对应的 handler

    handlers 是由一个超级长(348行)的函数返回的,src/server/session.ts#2099-2446

    private handlers = createMapFromTemplate<(request: protocol.Request) => HandlerResponse>({
      [CommandNames.Status]: () => { ... },
      [CommandNames.OpenExternalProject]: (request: protocol.OpenExternalProjectRequest) => { ... },
      [CommandNames.OpenExternalProjects]: (request: protocol.OpenExternalProjectsRequest) => { ... },
      ...
    });
    

    总共包含了 91 个 CommandNames 的情况。

    (3)handler

    下面我们来看 CommandNames.Implementation 这个 handler
    src/server/session.ts#2195

    [CommandNames.Implementation]: (request: protocol.Request) => {
        return this.requiredResponse(this.getImplementation(request.arguments, /*simplifiedResult*/ true));
    },
    

    主要逻辑在 this.getImplementation
    src/server/session.ts#1063

    private getImplementation(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): ReadonlyArray<protocol.FileSpan> | ReadonlyArray<ImplementationLocation> {
        ...
        const implementations = this.mapImplementationLocations(project.getLanguageService().getImplementationAtPosition(file, position) || emptyArray, project);
        ...
    }
    

    我们看到这里,调用了 project.getLanguageService() 返回了一个 languageServer,
    然后调用了这个 languageServer 的 getImplementationAtPosition 方法。

    (4)services

    经过一番查找,看到实现在 src/services/services.ts#1540
    注意我们从 server 文件夹来到了 services 文件夹中,

    function getImplementationAtPosition(fileName: string, position: number): ImplementationLocation[] | undefined {
        synchronizeHostData();
        return FindAllReferences.getImplementationsAtPosition(program, cancellationToken, program.getSourceFiles(), getValidSourceFile(fileName), position);
    }
    

    后又调用了 FindAllReferences.getImplementationsAtPosition
    位于 src/services/findAllReferences.ts#62

    export function getImplementationsAtPosition(program: Program, cancellationToken: CancellationToken, sourceFiles: ReadonlyArray<SourceFile>, sourceFile: SourceFile, position: number): ImplementationLocation[] | undefined {
        const node = getTouchingPropertyName(sourceFile, position);
        const referenceEntries = getImplementationReferenceEntries(program, cancellationToken, sourceFiles, node, position);
        const checker = program.getTypeChecker();
        return map(referenceEntries, entry => toImplementationLocation(entry, checker));
    }
    

    getTouchingPropertyName 是得到当前位置的 ast node,

    Gets the token whose text has range [start, end) and position >= start and (position < end or (position === end && token is literal or keyword or identifier))

    getImplementationReferenceEntries 是计算该标识符的所有实现位置,

    function getImplementationReferenceEntries(program: Program, cancellationToken: CancellationToken, sourceFiles: ReadonlyArray<SourceFile>, node: Node, position: number): ReadonlyArray<Entry> | undefined {
        if (node.kind === SyntaxKind.SourceFile) {
            return undefined;
        }
    
        const checker = program.getTypeChecker();
        // If invoked directly on a shorthand property assignment, then return
        // the declaration of the symbol being assigned (not the symbol being assigned to).
        if (node.parent.kind === SyntaxKind.ShorthandPropertyAssignment) {
            const result: NodeEntry[] = [];
            Core.getReferenceEntriesForShorthandPropertyAssignment(node, checker, node => result.push(nodeEntry(node)));
            return result;
        }
        else if (node.kind === SyntaxKind.SuperKeyword || isSuperProperty(node.parent)) {
            // References to and accesses on the super keyword only have one possible implementation, so no
            // need to "Find all References"
            const symbol = checker.getSymbolAtLocation(node)!;
            return symbol.valueDeclaration && [nodeEntry(symbol.valueDeclaration)];
        }
        else {
            // Perform "Find all References" and retrieve only those that are implementations
            return getReferenceEntriesForNode(position, node, program, sourceFiles, cancellationToken, { implementations: true });
        }
    }
    

    分了3种情况,

    • ShorthandPropertyAssignment:属性名简写,例如 { a, b }
    • SuperKeyword:super 关键字
    • 其他:找到所有的引用,并过滤找出实现

    参考

    LSP
    Language Servers
    theia-ide/typescript-language-server v0.3.8
    typescript v3.5.3

    相关文章

      网友评论

        本文标题:[IDE] typescript-language-server

        本文链接:https://www.haomeiwen.com/subject/hvlalctx.html