[IDE] typescript-language-server
背景
基于 LSP 的 TypeScript Language Servers,有两个开源实现,
其中,theia-ide 支持更多的 ServerCapabilities。
ServerCapabilities | theia-ide | sourcegraph |
---|---|---|
textDocumentSync | ✅ | ✅ |
hoverProvider | ✅ | ✅ |
completionProvider | ✅ | ✅ |
signatureHelpProvider | ✅ | ✅ |
definitionProvider | ✅ | ✅ |
referencesProvider | ✅ | ✅ |
documentHighlightProvider | ✅ | |
documentSymbolProvider | ✅ | ✅ |
workspaceSymbolProvider | ✅ | ✅ |
codeActionProvider | ✅ | ✅ |
codeLensProvider | ||
documentFormattingProvider | ✅ | |
documentRangeFormattingProvider | ||
documentOnTypeFormattingProvider | ||
renameProvider | ✅ | ✅ |
documentLinkProvider | ✅ | |
executeCommandProvider | ✅ | ✅ |
experimental | ||
implementationProvider | ✅ | |
typeDefinitionProvider | ✅ | ✅ |
workspace.workspaceFolders | ||
colorProvider | ||
foldingRangeProvider | ✅ | |
declarationProvider | ||
selectionRangeProvider |
下文我们简单阅读以下 theia-ide/typescript-language-server 的源码,
看看以上这些 ServerCapabilities 是怎样实现的。
1. language-server
(1)克隆源码 & 安装依赖
$ git clone https://github.com/theia-ide/typescript-language-server.git
$ cd typescript-language-server
$ yarn
(2)启动调试
修改单测 server/src/file-lsp-server.spec.ts,添加 .only
只跑这一条单测,
然后在 createServer
(第19行)打个断点,

按 F5
开始调试,
(注意,要在 file-lsp-server.spec.ts 文件中按 F5
)

(3)lspServer 初始化
Step Into 单步调试,进入 createServer
函数中,

在 server.initialize
(第72行)打个断点,按 F5
运行到断点,

Step Into 进入 initialize
函数中,
(如果无法进入这个函数,就在函数中打断点,重新启动调试进入)

(4)启动 tspClient 与 tsserver 通信
initialize
函数中,会先 new TspClient
,然后执行 tspClient.start()
,
TspClient
构造函数只初始化了 logger,

主要逻辑在 start
中,

在 cp.spawn
位置(第106行)打个断点,按 F5
运行到断点位置,



可以看到这里,会衍生出一个子进程,相当于执行以下 shell 命令,
$ tsserver --cancellationPipeName /private/var/folders/df/57vsznhs05qb8h15mw5qxg3r0000gn/T/6145afdd56d84e3d8a83d87bca78382a/tscancellation*
它会启动一个 tsserver
,这个 tsserver
是全局安装 typescript
时添加的,
( tsserver
相关的可详见本文第二节)
$ which tsserver
/Users/thzt/.nvm/versions/node/v10.15.0/bin/tsserver
启动 tsserver
后,使用 node 内置的 readline 模块 createInterface
,

然后监听 readlineInterface
的 line
事件,
(类似于 child.stdout.on('data', ...)
)
this.readlineInterface.on('line', line => this.processMessage(line));
(5)通信示例
先结束本次调试,把所有的断点都删掉,
(Debug - Remove All Breakpoints)

然后,把 line
事件监听代码改成两行,在 第116行 打个断点,
this.readlineInterface.on('line', line => {
return this.processMessage(line); // <- 在这一行打断点
});

然后打开 lsp-server.ts 在 initialize
函数,this.tspClient.request
(第125行)打个断点,

重新启动调试,
(打开 file-lsp-server.spec.ts 按 F5
)

tspClient
会发送一个 configure
命令给 tsserver
,按 F5
运行到下一个断点,

读到了子进程 tsserver
stdout 的第一行,
Content-Length: 76
再按 F5
,读到了第二行,是个空行,

再按 F5
,独到了最后一行,
{"seq":0,"type":"event","event":"typingsInstallerPid","body":{"pid":81542}}

