Other

[IDE] typescript-language-server

2019-07-23  本文已影响15人  何幻

背景

基于 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

然后监听 readlineInterfaceline 事件,
(类似于 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.tsinitialize 函数,this.tspClient.request第125行)打个断点,

重新启动调试,
(打开 file-lsp-server.spec.tsF5

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 仓库,并 resetv3.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种情况,


参考

LSP
Language Servers
theia-ide/typescript-language-server v0.3.8
typescript v3.5.3

上一篇 下一篇

猜你喜欢

热点阅读