简单调试语言服务器
在上一章中,我们实现了一个最小的语言服务器的客户端和服务端,它们没有任何功能,但是我们可以用它们来简单调试其他实际的语言服务器。
在 语言服务器的实现列表 中找到了 typescript-language-server,我们来尝试调试它。
在 min-lsp-example
项目中,执行:
npm i typescript typescript-language-server
然后修改 client.js
:
-const ls = spawn('node', ['server.js']);
+const ls = spawn('node', ['node_modules/typescript-language-server/lib/cli.mjs', '--stdio']);
运行:
node client.js
会产生如下输出:
===> {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"processId":53519,"rootUri":null,"capabilities":{}}}
<=== {"jsonrpc":"2.0","method":"window/logMessage","params":{"type":3,"message":"Using Typescript version (bundled) 5.5.2 from path \"/Users/renwei/Code/min-lsp-example/node_modules/typescript/lib/tsserver.js\""}}
<=== {"jsonrpc":"2.0","id":1,"result":{"capabilities":{"textDocumentSync":2,"completionProvider":{"triggerCharacters":[".","\"","'","/","@","<"],"resolveProvider":true},"codeActionProvider":true,"codeLensProvider":{"resolveProvider":true},"definitionProvider":true,"documentFormattingProvider":true,"documentRangeFormattingProvider":true,"documentHighlightProvider":true,"documentSymbolProvider":true,"executeCommandProvider":{"commands":["_typescript.applyWorkspaceEdit","_typescript.applyCodeAction","_typescript.applyRefactoring","_typescript.configurePlugin","_typescript.organizeImports","_typescript.applyRenameFile","_typescript.goToSourceDefinition"]},"hoverProvider":true,"inlayHintProvider":true,"linkedEditingRangeProvider":false,"renameProvider":true,"referencesProvider":true,"selectionRangeProvider":true,"signatureHelpProvider":{"triggerCharacters":["(",",","<"],"retriggerCharacters":[")"]},"workspaceSymbolProvider":true,"implementationProvider":true,"typeDefinitionProvider":true,"foldingRangeProvider":true,"semanticTokensProvider":{"documentSelector":null,"legend":{"tokenTypes":["class","enum","interface","namespace","typeParameter","type","parameter","variable","enumMember","property","function","member"],"tokenModifiers":["declaration","static","async","readonly","defaultLibrary","local"]},"full":true,"range":true},"workspace":{"fileOperations":{"willRename":{"filters":[{"scheme":"file","pattern":{"glob":"**/*.{ts,js,jsx,tsx,mjs,mts,cjs,cts}","matches":"file"}},{"scheme":"file","pattern":{"glob":"**","matches":"folder"}}]}}}}}}
===> {"jsonrpc":"2.0","method":"initialized","params":{}}
<=== {"jsonrpc":"2.0","method":"$/typescriptVersion","params":{"version":"5.5.2","source":"bundled"}}
===> {"jsonrpc":"2.0","id":2,"method":"shutdown"}
<=== {"jsonrpc":"2.0","id":2,"result":null}
===> {"jsonrpc":"2.0","method":"exit"}
child process exited with code 0
我们的客户端与 typescript-language-server
服务器成功完成了一个完整的生命周期。
向服务器发送 textDocument/didOpen
:
function dealMessage(data) {
let response = JSON.parse(data)
console.log("<===", data);
if (response.id === 1) {
send(protocol.getNotification("initialized", {}))
setTimeout(() => {
send(protocol.getRequest("shutdown"))
shutdownId = protocol.getId();
}, 1000);
+ send(protocol.getNotification("textDocument/didOpen", {
+ textDocument: {
+ uri: "file://test.ts",
+ languageId: "typescript",
+ version: 1,
+ text: "/** This is a function */\nfunction test(first: string) {}"
+ }
+ }))
} else if (response.id === shutdownId) {
send(protocol.getNotification("exit"))
}
}
下面我们展示向服务器发送一个 hover
请求并演示如何阅读文档:
-
找到 Hover 请求 的文档。
-
找到其 客户端能力(Client capability) 部分,我们看到属性路径为
textDocument.hover
,属性类型为HoverClientCapabilities
,在初始化请求中加入此能力的支持:
send(protocol.getRequest("initialize", {
processId: process.pid,
rootUri: null,
capabilities: {
+ textDocument: { hover: {} }
}
}))
如果你要开发的是服务端,那么同样找到 服务端能力(Server capability) 部分,我们看到属性路径为 hoverProvider
, 属性类型为 boolean | HoverOptions
,在初始化请求的响应中为服务端添加此能力的支持:
if (message.method === 'initialize') {
process.stdout.write(protocol.getResponse(message.id, {
capabilities: {
+ hoverProvider: true
}
}))
}
- 在 请求(Request) 部分,可以找到,方法的参数
textDocument/hover
和类型HoverParams
,此处我们在发送textDocument/didOpen
通知之后立即发送hover
请求:
function dealMessage(data) {
let response = JSON.parse(data)
console.log("<===", data);
if (response.id === 1) {
send(protocol.getNotification("initialized", {}))
setTimeout(() => {
send(protocol.getRequest("shutdown"))
shutdownId = protocol.getId();
}, 1000);
send(protocol.getNotification("textDocument/didOpen", {
textDocument: {
uri: "file://test.ts",
languageId: "typescript",
version: 1,
text: "/** This is a function */\nfunction test(first: string) {}"
}
}))
+ send(protocol.getRequest("textDocument/hover", {
+ textDocument: { uri: "file://test.ts" },
+ position: { line: 1, character: 10 }
+ }))
} else if (response.id === shutdownId) {
send(protocol.getNotification("exit"))
}
}
同样的,对于开发服务器,在 响应(Response) 部分,定义了返回值的类型 Hover | null
,在这里,我们直接返回固定值:
function dealMessage(data) {
let message = JSON.parse(data)
if (message.method === 'initialize') {
process.stdout.write(protocol.getResponse(message.id, {
capabilities: {
hoverProvider: true
}
}))
+ } else if (message.method === 'textDocument/hover') {
+ process.stdout.write(protocol.getResponse(message.id, {
+ contents: {
+ kind: "plaintext",
+ value: "test"
+ },
+ }))
} else if (message.method === 'shutdown') {
shutdown = true;
process.stdout.write(protocol.getResponse(message.id, null))
} else if (message.method === 'exit') {
if (shutdown) {
process.exit(0)
} else {
process.exit(1)
}
}
}
对于基础类型的定义,可以在 基础 JSON 结构 中找到,例如 TextDocumentPositionParams, MarkupContent。
最后,下面是完整的修改后的客户端代码:
const { spawn } = require('child_process');
const protocol = require('./protocol');
const ls = spawn('node', ['node_modules/typescript-language-server/lib/cli.mjs', '--stdio']);
send(protocol.getRequest("initialize", {
processId: process.pid,
rootUri: null,
capabilities: {
textDocument: { hover: {} }
}
}))
let length = 0
ls.stdout.on('data', (data) => {
data = data.toString()
let arr = data.split("Content-Length: ")
if (arr[0] && arr[0].length === length) {
dealMessage(arr[0])
}
for (let i = 1; i < arr.length; i++) {
let value = arr[i].split("\r\n\r\n")[1]
if (value) {
dealMessage(value)
} else {
length = Number(arr[i].split("\r\n\r\n")[0])
}
}
});
ls.stderr.on("data", (data) => {
console.error(`${data}`);
})
ls.on('close', (code) => {
console.log(`child process exited with code ${code}`);
});
function send(data) {
console.log("===>", data.split("\r\n\r\n")[1]);
ls.stdin.write(data)
}
let shutdownId = 0;
function dealMessage(data) {
let response = JSON.parse(data)
console.log("<===", data);
if (response.id === 1) {
send(protocol.getNotification("initialized", {}))
setTimeout(() => {
send(protocol.getRequest("shutdown"))
shutdownId = protocol.getId();
}, 1000);
send(protocol.getNotification("textDocument/didOpen", {
textDocument: {
uri: "file://test.ts",
languageId: "typescript",
version: 1,
text: "/** This is a function */\nfunction test(first: string) {}"
}
}))
send(protocol.getRequest("textDocument/hover", {
textDocument: { uri: "file://test.ts" },
position: { line: 1, character: 10 }
}))
} else if (response.id === shutdownId) {
send(protocol.getNotification("exit"))
}
}
现在,我们可以运行客户端:
node client.js
输出:
===> {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"processId":56553,"rootUri":null,"capabilities":{"textDocument":{"hover":{}}}}}
<=== {"jsonrpc":"2.0","method":"window/logMessage","params":{"type":3,"message":"Using Typescript version (bundled) 5.5.2 from path \"/Users/renwei/Code/min-lsp-example/node_modules/typescript/lib/tsserver.js\""}}
<=== {"jsonrpc":"2.0","id":1,"result":{"capabilities":{"textDocumentSync":2,"completionProvider":{"triggerCharacters":[".","\"","'","/","@","<"],"resolveProvider":true},"codeActionProvider":true,"codeLensProvider":{"resolveProvider":true},"definitionProvider":true,"documentFormattingProvider":true,"documentRangeFormattingProvider":true,"documentHighlightProvider":true,"documentSymbolProvider":true,"executeCommandProvider":{"commands":["_typescript.applyWorkspaceEdit","_typescript.applyCodeAction","_typescript.applyRefactoring","_typescript.configurePlugin","_typescript.organizeImports","_typescript.applyRenameFile","_typescript.goToSourceDefinition"]},"hoverProvider":true,"inlayHintProvider":true,"linkedEditingRangeProvider":false,"renameProvider":true,"referencesProvider":true,"selectionRangeProvider":true,"signatureHelpProvider":{"triggerCharacters":["(",",","<"],"retriggerCharacters":[")"]},"workspaceSymbolProvider":true,"implementationProvider":true,"typeDefinitionProvider":true,"foldingRangeProvider":true,"semanticTokensProvider":{"documentSelector":null,"legend":{"tokenTypes":["class","enum","interface","namespace","typeParameter","type","parameter","variable","enumMember","property","function","member"],"tokenModifiers":["declaration","static","async","readonly","defaultLibrary","local"]},"full":true,"range":true},"workspace":{"fileOperations":{"willRename":{"filters":[{"scheme":"file","pattern":{"glob":"**/*.{ts,js,jsx,tsx,mjs,mts,cjs,cts}","matches":"file"}},{"scheme":"file","pattern":{"glob":"**","matches":"folder"}}]}}}}}}
===> {"jsonrpc":"2.0","method":"initialized","params":{}}
===> {"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"file://test.ts","languageId":"typescript","version":1,"text":"/** This is a function */\nfunction test(first: string) {}"}}}
===> {"jsonrpc":"2.0","id":2,"method":"textDocument/hover","params":{"textDocument":{"uri":"file://test.ts"},"position":{"line":1,"character":10}}}
<=== {"jsonrpc":"2.0","method":"$/typescriptVersion","params":{"version":"5.5.2","source":"bundled"}}
<=== {"jsonrpc":"2.0","id":2,"result":{"contents":{"kind":"markdown","value":"\n```typescript\nfunction test(first: string): void\n```\nThis is a function"},"range":{"start":{"line":1,"character":9},"end":{"line":1,"character":13}}}}
===> {"jsonrpc":"2.0","id":3,"method":"shutdown"}
<=== {"jsonrpc":"2.0","id":3,"result":null}
===> {"jsonrpc":"2.0","method":"exit"}
child process exited with code 0
我们在第 6 行,找到了 hover
请求, 在第 8 行可以找到它的响应。
现在,我们完成了一个简单的调试,你也可以为 client.js
增加更多的功能来调试你自己的语言服务器。
你也可以使用现存的库来调试: lsp-tester。