一、项目描述
我们的目标是实现一个基础库的插件,其功能包含以下内容:
- 语法校验,校验API命令调用的正确性,参数类型的正确性,参数个数的正确性,关键参数和系统打通校验有效性等
- 语法自动补全,辅助用户编写代码
- 语法悬停提示,辅助说明语法
- 其他功能
- webview查看远程信息
本篇我们先介绍如何完成整个项目的搭建。完整代码可在我的github中查看。
二、项目架构设计
因为我们涉及到了语法校验,vscode为提高运行效率,涉及到语法校验这类任务,提供了语言服务器来异步支持,目前前述的脚手架并不支持这类项目架构初始化。
但好在官方提供了两个示例:lsp-sample、lsp-multi-server-sample,最终我们的项目架构基本和lsp-sample类似,但参考了vscode-eslint做了一些优化,主要是vscode插件发布对文件个数有限制,我们期望有一个打包的优化,最终的目录如下:
├── CHANGELOG.md // 修改日志
├── README.md // 插件发布后,插件主页内容
├── client // client部分,后续会讲到的如悬浮提示、自动补全等会在这部分实现
| ├── package-lock.json
| ├── package.json
| ├── src
| | ├── config
| | | └── index.ts // 配置,支持的文件
| | ├── extension.ts // 插件入口文件
| | └── provider
| | └── lintClient.ts // 语言服务器客户端实现
| ├── tsconfig.json
| └── webpack.config.js // client webpack编译
├── package-lock.json
├── package.json // 项目公共依赖
├── server // 语言服务器
| ├── package-lock.json
| ├── package.json
| ├── src
| | ├── global.d.ts
| | ├── lint.ts // 校验实现
| | ├── server.ts // 语言服务器实现文件
| | └── utils
| | └── ast.ts // ast解析函数
| ├── tsconfig.json
| └── webpack.config.js // server webpack编译
├── shared.webpack.config.js // 公共配置
├── tsconfig.json
└── vsc-extension-quickstart.md
这里我们详细介绍一下加入了语言服务器会需要的内容:(其他内容可阅读上一篇vscode插件开发指南(一)-理论篇)
1)插件配置,我们可以增加一个配置,用于控制是否展示warning配置,官方说明: contributes.configuration
// 在package.json中增加configuration配置
"contributes": {
"configuration": {
"type": "object",
"title": "vscode-example-tyc",
"properties": {
"vscode-example-tyc.warning": {
"scope": "resource",
"type": "boolean",
"default": false,
"description": "是否开启warning提示"
}
}
}
}
2)插件入口文件
这里为了做到项目优化,我们将client的初始化单独拆到一个文件中
// client/src/provider/lintClient.ts
import * as vscode from 'vscode';
import * as path from 'path';
import {
LanguageClient,
LanguageClientOptions,
ServerOptions,
TransportKind
} from 'vscode-languageclient';
import { file } from '../config';
export default function(context: vscode.ExtensionContext) {
// 服务器由node实现
let serverModule = context.asAbsolutePath(
path.join('server', 'out', 'server.js')
);
// 为服务器提供debug选项
// --inspect=6011: 运行在Node's Inspector mode,这样VS Code就能调试服务器了
// todo: 目前报错,需要调研
let debugOptions = { execArgv: ['--nolazy', '--inspect=6011'] };
// 如果插件运行在调试模式那么就会使用debug server options
// 不然就使用run options
let serverOptions: ServerOptions = {
run: { module: serverModule, transport: TransportKind.ipc },
debug: {
module: serverModule,
transport: TransportKind.ipc,
options: debugOptions
}
};
// 控制语言客户端的选项
let clientOptions: LanguageClientOptions = {
documentSelector: file,
synchronize: {
fileEvents: vscode.workspace.createFileSystemWatcher('**/.clientrc')
}
};
// 创建语言客户端并启动
const client = new LanguageClient(
'vscode-example-tyc',
'vscode-example-tyc',
serverOptions,
clientOptions
);;
// 启动客户端,这也同时启动了服务器
client.start();
return client;
};
插件入口文件:
// client/src/extension.ts
import * as vscode from 'vscode';
import { LanguageClient } from 'vscode-languageclient';
import lintClient from './provider/lintClient';
let client: LanguageClient;
export function activate(context: vscode.ExtensionContext) {
// 启动服务,server端处理lint
client = lintClient(context);
}
export function deactivate(): Thenable<void> | undefined {
if (!client) {
return undefined;
}
return client.stop();
}
3)语言服务器实现
// server/src/server.ts
import {
createConnection,
TextDocuments,
Diagnostic,
ProposedFeatures,
InitializeParams,
DidChangeConfigurationNotification,
TextDocumentSyncKind,
InitializeResult,
_
} from 'vscode-languageserver';
import {
TextDocument
} from 'vscode-languageserver-textdocument';
// 校验
import lint from './lint';
// 创建一个服务器连接。使用Node的IPC作为传输方式。
// 也包含所有的预览、建议等LSP特性
let connection = createConnection(ProposedFeatures.all);
// 创建一个简单的文本管理器。
// 文本管理器只支持全文本同步。
let documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);
let hasConfigurationCapability: boolean = false;
let hasWorkspaceFolderCapability: boolean = false;
let hasDiagnosticRelatedInformationCapability: boolean = false;
connection.onInitialize((params: InitializeParams) => {
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: InitializeResult = {
capabilities: {
textDocumentSync: TextDocumentSyncKind.Incremental,
completionProvider: {
resolveProvider: true
}
}
};
if (hasWorkspaceFolderCapability) {
result.capabilities.workspace = {
workspaceFolders: {
supported: true
}
};
}
return result;
});
connection.onInitialized(() => {
if (hasConfigurationCapability) {
// 为所有配置Register for all configuration changes.
connection.client.register(DidChangeConfigurationNotification.type, undefined);
}
if (hasWorkspaceFolderCapability) {
connection.workspace.onDidChangeWorkspaceFolders(_event => {
connection.console.log('Workspace folder change event received.');
});
}
});
// 配置示例
interface ExampleSettings {
[prop: string]: any;
}
// 当客户端不支持`workspace/configuration`请求时,使用global settings
// 请注意,在这个例子中服务器使用的客户端并不是问题所在,而是这种情况还可能发生在其他客户端身上。
const defaultSettings: ExampleSettings = { warning: true };
let globalSettings: ExampleSettings = defaultSettings;
// 对所有打开的文档配置进行缓存
let documentSettings: Map<string, Thenable<ExampleSettings>> = new Map();
connection.onDidChangeConfiguration(change => {
if (hasConfigurationCapability) {
// 重置所有已缓存的文档配置
documentSettings.clear();
} else {
globalSettings = <ExampleSettings>(
(change.settings['vscode-example-tyc'] || defaultSettings)
);
}
// 重新验证所有打开的文本文档
documents.all().forEach(validateTextDocument);
});
// 获取配置
function getDocumentSettings(resource: string): Thenable<ExampleSettings> {
if (!hasConfigurationCapability) {
return Promise.resolve(globalSettings);
}
let result = documentSettings.get(resource);
if (!result) {
result = connection.workspace.getConfiguration({
scopeUri: resource,
section: 'vscode-example-tyc'
});
documentSettings.set(resource, result);
}
return result;
}
// 只保留打开文档的设置
documents.onDidClose(e => {
documentSettings.delete(e.document.uri);
});
// 文档变更时触发(第一次打开或内容变更)
documents.onDidChangeContent(change => {
validateTextDocument(change.document);
});
// lint文档函数
async function validateTextDocument(textDocument: TextDocument): Promise<void> {
let diagnostics: Diagnostic[] = [];
// 获取当前文档设置
let settings = await getDocumentSettings(textDocument.uri);
// 校验
diagnostics.push(...lint(textDocument, hasDiagnosticRelatedInformationCapability, settings));
// 发送诊断结果
connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
}
// 监听文档变化
connection.onDidChangeWatchedFiles(_change => {
connection.console.log('We received an file change event');
});
// 让文档管理器监听文档的打开,变动和关闭事件
documents.listen(connection);
// 连接后启动监听
connection.listen();
4)activationEvents
我们在这里重点再讲一下activationEvents:插件触发动作,可配置项包括以下几类
- onLanguage:打开特定语言文件时激活事件和相关插件
- onCommand:调用命令时激活
- onDebug:调试会话(debug session)启动前激活
- onDebugInitialConfigurations
- onDebugResolve
- workspaceContains:文件夹打开后,且文件夹中至少包含一个符合glob模式的文件时触发
- onFileSystem: 以协议(scheme)打开文件或文件夹时触发。通常是file-协议,也可以用自定义的文件供应器函数替换掉,比如ftp、ssh.h
- onView:每当在VS Code侧栏中展开指定ID的视图时
- onUri:插件的系统级URI打开时触发。这个URI协议需要带上vscode或者 vscode-insiders协议。URI主机名必须是插件的唯一标识,剩余的URI是可选的
- onWebviewPanel: VS Code需要恢复匹配到viewType的webview视图时触发
- onCustomEditor:当VS Code需要使用匹配的viewType创建自定义编辑器时触发
- *:当VS Code启动时触发,性能会变差,不建议使用
- onStartupFinished: VS Code启动一定时间后触发,和上述*效果类似,但性能更好一些
这里我们选择了onStartupFinished,如果你的触发场景更少,比如仅仅是打开js文件时,建议使用更加具体的配置
5)语言服务器日志
可在调试窗口的output中查看