[Node] 隨遇而安 TypeScript(七):Debug VSCode Refactor

背景

有很多優(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-brkcommit 已經(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é)果的鹤盒。


參考

Language Server Protocol
vscode v1.45.1
typescript v3.7.3

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末军掂,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子昨悼,更是在濱河造成了極大的恐慌,老刑警劉巖跃洛,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件率触,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡汇竭,警方通過查閱死者的電腦和手機(jī)葱蝗,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來细燎,“玉大人两曼,你說我怎么就攤上這事〔Wぃ” “怎么了悼凑?”我有些...
    開封第一講書人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵偿枕,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我户辫,道長(zhǎng)渐夸,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任渔欢,我火速辦了婚禮墓塌,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘奥额。我一直安慰自己苫幢,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開白布垫挨。 她就那樣靜靜地躺著韩肝,像睡著了一般。 火紅的嫁衣襯著肌膚如雪棒拂。 梳的紋絲不亂的頭發(fā)上伞梯,一...
    開封第一講書人閱讀 49,144評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音帚屉,去河邊找鬼谜诫。 笑死,一個(gè)胖子當(dāng)著我的面吹牛攻旦,可吹牛的內(nèi)容都是我干的喻旷。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼牢屋,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼且预!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起烙无,我...
    開封第一講書人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤锋谐,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后截酷,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體涮拗,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年迂苛,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了三热。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡三幻,死狀恐怖就漾,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情念搬,我是刑警寧澤抑堡,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布摆出,位于F島的核電站,受9級(jí)特大地震影響夷野,放射性物質(zhì)發(fā)生泄漏懊蒸。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一悯搔、第九天 我趴在偏房一處隱蔽的房頂上張望骑丸。 院中可真熱鬧,春花似錦妒貌、人聲如沸通危。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽菊碟。三九已至,卻和暖如春在刺,著一層夾襖步出監(jiān)牢的瞬間逆害,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工蚣驼, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留魄幕,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓颖杏,卻偏偏與公主長(zhǎng)得像纯陨,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子留储,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345