VSCode插件开发实战:从零构建完整LSP工程
经过前面基础知识的学习积累,现在正式开始搭建一个功能完整的LSP插件,采用经典的客户端与服务端分离架构。整个实现过程分为两大模块:首先完成服务端的核心逻辑,其次实现客户端的启动与通信,最后将两者无缝集成,形成可用插件。
服务端目录结构
首先聚焦服务端代码的构建。从package.json入手,微软提供的SDK已将大量底层细节封装完毕,我们仅需引入vscode-languageserver模块即可快速开发语言服务器。
package.json配置
{
"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,这些都是协议通信与类型定义的核心组件。
TypeScript编译配置
既然采用TypeScript编写服务端,就必须配置tsconfig.json,编译选项如下:
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"outDir": "out",
"rootDir": "src",
"lib": ["es6"]
},
"include": ["src"],
"exclude": ["node_modules", ".vscode-test"]
}
服务端核心逻辑 server.ts
接下来编写服务端的主入口——server.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
}
}
};
});
初始化阶段的三次握手完成后,可以在VS Code界面中弹出欢迎消息:
connection.onInitialized(() => {
connection.window.showInformationMessage('Hello World! form server side');
});
最后,将第5节中实现的代码补全功能集成进来:
connection.onCompletion((_textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => {
return [
{
label: 'TextView',
kind: CompletionItemKind.Text,
data: 1
},
{
label: 'Button',
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 = 'Ja vaScript documentation';
} else if (item.data === 3) {
item.detail = 'ListView';
item.documentation = 'ListView documentation';
}
return item;
});
客户端目录结构
服务端核心代码完成后,接下来开发客户端的启动与通信部分。
客户端 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
客户端依然使用TypeScript,编译配置与服务端基本相同,直接复用:
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"outDir": "out",
"rootDir": "src",
"lib": ["es6"],
"sourceMap": true
},
"include": ["src"],
"exclude": ["node_modules", ".vscode-test"]
}
客户端主入口 extension.ts
接下来编写extension.ts,客户端的主要职责是启动语言服务器,本身逻辑相对精简:
// 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);
相关类型的关系可参考下图:

具体配置如下:
// 服务端配置
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 | undefined {
if (!client) {
return undefined;
}
return client.stop();
}
整体组装与运行
服务端与客户端的代码均已完成,现在将它们整合在一起。核心步骤包括配置插件的package.json、总的tsconfig.json以及VS Code调试环境。
插件根目录 package.json
重点设置入口函数与激活事件:
"activationEvents": ["onLanguage:ja vascript"],
"main": "./client/out/extension",
完整配置如下:
{
"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:ja vascript"],
"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,通过references同时引用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" }
]
}
VS Code调试环境配置
至此,client、server以及用于整合的代码全部编写完成。在.vscode目录下创建两个配置文件,便于调试与运行。
.vscode/launch.json
配置完成后可按F5启动调试。
// A launch configuration that compiles the extension and then opens it inside a new window
{
"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,然后在VS Code中运行构建命令(mac下为cmd+shift+b),即可自动编译生成server与client的out目录下的js及map文件。此时按F5即可启动扩展调试窗口,体验完整的LSP插件功能。
本示例的完整源码已托管至 git@code.aliyun.com:lusinga/testlsp.git,读者可按需克隆参考。
