背景
有很多優(yōu)秀的代碼編輯器,具有自動(dòng)重構(gòu)選中代碼的功能,VSCode 也能執(zhí)行這樣的操作胆描。
我們用 VSCode 打開一個(gè) .ts 文件,
const f = x => {
x, y
};
鼠標(biāo)選中函數(shù)體 x, y
仗阅,按快捷鍵 ? + .
昌讲,就會(huì)彈出下圖這樣的選擇框,
我們選第二個(gè) Extract to function in global scope
减噪,選中的代碼就會(huì)被提取到一個(gè)全局函數(shù)中短绸,
const f = x => {
newFunction(x);
};
function newFunction(x: any) {
x, y;
}
VSCode 到底是怎么做到的呢?
簡(jiǎn)單說筹裕,它是通過 Language Server Protocol 調(diào)用了 tsserver醋闭。
由 tsserver 返回了重構(gòu)后的結(jié)果。
本文我們先來研究這條鏈路是怎么跑通的饶碘,下一篇文章再來探討 tsserver 的業(yè)務(wù)邏輯目尖。
1. 調(diào)試 vscode 和 tsserver
為了看清整條鏈路,我們需要同時(shí)對(duì) vscode 和 tsserver 進(jìn)行調(diào)試扎运。
1.1 源碼準(zhǔn)備
vscode 源碼瑟曲,
我們選用了當(dāng)前 release 最新的版本 v1.45.1。
$ git clone https://github.com/microsoft/vscode.git
$ cd vscode
$ git checkout 1.45.1
為了描述方便豪治,我們將這里的 vscode
目錄洞拨,記為 {VSCodeRoot}
。
tsserver 源碼在 typescript 中负拟,當(dāng)前已經(jīng)更新到 v3.9.3 了烦衣,
但為了保持與前幾篇文章一致,我們?nèi)匀皇褂?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
會(huì)失敗衅澈,我本機(jī)的 node 版本是 v10.17.0
键菱。
yarn
版本是 1.22.4
。
$ cd {TypeScriptRoot}
$ npm i
$ node node_modules/.bin/gulp LKG
gulp LKG
今布,會(huì)將 src/
中的源碼編譯到 built/local/
文件夾中经备。
1.3 一些必要的軟鏈接
為了能讓 vscode 源碼調(diào)用我們下載的 typescript 源碼,需要添加一些軟鏈接部默。
# 進(jìn)入 vscode 源碼根目錄
$ cd {VSCodeRoot}
# 內(nèi)置插件依賴的 typescript 目錄改個(gè)名侵蒙,不用這個(gè)目錄了
$ mv extensions/node_modules/typescript extensions/node_modules/_typescript
# 軟鏈到 typescript 源碼根目錄
$ ln -s {TypeScriptRoot} extensions/node_modules/typescript
vscode 源碼中的內(nèi)置插件,依賴的 TypeScript 默認(rèn)位于 {VSCodeRoot}/extensions/node_modules
中傅蹂。
為了對(duì) TypeScript(tsserver)源碼進(jìn)行調(diào)試(固定 v3.7.3 版本纷闺,且用上 source map),
這里創(chuàng)建了一個(gè)軟鏈接份蝴,讓 vscode 直接依賴我們之前下載的 TypeScript 源碼急但。
# 進(jìn)入 typescript 源碼根目錄
$ cd {TypeScriptRoot}
# lib/ 目錄改個(gè)名,不用這個(gè)目錄了
$ mv lib _lib
# 將 lib/ 軟鏈到 typescript 構(gòu)建產(chǎn)物 built/local/ 目錄
$ ln -s built/local lib
vscode 啟動(dòng) tsserver 時(shí)搞乏,硬編碼了 tsserver 的路徑(即,lib
這個(gè)名字不能修改)戒努,
而這個(gè)路徑下的 tsserver.js
是沒有 source map 的请敦。
為了能調(diào)試源碼,我們將原來的 lib/
目錄刪掉储玫,并建立軟鏈接侍筛,指向 TypeScript 項(xiàng)目編譯產(chǎn)物目錄 built/local/
1.4 調(diào)試配置
打開 vscode 源碼根目錄 {VSCodeRoot}
中的 .vscode/launch.json
,
添加這樣一個(gè)調(diào)試配置撒穷,名字記為 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
設(shè)置為了 9003
,這是 vscode 內(nèi)置 TypeScript 插件(typescript-language-features)啟動(dòng) tsserver 的調(diào)試端口號(hào)端礼。
位于 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
啟動(dòng)后并不會(huì)停在第一行等待 attach蛤奥,而是直接繼續(xù)運(yùn)行佳镜。
源碼中支持 --inspect-brk
的 commit 已經(jīng) merge 到 master 了。
只是在寫這篇文章的時(shí)候凡桥,還沒有 release蟀伸,
下一個(gè) release 應(yīng)該就可以使用 env.TSS_DEBUG_BRK
來配置 --inspect-brk
形式的調(diào)試端口號(hào)了。
typescript 源碼目錄 {TypeScript}
下是沒有 .vscode/launch.json
的,
我們新建這樣一個(gè)文件(或點(diǎn)擊菜單:Run - Add configuration
也行)啊掏,并添加如下配置蠢络,
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "attach",
"name": "attach to tsserver",
"port": 9003,
"skipFiles": [
"<node_internals>/**"
]
}
]
}
注意到這里的 port
端口號(hào),與 vscode 那邊的 env.TSS_DEBUG
應(yīng)保持一致迟蜜。
1.5 啟動(dòng)調(diào)試
用 VSCode 打開 vscode 源碼目錄 {VSCodeRoot}
刹孔,
提前在 extensions/typescript-language-features/src/extension.ts#L27,
typescript 插件(typescript-language-features)的激活函數(shù) activate
中第一行打個(gè)斷點(diǎn)小泉。
在調(diào)試面板中選擇剛才創(chuàng)建的配置 Debug TypeScript Extension
芦疏,按 F5
啟動(dòng)調(diào)試。
它會(huì)打開一個(gè)新的 VSCode 窗口微姊,名為 [Extension Development Host]
酸茴。
我們?cè)谶@個(gè)窗口中打開一個(gè) .ts 文件兢交,以激活 typescript 插件(typescript-language-features)薪捍。
vscode 那邊已激活 typescript 插件(typescript-language-features)之后(按 F5
運(yùn)行下去),
用 VSCode 打開 typescript 源碼目錄 {TypeScriptRoot}
配喳,
直接按 F5
酪穿,啟動(dòng)調(diào)試(因?yàn)榫鸵粋€(gè)配置,默認(rèn)選中了 attach to tsserver
)晴裹。
看起來好像沒有反應(yīng)被济,其實(shí)已經(jīng)
attach
到 tsserver 了。上文我們提到了涧团,這是因?yàn)楫?dāng)前版本的 vscode(1.45.1)采用了
--inspect
方式啟動(dòng) tsserver
只磷,而不是 --inspect-brk
。
2. 業(yè)務(wù)邏輯
按照上文的介紹泌绣,我們已經(jīng)啟動(dòng)了 vscode 源碼倉(cāng)庫(kù)中的 typescript 插件(typescript-language-features)钮追,
這個(gè)插件啟動(dòng)的 tsserver 我們也已經(jīng) attach 上了。
下面我們來看一下整體的代碼重構(gòu)邏輯阿迈。
2.1 tsserver
先到 typescript 源碼倉(cāng)庫(kù) getEditsForRefactor
函數(shù)中打個(gè)斷點(diǎn)元媚,
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]
窗口中(已打開了一個(gè) .ts 文件),重復(fù)本文開篇背景中介紹的操作步驟苗沧。
const f = x => {
x, y
};
選中 x, y
刊棕,按 ? + .
,選擇 Extract to function in global scope
待逞。
就發(fā)現(xiàn)代碼跑到了 getEditsForRefactor
函數(shù)的斷點(diǎn)處了鞠绰。
這說明重構(gòu)操作確實(shí)用到了 tsserver,跑到了 tsserver 的代碼中飒焦。
查看調(diào)用棧蜈膨,tsserver 是通過監(jiān)聽 message 的方式屿笼,來執(zhí)行代碼重構(gòu)操作的。
2.2 typescript-language-features
vscode 這邊是怎么發(fā)送消息的呢翁巍?
通過查看 typescript 插件(typescript-language-features)的激活邏輯驴一,或者搜索 getEditsForRefactor
關(guān)鍵字,
我們發(fā)現(xiàn)灶壶,消息是在 extensions/typescript-language-features/src/features/refactor.ts#L77 execute
函數(shù)中發(fā)送的肝断,
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
函數(shù),先是給 tsserver 發(fā)消息驰凛,得到了重構(gòu)結(jié)果胸懈,
然后再調(diào)用 this.toWorkspaceEdit
將改動(dòng)結(jié)果應(yīng)用到編輯器中。
查看調(diào)用棧恰响,可以粗略的識(shí)別出這是一個(gè)響應(yīng)前端快捷鍵趣钱,然后再向 tsserver 發(fā)送消息的過程。
總結(jié)
本文花了較大篇幅介紹 vscode + typescript(tsserver)的聯(lián)合調(diào)試過程胚宦。
這個(gè)過程看起來很簡(jiǎn)單首有,但其實(shí)跑通它也花費(fèi)了不少的精力。
一圖勝千言枢劝,我們借助軟鏈接井联,讓 vscode 啟動(dòng)了我們 typescript 源碼中的 tsserver。
鏈路通了以后您旁,再研究重構(gòu)相關(guān)的代碼邏輯就事半功倍了烙常。
下文開始探討 tsserver getEditsForRefactor
,看它是怎樣得到重構(gòu)結(jié)果的鹤盒。