- [Node] 随遇而安 TypeScript(七):Debug
- Typescript vscode debug
- [Node] 随遇而安 TypeScript(六):babel
- [Node] 随遇而安 TypeScript(五):typesc
- [Node] 随遇而安 TypeScript(八):TSServ
- [Node] 随遇而安 TypeScript(十):查找引用
- [Node] 随遇而安 TypeScript(一):查找符号
- [Node] 随遇而安 TypeScript(四):增量编译
- [Node] 随遇而安 TypeScript(九):多文件处理
- [Node] 随遇而安 TypeScript(二):符号表
背景
有很多优秀的代码编辑器,具有自动重构选中代码的功能,VSCode 也能执行这样的操作。
我们用 VSCode 打开一个 .ts 文件,
const f = x => {
x, y
};
鼠标选中函数体 x, y
,按快捷键 ⌘ + .
,就会弹出下图这样的选择框,
我们选第二个 Extract to function in global scope
,选中的代码就会被提取到一个全局函数中,
const f = x => {
newFunction(x);
};
function newFunction(x: any) {
x, y;
}
VSCode 到底是怎么做到的呢?
简单说,它是通过 Language Server Protocol 调用了 tsserver。
由 tsserver 返回了重构后的结果。
本文我们先来研究这条链路是怎么跑通的,下一篇文章再来探讨 tsserver 的业务逻辑。
1. 调试 vscode 和 tsserver
为了看清整条链路,我们需要同时对 vscode 和 tsserver 进行调试。
1.1 源码准备
vscode 源码,
我们选用了当前 release 最新的版本 v1.45.1。
$ git clone https://github.com/microsoft/vscode.git
$ cd vscode
$ git checkout 1.45.1
为了描述方便,我们将这里的 vscode
目录,记为 {VSCodeRoot}
。
tsserver 源码在 typescript 中,当前已经更新到 v3.9.3 了,
但为了保持与前几篇文章一致,我们仍然使用 v3.7.3。
$ git clone https://github.com/microsoft/TypeScript.git
$ cd TypeScript
$ git checkout v3.7.3
为了描述方便,我们将这里的 TypeScript
目录,记为 {TypeScriptRoot}
。
1.2 编译
$ cd {VSCodeRoot}
$ yarn
$ yarn compile
有些 node 版本中 yarn
会失败,我本机的 node 版本是 v10.17.0
。
yarn
版本是 1.22.4
。
$ cd {TypeScriptRoot}
$ npm i
$ node node_modules/.bin/gulp LKG
gulp LKG
,会将 src/
中的源码编译到 built/local/
文件夹中。
1.3 一些必要的软链接
为了能让 vscode 源码调用我们下载的 typescript 源码,需要添加一些软链接。
# 进入 vscode 源码根目录
$ cd {VSCodeRoot}
# 内置插件依赖的 typescript 目录改个名,不用这个目录了
$ mv extensions/node_modules/typescript extensions/node_modules/_typescript
# 软链到 typescript 源码根目录
$ ln -s {TypeScriptRoot} extensions/node_modules/typescript
vscode 源码中的内置插件,依赖的 TypeScript 默认位于 {VSCodeRoot}/extensions/node_modules
中。
为了对 TypeScript(tsserver)源码进行调试(固定 v3.7.3 版本,且用上 source map),
这里创建了一个软链接,让 vscode 直接依赖我们之前下载的 TypeScript 源码。
# 进入 typescript 源码根目录
$ cd {TypeScriptRoot}
# lib/ 目录改个名,不用这个目录了
$ mv lib _lib
# 将 lib/ 软链到 typescript 构建产物 built/local/ 目录
$ ln -s built/local lib
vscode 启动 tsserver 时,硬编码了 tsserver 的路径(即,lib
这个名字不能修改),
而这个路径下的 tsserver.js
是没有 source map 的。
为了能调试源码,我们将原来的 lib/
目录删掉,并建立软链接,指向 TypeScript 项目编译产物目录 built/local/
1.4 调试配置
打开 vscode 源码根目录 {VSCodeRoot}
中的 .vscode/launch.json
,
添加这样一个调试配置,名字记为 Debug TypeScript Extension
。
{
...,
"configurations": [
{
"type": "extensionHost",
"request": "launch",
"name": "Debug TypeScript Extension",
"runtimeExecutable": "${execPath}",
"args": [
"${workspaceFolder}",
"--extensionDevelopmentPath=${workspaceFolder}/extensions/typescript-language-features",
],
"outFiles": [
"${workspaceFolder}/extensions/typescript-language-features/out/**/*.js"
],
"env": {
"TSS_DEBUG": "9003",
}
},
...,
]
}
env.TSS_DEBUG
设置为了 9003
,这是 vscode 内置 TypeScript 插件(typescript-language-features)启动 tsserver 的调试端口号。
位于 extensions/typescript-language-features/src/tsServer/spawner.ts#L98。
const childProcess = electron.fork(version.tsServerPath, args, this.getForkOptions(kind, configuration));
getForkOptions(kind: ServerKind, configuration: TypeScriptServiceConfiguration) {
const debugPort = TypeScriptServerSpawner.getDebugPort(kind);
const tsServerForkOptions: electron.ForkOptions = {
execArgv: [
...(debugPort ? [`--inspect=${debugPort}`] : []),
...(configuration.maxTsServerMemory ? [`--max-old-space-size=${configuration.maxTsServerMemory}`] : [])
]
};
return tsServerForkOptions;
}
注意这里用了 --inspect
而不是 --inspect-brk
,
这说明 tsserver
启动后并不会停在第一行等待 attach,而是直接继续运行。
源码中支持 --inspect-brk
的 commit 已经 merge 到 master 了。
只是在写这篇文章的时候,还没有 release,
下一个 release 应该就可以使用 env.TSS_DEBUG_BRK
来配置 --inspect-brk
形式的调试端口号了。
typescript 源码目录 {TypeScript}
下是没有 .vscode/launch.json
的,
我们新建这样一个文件(或点击菜单:Run - Add configuration
也行),并添加如下配置,
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "attach",
"name": "attach to tsserver",
"port": 9003,
"skipFiles": [
"<node_internals>/**"
]
}
]
}
注意到这里的 port
端口号,与 vscode 那边的 env.TSS_DEBUG
应保持一致。
1.5 启动调试
用 VSCode 打开 vscode 源码目录 {VSCodeRoot}
,
提前在 extensions/typescript-language-features/src/extension.ts#L27,
typescript 插件(typescript-language-features)的激活函数 activate
中第一行打个断点。
在调试面板中选择刚才创建的配置 Debug TypeScript Extension
,按 F5
启动调试。
它会打开一个新的 VSCode 窗口,名为 [Extension Development Host]
。
我们在这个窗口中打开一个 .ts 文件,以激活 typescript 插件(typescript-language-features)。
vscode 那边已激活 typescript 插件(typescript-language-features)之后(按 F5
运行下去),
用 VSCode 打开 typescript 源码目录 {TypeScriptRoot}
,
直接按 F5
,启动调试(因为就一个配置,默认选中了 attach to tsserver
)。
看起来好像没有反应,其实已经
attach
到 tsserver 了。上文我们提到了,这是因为当前版本的 vscode(1.45.1)采用了
--inspect
方式启动 tsserver
,而不是 --inspect-brk
。
2. 业务逻辑
按照上文的介绍,我们已经启动了 vscode 源码仓库中的 typescript 插件(typescript-language-features),
这个插件启动的 tsserver 我们也已经 attach 上了。
下面我们来看一下整体的代码重构逻辑。
2.1 tsserver
先到 typescript 源码仓库 getEditsForRefactor
函数中打个断点,
src/services/refactorProvider.ts#L36
export function getEditsForRefactor(context: RefactorContext, refactorName: string, actionName: string): RefactorEditInfo | undefined {
const refactor = refactors.get(refactorName);
return refactor && refactor.getEditsForAction(context, actionName);
}
然后在 vscode 起来的 [Extension Development Host]
窗口中(已打开了一个 .ts 文件),重复本文开篇背景中介绍的操作步骤。
const f = x => {
x, y
};
选中 x, y
,按 ⌘ + .
,选择 Extract to function in global scope
。
就发现代码跑到了 getEditsForRefactor
函数的断点处了。
这说明重构操作确实用到了 tsserver,跑到了 tsserver 的代码中。
查看调用栈,tsserver 是通过监听 message 的方式,来执行代码重构操作的。
2.2 typescript-language-features
vscode 这边是怎么发送消息的呢?
通过查看 typescript 插件(typescript-language-features)的激活逻辑,或者搜索 getEditsForRefactor
关键字,
我们发现,消息是在 extensions/typescript-language-features/src/features/refactor.ts#L77 execute
函数中发送的,
class ApplyRefactoringCommand implements Command {
public async execute(
...,
): Promise<boolean> {
...,
const response = await this.client.execute('getEditsForRefactor', args, nulToken);
...,
const workspaceEdit = await this.toWorkspaceEdit(response.body);
...,
}
}
execute
函数,先是给 tsserver 发消息,得到了重构结果,
然后再调用 this.toWorkspaceEdit
将改动结果应用到编辑器中。
查看调用栈,可以粗略的识别出这是一个响应前端快捷键,然后再向 tsserver 发送消息的过程。
总结
本文花了较大篇幅介绍 vscode + typescript(tsserver)的联合调试过程。
这个过程看起来很简单,但其实跑通它也花费了不少的精力。
一图胜千言,我们借助软链接,让 vscode 启动了我们 typescript 源码中的 tsserver。
链路通了以后,再研究重构相关的代码逻辑就事半功倍了。
下文开始探讨 tsserver getEditsForRefactor
,看它是怎样得到重构结果的。
网友评论