有了一定的基础知识之后, 我们就可以开始搭建一个 client 和 server 模式的 lsp 的插件了.
server 目录
首先我们来写 server 端的代码.
package.JSON
首先我们来写 package.JSON. 因为微软的 sdk 已经帮我们封装好了大部分细节, 其实我们只要引用 vscode-languageserver 的模块就可以了:
- {
- "name": "lsp-demo-server",
- "description": "demo language server",
- "version": "1.0.0",
- "author": "Xulun",
- "license": "MIT",
- "engines": {
- "node": "*"
- },
- "repository": {
- "type": "git",
- "url": "git@code.aliyun.com:lusinga/testlsp.git"
- },
- "dependencies": {
- "vscode-languageserver": "^4.1.3"
- },
- "scripts": {}
- }
有了 package.JSON 之后, 我们就可以在 server 目录下运行 NPM install 命令将依赖安装进来.
安装之后会有下面的模块被引用进来:
- vscode-jsonrpc
- vscode-languageserver
- vscode-languageserver-protocol
- vscode-languageserver-types vscode-uri
- tsconfig.JSON
因为我们是要用 typescript 来写 server, 所以我们用 tsconfig.JSON 来配置 Typescript 的选项:
- {
- "compilerOptions": {
- "target": "es6",
- "module": "commonjs",
- "moduleResolution": "node",
- "sourceMap": true,
- "outDir": "out",
- "rootDir": "src",
- "lib": ["es6"]
- },
- "include": ["src"],
- "exclude": ["node_modules", ".vscode-test"]
- }
- server.ts
下面我们开始写服务端的 ts 文件, 首先我们要把 vscode-languageserver 和 vscode-jsonrpc 的依赖引入进来:
- import {
- createConnection,
- TextDocuments,
- TextDocument,
- Diagnostic,
- DiagnosticSeverity,
- ProposedFeatures,
- InitializeParams,
- DidChangeConfigurationNotification,
- CompletionItem,
- CompletionItemKind,
- TextDocumentPositionParams,
- SymbolInformation,
- WorkspaceSymbolParams,
- WorkspaceEdit,
- WorkspaceFolder
- } from 'vscode-languageserver';
- import { HandlerResult } from 'vscode-jsonrpc';
下面, 为了打印日志方便, 我们使用 log4js 来打印日志, 通过 NPM i log4js --save 将其模块引入进来, 然后对其进行初始化:
- import { configure, getLogger } from "log4js";
- configure({
- appenders: {
- lsp_demo: {
- type: "dateFile",
- filename: "/Users/ziyingliuziying/working/lsp_demo",
- pattern: "yyyy-MM-dd-hh.log",
- alwaysIncludePattern: true,
- },
- },
- categories: { default: { appenders: ["lsp_demo"], level: "debug" } }
- });
- const logger = getLogger("lsp_demo");
然后我们就可以调用 createConnection 来创建连接了:
let connection = createConnection(ProposedFeatures.all);
接着我们就可以处理一个个的事件啦, 比如处理第 6 节介绍的初始化事件:
- connection.onInitialize((params: InitializeParams) => {
- let capabilities = params.capabilities;
- return {
- capabilities: {
- completionProvider: {
- resolveProvider: true
- }
- }
- };
- });
三次握手之后, 我们可以在 vscode 上显示一条消息:
- connection.onInitialized(() => {
- connection.Windows.showInformationMessage('Hello World! form server side');
- });
最后, 我们可以把第 5 节学过的代码补全的部分给加上:
- connection.onCompletion(
- (_textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => {
- return [
- {
- label: 'TextView' + _textDocumentPosition.position.character,
- kind: CompletionItemKind.Text,
- data: 1
- },
- {
- label: 'Button' + _textDocumentPosition.position.line,
- kind: CompletionItemKind.Text,
- data: 2
- },
- {
- label: 'ListView',
- kind: CompletionItemKind.Text,
- data: 3
- }
- ];
- }
- );
- connection.onCompletionResolve(
- (item: CompletionItem): CompletionItem => {
- if (item.data === 1) {
- item.detail = 'TextView';
- item.documentation = 'TextView documentation';
- } else if (item.data === 2) {
- item.detail = 'Button';
- item.documentation = 'JavaScript documentation';
- } else if (item.data === 3) {
- item.detail = 'ListView';
- item.documentation = 'ListView documentation';
- }
- return item;
- }
- );
client 目录
服务端这就算开发就绪了, 下面我们来开发客户端.
package.JSON
首先还是先写 package.JSON, 依赖于 vscode-languageclient, 不要跟服务端用的库 vscode-languageserver 搞混了哈.
- {
- "name": "lspdemo-client",
- "description": "demo language server client",
- "author": "Xulun",
- "license": "MIT",
- "version": "0.0.1",
- "publisher": "Xulun",
- "repository": {
- "type": "git",
- "url": "git@code.aliyun.com:lusinga/testlsp.git"
- },
- "engines": {
- "vscode": "^1.33.1"
- },
- "scripts": {
- "update-vscode": "vscode-install",
- "postinstall": "vscode-install"
- },
- "dependencies": {
- "path": "^0.12.7",
- "vscode-languageclient": "^4.1.4"
- },
- "devDependencies": {
- "vscode": "^1.1.30"
- }
- }
- tsconfig.JSON
反正都是 ts, 客户端与服务端比也没有增加啥特别的, 于是照抄一份:
- {
- "compilerOptions": {
- "module": "commonjs",
- "target": "es6",
- "outDir": "out",
- "rootDir": "src",
- "lib": ["es6"],
- "sourceMap": true
- },
- "include": ["src"],
- "exclude": ["node_modules", ".vscode-test"]
- }
- extension.ts
下面我们来写 extension.ts.
其实客户端要做的事情比 server 还少, 本质上就是启动 server 就好:
- // Create the language client and start the client.
- client = new LanguageClient(
- 'DemoLanguageServer',
- 'Demo Language Server',
- serverOptions,
- clientOptions
- );
- // Start the client. This will also launch the server
- client.start();
serverOptions 用来配置服务端的参数, 其定义为:
- export type ServerOptions =
- Executable |
- {
- run: Executable; debug: Executable;
- } |
- {
- run: NodeModule; debug: NodeModule
- } |
- NodeModule |
- (() => Thenable<ChildProcess | StreamInfo | MessageTransports | ChildProcessInfo>);
相关类型的简图如下:
下面我们来配置一下:
- // 服务端配置
- let serverModule = context.asAbsolutePath(
- path.join('server', 'out', 'server.js')
- );
- let serverOptions: ServerOptions = {
- module: serverModule, transport: TransportKind.ipc
- };
- // 客户端配置
- let clientOptions: LanguageClientOptions = {
- // JS 代码触发事情
- documentSelector: [{ scheme: 'file', language: 'js' }],
- };
extension.ts 的完整代码如下:
- import * as path from 'path';
- import { workspace, ExtensionContext } from 'vscode';
- import {
- LanguageClient,
- LanguageClientOptions,
- ServerOptions,
- TransportKind
- } from 'vscode-languageclient';
- let client: LanguageClient;
- export function activate(context: ExtensionContext) {
- // 服务端配置
- let serverModule = context.asAbsolutePath(
- path.join('server', 'out', 'server.js')
- );
- let serverOptions: ServerOptions = {
- module: serverModule, transport: TransportKind.ipc
- };
- // 客户端配置
- let clientOptions: LanguageClientOptions = {
- // JS 代码触发事情
- documentSelector: [{ scheme: 'file', language: 'js' }],
- };
- client = new LanguageClient(
- 'DemoLanguageServer',
- 'Demo Language Server',
- serverOptions,
- clientOptions
- );
- // 启动客户端, 同时启动语言服务器
- client.start();
- }
- export function deactivate(): Thenable<void> | undefined {
- if (!client) {
- return undefined;
- }
- return client.stop();
- }
组装运行
万事俱备, 只欠包装, 下面我们将上面的客户端和服务器组装一下.
插件配置 - package.JSON
我们关注点主要是入口函数和触发事件:
- "activationEvents": [
- "onLanguage:javascript"
- ],
- "main": "./client/out/extension",
完整的 package.JSON 如下:
- {
- "name": "lsp_demo_server",
- "description": "A demo language server",
- "author": "Xulun",
- "license": "MIT",
- "version": "1.0.0",
- "repository": {
- "type": "git",
- "url": "git@code.aliyun.com:lusinga/testlsp.git"
- },
- "publisher": "Xulun",
- "categories": [],
- "keywords": [],
- "engines": {
- "vscode": "^1.33.1"
- },
- "activationEvents": [
- "onLanguage:javascript"
- ],
- "main": "./client/out/extension",
- "contributes": {},
- "scripts": {
- "vscode:prepublish": "cd client && npm run update-vscode && cd .. && npm run compile",
- "compile": "tsc -b",
- "watch": "tsc -b -w",
- "postinstall": "cd client && npm install && cd ../server && npm install && cd ..",
- "test": "sh ./scripts/e2e.sh"
- },
- "devDependencies": {
- "@types/mocha": "^5.2.0",
- "@types/node": "^8.0.0",
- "tslint": "^5.11.0",
- "typescript": "^3.1.3"
- }
- }
装配 tsconfig.JSON
我们还需要一个总的 tsconfig.JSON, 引用 client 和 server 两个目录:
- {
- "compilerOptions": {
- "module": "commonjs",
- "target": "es6",
- "outDir": "out",
- "rootDir": "src",
- "lib": [ "es6" ],
- "sourceMap": true
- },
- "include": [
- "src"
- ],
- "exclude": [
- "node_modules",
- ".vscode-test"
- ],
- "references": [
- { "path": "./client" },
- { "path": "./server" }
- ]
- }
配置 vscode
上面, 我们就将 client, server 和整合它们的代码全部写完了.
下面我们在. vscode 目录中写两个配置文件, 使我们可以更方便地调试和运行.
.vscode/launch.JSON
有了这个文件之后, 我们就有了运行的配置, 可以通过 F5 来启动.
- // A launch configuration that compiles the extension and then opens it inside a new Windows
- {
- "version": "0.2.0",
- "configurations": [
- {
- "type": "extensionHost",
- "request": "launch",
- "name": "Launch Client",
- "runtimeExecutable": "${execPath}",
- "args": ["--extensionDevelopmentPath=${workspaceRoot}"],
- "outFiles": ["${workspaceRoot}/client/out/**/*.js"],
- "preLaunchTask": {
- "type": "npm",
- "script": "watch"
- }
- },
- {
- "type": "node",
- "request": "attach",
- "name": "Attach to Server",
- "port": 6009,
- "restart": true,
- "outFiles": ["${workspaceRoot}/server/out/**/*.js"]
- },
- ],
- "compounds": [
- {
- "name": "Client + Server",
- "configurations": ["Launch Client", "Attach to Server"]
- }
- ]
- }
- .vscode/tasks.JSON
配置 NPM compile 和 NPM watch 两个脚本.
- {
- "version": "2.0.0",
- "tasks": [
- {
- "type": "npm",
- "script": "compile",
- "group": "build",
- "presentation": {
- "panel": "dedicated",
- "reveal": "never"
- },
- "problemMatcher": [
- "$tsc"
- ]
- },
- {
- "type": "npm",
- "script": "watch",
- "isBackground": true,
- "group": {
- "kind": "build",
- "isDefault": true
- },
- "presentation": {
- "panel": "dedicated",
- "reveal": "never"
- },
- "problemMatcher": [
- "$tsc-watch"
- ]
- }
- ]
- }
一切就绪之后, 在插件根目录下运行下 NPM install.
然后在 vscode 中运行 build 命令, 比如 Mac 下是 cmd-shift-b, 于是就构建生成了 server 和 client 的 out 目录下的 JS 和 map.
现在就可以通过 F5 键运行啦.
本示例的源代码放在 code.aliyun.com:lusinga/testlsp.Git 中.
来源: https://yq.aliyun.com/articles/704465