美文网首页Node.js
[Node] 淡如止水 TypeScript (十):自动补全

[Node] 淡如止水 TypeScript (十):自动补全

作者: 何幻 | 来源:发表于2020-01-06 20:19 被阅读0次

0. 回顾

上文我们向 tsserver 发送了两条消息,然后跟踪了进程间的通信过程。
并没有深入到 tsserver 源码中去看消息是怎么处理的。

需要留意的有以下两点:
(1)使用 child.stdin.write 向子进程写入消息内容时,应以 \n 结尾
这是因为 tsserver 监控了 .on('line', xxx => ...) 事件,src/tsserver/server.ts#L574
消息不以 \n 结尾,会被认为尚未发送完。

class ... extends Session {
  ...

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

    ...
  }
}

(2)tsserver 子进程自动后,在不接受任何父进程的消息前,会向父进程返回一条消息,

Content-Length: 76

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

在进行调试时,很容易被这条消息干扰到。

1. 补全概述

本文用 completions command 为例,来介绍 TypeScript 补全是怎样实现的。

我们先来宏观的看一下,

getCompletions
  project.getLanguageService().getCompletionsAtPosition
    Completions.getCompletionsAtPosition
      getCompletionData
        getTypeScriptMemberSymbols
          typeChecker.getTypeAtLocation
            ...
      completionInfoFromData
  sort
    compareStringsCaseSensitiveUI
      compareWithCallback
        new Intl.Collator

(1)getCompletions 调用了语言服务 project.getLanguageService()getCompletionsAtPosition 来获取补全列表。
里面又调用了 typeChecker 来获取类型信息。
(2)上述过程返回补全列表之后,getCompletions 函数中,再对列表进行排序。

2. 关于补全位置

修改 debug/index.ts 文件内容如下,

console.

我们看到输入 . 之后,VSCode 弹出了补全下拉框,里面总共有 28 项目。
现在,我们想探索一下,tsserver 到底是怎样计算出这个列表的。

为此,我们需要修改 client 端的 index.js 文件,向 tsserver 发送 completions command。

const path = require('path');
const { spawn } = require('child_process');

const root = '/Users/.../TypeScript';  // <- 这是 TypeScript 源码仓库的根目录

const child = spawn('node', [
  '--inspect-brk=9002',
  path.join(root, 'bin/tsserver'),
]);

child.stdout.on('data', data => {
  console.log(data.toString());
});

child.on('close', code => {
  console.log(code);
});

const filePath = path.join(root, 'debug/index.ts');

const openFile = {
  seq: 0,
  type: 'request',
  command: 'open',
  arguments: {
    file: filePath,
  }
};
const getCompletions = {
  seq: 1,
  type: 'request',
  command: 'completions',
  arguments: {
    file: filePath,
    line: 1,
    offset: 9,
  }
};

child.stdin.write(`${JSON.stringify(openFile)}\n`);
child.stdin.write(`${JSON.stringify(getCompletions)}\n`);

注意到 line: 1offset: 9,这两个值正是 VSCode 提示的行列号,Ln 1, Col 9

3. 补全逻辑

3.1 回顾:消息处理过程

我们先启动 client 端,然后再 attach 到 tsserver,


我们知道 tsserver 启动后,会主动向主进程返回一条消息,这里不再赘述。
上文的例子中,client 端通过 child.stdin.write 发送了两条消息,
因此,server 端启动之后,会收到两次消息。

第一次是 open command,我们暂且略过,

第二次就是 completions command,重点看它,

3.2 getCompletions

处理完消息之后,TypeScript 马上调用了 getCompletionssrc/server/session.ts#L1585
它是补全逻辑的入口函数,

图中,最下方的 (ananymous function) 就是 tsserver 监听 stdin 的回调函数了。

export class Session implements EventSender {
  ...

  private getCompletions(...): ... {
    ...
    const completions = project.getLanguageService().getCompletionsAtPosition(file, position, {
      ...
    });

    ...
    const entries = mapDefined<...>(completions.entries, entry => {
      ...
    }).sort((a, b) => compareStringsCaseSensitiveUI(a.name, b.name));

    ...
  }
  ...
}

