断点进不去 language server 的 server.ts,是因为其采用分离式架构,server 是独立子进程,默认未被 vscode attach;需用 node --inspect 启动并配置 launch.json 的 attach 模式,同时确保 sourcemaps、outfiles 和调试端口正确。

VSCode 调试语言服务(Language Server)不是启动一个进程就能连上的事,关键在于它和编辑器之间必须通过语言服务器协议(LSP)完成双向握手,且调试目标是 language server 进程本身,不是你写的插件主逻辑。
为什么断点进不去 language server 的 server.ts?
因为绝大多数 VSCode 插件语言服务是“分离式”架构:插件前端(extension)负责注册 LSP 客户端,真正干活的 server 是独立子进程(比如用 tsc、typescript-language-server 或自研 Node.js 服务),VSCode 默认不 attach 它。
- 直接在 extension 的
activate里打的断点,只能拦住插件激活过程,拦不住后续 LSP 请求流转 - 如果你的
server是用node --inspect启动的,但没在launch.json中配attach模式,VSCode 根本不会连接那个调试端口 - 常见错误现象:
console.log有输出,但断点灰掉、提示 “Breakpoint ignored because generated code not found”——说明源码映射(source map)没对上,或 server 没走 TypeScript 编译路径
如何正确 attach 到 language server 进程?
必须让 server 进程带调试参数启动,并在 VSCode 中用 attach 配置连接。不推荐用 launch 直接跑 server 入口,容易绕过插件环境上下文。
- 在插件 package.json 的
activationEvents或scripts中,把 server 启动命令改为:node --inspect=6009 ./out/server.js(端口避开 9229,防冲突) -
.vscode/launch.json新增配置,type设为node,request设为attach,指定port和address(默认 localhost) - 确保
out/server.js旁存在server.js.map,且launch.json中启用sourceMaps: true和outFiles指向编译产物目录 - 如果 server 是用 ts-node 启动的,需加
--project ./tsconfig.json --files参数,否则断点无法解析原始.ts文件
调试时变量显示 undefined 或结构扁平化?
这是 LSP 消息体被序列化/反序列化两次导致的典型问题:client → server 传输用 JSON-RPC,server 内部再解析一次,原始对象原型链和 getter 全丢失。
- 不要在断点处直接展开
params或textDocument对象——它们是 JSON-Parsed 后的 plain object,TextDocument实例方法(如getText())已不可用 - 想查真实内容,改用
JSON.stringify(params, null, 2)打印,或在 server 启动前加util.inspect日志 - 若用
vscode-language-client,可在 client 端监听onNotification或onRequest,那里拿到的是未序列化的原始 JS 对象(仅限 client 侧调试) - 性能敏感场景下,避免在
textDocument/didChange回调中做深克隆或递归遍历,LSP 规范明确要求 client 不保证 payload 引用稳定性
本地开发时,server 修改后为何不生效?
LSP server 通常不支持热重载,尤其是涉及初始化逻辑(initialize 方法)或能力注册(capabilities)的部分。改了就得重启整个 server 进程,而不是 reload 插件。
- VSCode 插件 host 会缓存已加载的 server 模块,即使你改了
server.ts并重新编译,旧require缓存仍生效 - 简单粗暴但有效:每次改完 server,手动在终端 kill 掉
node --inspect进程,再重新运行启动命令 - 更可持续的做法:在插件
deactivate中显式调用languageClient.stop(),并在activate中检查languageClient.needsStart,避免重复实例化 - 注意:某些语言服务(如
pyright)会 fork 子进程,kill 主进程不一定能干掉 worker,得用ps aux | grep pyright确认并清理
最常被忽略的一点:LSP 初始化阶段的 rootUri 和 workspaceFolders 来自 client(即你的插件),不是 server 自己猜的。如果调试时发现 server 加载不到项目配置(比如 tsconfig.json 找不到),先检查 client 发过去的初始化参数是否包含正确的 rootUri,而不是一头扎进 server 代码里翻路径拼接逻辑。