这就是一个完整的通信过程了。
2. tsserver
The TypeScript standalone server (aka
tsserver
) is a node executable that encapsulates the TypeScript compiler and language services, and exposes them through a JSON protocol.tsserver
is well suited for editors and IDE support.
(1)安装 & 运行
安装 tsserver
,
$ tnpm init -f
$ tnpm i -S typescript
会在 node_modules/.bin 目录安装两个可执行文件,
./node_modules/.bin/tsc
./node_modules/.bin/tsserver
启动 tsserver
,(二选一)
$ node node_modules/.bin/tsserver
$ npx tsserver
(2)父子进程通过 stdio 通信
tsserver
listens on stdin and writes messages back to stdout.
index.js
const path = require('path');
const cp = require('child_process');
const child = cp.spawn('npx', ['tsserver']);
child.stdout.on('data', data => {
console.log('child.stdout.on:data');
console.log(data.toString());
});
child.on('close', code => {
console.log('child.stdout.on:close');
console.log(code);
});
child.stdin.write(JSON.stringify({
seq: 1,
type: 'quickinfo',
command: 'open',
arguments: {
file: path.resolve('./index.js'),
}
}));
$ node index.js
child.stdout.on:data
Content-Length: 76
{"seq":0,"type":"event","event":"typingsInstallerPid","body":{"pid":80170}}
3. typescript
(1)克隆 & build
克隆 typescript 仓库,并 reset
到 v3.5.3 时的代码,
$ git clone https://github.com/microsoft/TypeScript.git
$ cd TypeScript
$ git reset --hard aff7ef12305de00cca7ac405fdaf8402ba0e6973 # v3.5.3 的 commit id
安装依赖,
$ tnpm i -g gulp
$ tnpm i
构建到 built/local/,
$ gulp local
注:阅读源码之前,要先 gulp local
,不然有些定义无法直接跳转过去。
其他命令,
gulp local # Build the compiler into built/local
gulp clean # Delete the built compiler
gulp LKG # Replace the last known good with the built one.
# Bootstrapping step to be executed when the built compiler reaches a stable state.
gulp tests # Build the test infrastructure using the built compiler.
gulp runtests # Run tests using the built compiler and test infrastructure.
# You can override the host or specify a test for this command.
# Use --host=<hostName> or --tests=<testPath>.
gulp baseline-accept # This replaces the baseline test results with the results obtained from gulp runtests.
gulp lint # Runs tslint on the TypeScript source.
gulp help # List the above commands.
(2)listener
src/tsserver/server.ts#964
tsserver
启动后开始 listen
,
ioSession.listen();
listen
位于 src/tsserver/server.ts#562
接到 stdin 之后,会调用 this.onMessage
,
listen() {
rl.on("line", (input: string) => {
...
this.onMessage(message);
});
...
}
onMessage
的实现位于 src/server/session.ts#2489
public onMessage(message: string) {
...
try {
...
const { response, responseRequired } = this.executeCommand(request);
...
}
catch (err) {
...
}
}
其中,#2504 行,调用了 this.executeCommand
。
executeCommand
的实现在 src/server/session.ts#2477,
public executeCommand(request: protocol.Request): HandlerResponse {
const handler = this.handlers.get(request.command);
if (handler) {
return this.executeWithRequestId(request.seq, () => handler(request));
}
else {
...
}
}
这里根据不同的 request.command
,获取对应的 handler
。
handlers
是由一个超级长(348行)的函数返回的,src/server/session.ts#2099-2446
private handlers = createMapFromTemplate<(request: protocol.Request) => HandlerResponse>({
[CommandNames.Status]: () => { ... },
[CommandNames.OpenExternalProject]: (request: protocol.OpenExternalProjectRequest) => { ... },
[CommandNames.OpenExternalProjects]: (request: protocol.OpenExternalProjectsRequest) => { ... },
...
});
总共包含了 91 个 CommandNames
的情况。
(3)handler
下面我们来看 CommandNames.Implementation
这个 handler
,
src/server/session.ts#2195
[CommandNames.Implementation]: (request: protocol.Request) => {
return this.requiredResponse(this.getImplementation(request.arguments, /*simplifiedResult*/ true));
},
主要逻辑在 this.getImplementation
,
src/server/session.ts#1063
private getImplementation(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): ReadonlyArray<protocol.FileSpan> | ReadonlyArray<ImplementationLocation> {
...
const implementations = this.mapImplementationLocations(project.getLanguageService().getImplementationAtPosition(file, position) || emptyArray, project);
...
}
我们看到这里,调用了 project.getLanguageService()
返回了一个 languageServer,
然后调用了这个 languageServer 的 getImplementationAtPosition
方法。
(4)services
经过一番查找,看到实现在 src/services/services.ts#1540,
注意我们从 server
文件夹来到了 services
文件夹中,
function getImplementationAtPosition(fileName: string, position: number): ImplementationLocation[] | undefined {
synchronizeHostData();
return FindAllReferences.getImplementationsAtPosition(program, cancellationToken, program.getSourceFiles(), getValidSourceFile(fileName), position);
}
后又调用了 FindAllReferences.getImplementationsAtPosition
,
位于 src/services/findAllReferences.ts#62,
export function getImplementationsAtPosition(program: Program, cancellationToken: CancellationToken, sourceFiles: ReadonlyArray<SourceFile>, sourceFile: SourceFile, position: number): ImplementationLocation[] | undefined {
const node = getTouchingPropertyName(sourceFile, position);
const referenceEntries = getImplementationReferenceEntries(program, cancellationToken, sourceFiles, node, position);
const checker = program.getTypeChecker();
return map(referenceEntries, entry => toImplementationLocation(entry, checker));
}
getTouchingPropertyName
是得到当前位置的 ast node,
Gets the token whose text has range [start, end) and position >= start and (position < end or (position === end && token is literal or keyword or identifier))
getImplementationReferenceEntries
是计算该标识符的所有实现位置,
function getImplementationReferenceEntries(program: Program, cancellationToken: CancellationToken, sourceFiles: ReadonlyArray<SourceFile>, node: Node, position: number): ReadonlyArray<Entry> | undefined {
if (node.kind === SyntaxKind.SourceFile) {
return undefined;
}
const checker = program.getTypeChecker();
// If invoked directly on a shorthand property assignment, then return
// the declaration of the symbol being assigned (not the symbol being assigned to).
if (node.parent.kind === SyntaxKind.ShorthandPropertyAssignment) {
const result: NodeEntry[] = [];
Core.getReferenceEntriesForShorthandPropertyAssignment(node, checker, node => result.push(nodeEntry(node)));
return result;
}
else if (node.kind === SyntaxKind.SuperKeyword || isSuperProperty(node.parent)) {
// References to and accesses on the super keyword only have one possible implementation, so no
// need to "Find all References"
const symbol = checker.getSymbolAtLocation(node)!;
return symbol.valueDeclaration && [nodeEntry(symbol.valueDeclaration)];
}
else {
// Perform "Find all References" and retrieve only those that are implementations
return getReferenceEntriesForNode(position, node, program, sourceFiles, cancellationToken, { implementations: true });
}
}
分了3种情况,
-
ShorthandPropertyAssignment
:属性名简写,例如{ a, b }
-
SuperKeyword
:super 关键字 - 其他:找到所有的引用,并过滤找出实现
参考
LSP
Language Servers
theia-ide/typescript-language-server v0.3.8
typescript v3.5.3