getCompletions 先调用 project.getLanguageService().getCompletionsAtPosition 获取补全列表。
然后再调用 sort compareStringsCaseSensitiveUI 进行排序。

3.3 补全列表

我们主要看补全数据是从哪里来的,即,project.getLanguageService().getCompletionsAtPosition
src/services/services.ts#L1446

function getCompletionsAtPosition(...): ... {
  ...
  return Completions.getCompletionsAtPosition(
    ...
  );
}

它调用了 Completions.getCompletionsAtPositionsrc/services/completions.ts#L134

export function getCompletionsAtPosition(
  ...
): ... {
  ...

  const completionData = getCompletionData(...);
  ...

  switch (completionData.kind) {
    case CompletionDataKind.Data:
      return completionInfoFromData(...);
    ...
  }
}

又调用了 getCompletionData,获取补全数据。
其中,completionInfoFromData 是对补全数据进行的后处理,可以不细看了。

getCompletionDatasrc/services/completions.ts#L778,这个函数非常长,有 1575 行,

function getCompletionData(
  ...
): ... {
  ...
  let symbols: Symbol[] = [];
  ...

  if (isRightOfDot || isRightOfQuestionDot) {
    getTypeScriptMemberSymbols();
  }
  ...

  ...
  return {
    ...
    symbols,
    ...
  };

  ...
}

它调用 getTypeScriptMemberSymbolssrc/services/completions.ts#L1076 修改了 symbols 变量,
symbols 变量中包含了补全数据。

然而,getTypeScriptMemberSymbols 调用链路特别长,

function getTypeScriptMemberSymbols(): void {
  ...

  if (!isTypeLocation) {
    let type = typeChecker.getTypeAtLocation(node).getNonOptionalType();
    ...
  }
}

调用了 typeChecker 来获取类型信息,补全数据原来在 type 中。

因此,补全数据是 typeChecker 计算出来的,

project.getLanguageService().getCompletionsAtPosition
  Completions.getCompletionsAtPosition
    getCompletionData             // 获取补全数据
      getTypeScriptMemberSymbols  // 从 type 中获取补全数据
        typeChecker.getTypeAtLocation
          ...
    completionInfoFromData        // 后处理

4. typeChecker.getTypeAtLocation

由于 typeChecker.getTypeAtLocation 的调用链路太长了,
因此,我们专门另开一节来介绍它。

首先,在 resolveNameHelpersrc/compiler/checker.ts#L1752,第 1752 行加一个条件断点,

name === 'Console'

然后启动调试,



这样就能得到一个完整的调用链了。

resolveNameHelpergetTypeAtLocation 都是 typeChecker 的代码逻辑。

resolveNameHelper
...
typeChecker.getTypeAtLocation
getTypeScriptMemberSymbols
getCompletionData
Completions.getCompletionsAtPosition
project.getLanguageService().getCompletionsAtPosition
...

resolveNameHelpersrc/compiler/checker.ts#L1442,调用了 lookup

function resolveNameHelper(
  ...

  if (!result) {
    ...

    if (!excludeGlobals) {
      result = lookup(globals, name, meaning);
    }
  }
  
  ...
  return result;
}

lookup 从全局配置中,查询与 name 关联的结果。
由于我们设置好了条件断点,这里的 name 值为 Console

globals 是预先计算好的很多个键值对,

我们关心的 Console 补全列表,在 541 这个位置,

至于 globals 是怎么计算出来的,我们就不再展开了。


总结

本文介绍了 tsserver 中补全相关的实现逻辑,从发送 completions command 出发,
我们跟到了 typeChecker 里面,找到了补全信息源头位置。

拿到源数据之后,tsserver 又进行了一些排序处理,最终返回给父进程。
以下就是完整的调用链路了,

getCompletions
  project.getLanguageService().getCompletionsAtPosition
    Completions.getCompletionsAtPosition
      getCompletionData
        getTypeScriptMemberSymbols
          typeChecker.getTypeAtLocation
            ...
            resolveNameHelper
              lookup
      completionInfoFromData
  sort
    compareStringsCaseSensitiveUI
      compareWithCallback
        new Intl.Collator

参考

TypeScript v3.7.3

相关文章

网友评论

    本文标题:[Node] 淡如止水 TypeScript (十):自动补全

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