语言服务器分为客户端和服务端,vscode插件启动的时候启动的是客户端部分,客户端代码里面会唤起服务端,然后进行通讯。
语言服务器可以用任何语言实现,并在自己的进程中运行,以避免性能成本,因为它们通过语言服务器协议(Language Server Protocol )与代码编辑器通信。
例如,为了正确验证一个文件,语言服务器需要解析大量的文件,为它建立抽象语法树,并执行静态代码分析。这些操作可能会占用大量的CPU和内存,我们需要确保VSCode代码的性能不受影响。所以就有了语言服务器的诞生。
语言服务器通过语言服务器与客户端通讯,语言服务器协议是适应具有不同api的代码编辑器。你开发一个插件可以在不同编辑器中使用。
开发插件我们需要进行如下操作;
├── client // 语言客户端
│ ├── package.json
│ ├── src
│ │ ├── test // 单元测试文件
│ │ └── extension.js // 语言客户端的入口文件
├── package.json // 插件的描述文件
└── server // 语言服务端
└── package.json
└── src
└── server.js // 语言服务端的入口文件
了解两个包:
vscode-languageclient:npm模块,用于从VSCode客户端与VSCode语言服务器通信:
vscode-languageserver:npm模块,用于使用Node.js作为运行时来实现VSCode语言服务器:
实现纯文本的代码校验工作
创建客户端的代码 src/extension.js
const path = require("path");
const vscode_1 = require("vscode");
const vscode_languageclient_1 = require("vscode-languageclient");
let client;
function activate(context) {
// node 服务器路径
let serverModule = context.asAbsolutePath(path.join('server', 'out', 'server.js'));
// --inspect=6009: 开启调试模式
let debugOptions = { execArgv: ['--nolazy', '--inspect=6009'] };
// 如果插件在调试模式下启动,则使用调试服务器选项,否则将使用运行选项
let serverOptions = {
// 运行时的参数
run: { module: serverModule, transport: vscode_languageclient_1.TransportKind.ipc },
// 调试时的参数
debug: {
module: serverModule,
transport: vscode_languageclient_1.TransportKind.ipc,
options: debugOptions
}
};
// 语言客户端的一些参数
let clientOptions = {
// 为纯文本文档注册服务器
documentSelector: [{ scheme: 'file', language: 'plaintext' }],
synchronize: {
// 在“.clientrc文件”的文件更改通知服务器,如果不想校验这个代码可以在这里配置
fileEvents: vscode_1.workspace.createFileSystemWatcher('**/.clientrc')
}
};
// 创建客户端
client = new vscode_languageclient_1.LanguageClient('languageServerExample', 'Language Server Example', serverOptions, clientOptions);
// 启动
client.start();
}
exports.activate = activate;
function deactivate() {
if (!client) {
return undefined;
}
return client.stop();
}
exports.deactivate = deactivate;
接下来来的服务端的代码src/server.js
const vscode_languageserver_1 = require("vscode-languageserver");
const vscode_languageserver_textdocument_1 = require("vscode-languageserver-textdocument");
// 为服务创建连接,通过IPC管道与客户端通讯
let connection = vscode_languageserver_1.createConnection(vscode_languageserver_1.ProposedFeatures.all);
// npm模块,用于实现使用Node.js作为运行时的LSP服务器中可用的文本文档:
let documents = new vscode_languageserver_1.TextDocuments(vscode_languageserver_textdocument_1.TextDocument);
let hasConfigurationCapability = false;
let hasWorkspaceFolderCapability = false;
let hasDiagnosticRelatedInformationCapability = false;
connection.onInitialize((params) => {
let capabilities = params.capabilities;
// 客户端支持 `workspace/configuration` 配置?
// 如果没有支持全局配置
hasConfigurationCapability = !!(capabilities.workspace && !!capabilities.workspace.configuration);
hasWorkspaceFolderCapability = !!(capabilities.workspace && !!capabilities.workspace.workspaceFolders);
hasDiagnosticRelatedInformationCapability = !!(capabilities.textDocument &&
capabilities.textDocument.publishDiagnostics &&
capabilities.textDocument.publishDiagnostics.relatedInformation);
const result = {
capabilities: {
textDocumentSync: vscode_languageserver_1.TextDocumentSyncKind.Incremental,
// 告诉客户端支持代码补全
completionProvider: {
resolveProvider: true
}
}
};
if (hasWorkspaceFolderCapability) {
result.capabilities.workspace = {
workspaceFolders: {
supported: true
}
};
}
return result;
});
connection.onInitialized(() => {
if (hasConfigurationCapability) {
// 注册所有配置更改。
connection.client.register(vscode_languageserver_1.DidChangeConfigurationNotification.type, undefined);
}
if (hasWorkspaceFolderCapability) {
connection.workspace.onDidChangeWorkspaceFolders(_event => {
connection.console.log('Workspace folder change event received.');
});
}
});
// The global settings, used when the `workspace/configuration` request is not supported by the client.
// Please note that this is not the case when using this server with the client provided in this example
// but could happen with other clients.
const defaultSettings = { maxNumberOfProblems: 1000 };
let globalSettings = defaultSettings;
// Cache the settings of all open documents
let documentSettings = new Map();
connection.onDidChangeConfiguration(change => {
if (hasConfigurationCapability) {
// Reset all cached document settings
documentSettings.clear();
}
else {
globalSettings = ((change.settings.languageServerExample || defaultSettings));
}
// Revalidate all open text documents
documents.all().forEach(validateTextDocument);
});
function getDocumentSettings(resource) {
if (!hasConfigurationCapability) {
return Promise.resolve(globalSettings);
}
let result = documentSettings.get(resource);
if (!result) {
result = connection.workspace.getConfiguration({
scopeUri: resource,
section: 'languageServerExample'
});
documentSettings.set(resource, result);
}
return result;
}
// Only keep settings for open documents
documents.onDidClose(e => {
documentSettings.delete(e.document.uri);
});
// The content of a text document has changed. This event is emitted
// when the text document first opened or when its content has changed.
documents.onDidChangeContent(change => {
validateTextDocument(change.document);
});
async function validateTextDocument(textDocument) {
// In this simple example we get the settings for every validate run.
let settings = await getDocumentSettings(textDocument.uri);
// The validator creates diagnostics for all uppercase words length 2 and more
let text = textDocument.getText();
let pattern = /\b[A-Z]{2,}\b/g;
let m;
let problems = 0;
let diagnostics = [];
while ((m = pattern.exec(text)) && problems < settings.maxNumberOfProblems) {
problems++;
let diagnostic = {
severity: vscode_languageserver_1.DiagnosticSeverity.Warning,
range: {
start: textDocument.positionAt(m.index),
end: textDocument.positionAt(m.index + m[0].length)
},
message: `${m[0]} is all uppercase.`,
source: 'ex'
};
if (hasDiagnosticRelatedInformationCapability) {
diagnostic.relatedInformation = [
{
location: {
uri: textDocument.uri,
range: Object.assign({}, diagnostic.range)
},
message: 'Spelling matters'
},
{
location: {
uri: textDocument.uri,
range: Object.assign({}, diagnostic.range)
},
message: 'Particularly for names'
}
];
}
diagnostics.push(diagnostic);
}
// Send the computed diagnostics to VSCode.
connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
}
connection.onDidChangeWatchedFiles(_change => {
// Monitored files have change in VSCode
connection.console.log('We received an file change event');
});
// This handler provides the initial list of the completion items.
connection.onCompletion((_textDocumentPosition) => {
// The pass parameter contains the position of the text document in
// which code complete got requested. For the example we ignore this
// info and always provide the same completion items.
return [
{
label: 'TypeScript',
kind: vscode_languageserver_1.CompletionItemKind.Text,
data: 1
},
{
label: 'JavaScript',
kind: vscode_languageserver_1.CompletionItemKind.Text,
data: 2
}
];
});
// This handler resolves additional information for the item selected in
// the completion list.
connection.onCompletionResolve((item) => {
if (item.data === 1) {
item.detail = 'TypeScript details';
item.documentation = 'TypeScript documentation';
}
else if (item.data === 2) {
item.detail = 'JavaScript details';
item.documentation = 'JavaScript documentation';
}
return item;
});
// Make the text document manager listen on the connection
// for open, change and close text document events
documents.listen(connection);
// Listen on the connection
connection.listen();
然后配置几个命令 .vscode/lunck.json
// 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/src/**/*.js"],
"preLaunchTask": {
"type": "npm",
"script": "watch"
}
},
{
"type": "node",
"request": "attach",
"name": "Attach to Server",
"port": 6009,
"restart": true,
"outFiles": ["${workspaceRoot}/server/src/**/*.js"]
}
],
"compounds": [
{
"name": "Client + Server",
"configurations": ["Launch Client", "Attach to Server"]
}
]
}
demo地址