vscode插件开发指南(二)-实战篇-搭建项目

2,826 阅读5分钟

一、项目描述

我们的目标是实现一个基础库的插件,其功能包含以下内容:

  • 语法校验,校验API命令调用的正确性,参数类型的正确性,参数个数的正确性,关键参数和系统打通校验有效性等
  • 语法自动补全,辅助用户编写代码
  • 语法悬停提示,辅助说明语法
  • 其他功能
    • webview查看远程信息

本篇我们先介绍如何完成整个项目的搭建。完整代码可在我的github中查看。

二、项目架构设计

因为我们涉及到了语法校验,vscode为提高运行效率,涉及到语法校验这类任务,提供了语言服务器来异步支持,目前前述的脚手架并不支持这类项目架构初始化。

但好在官方提供了两个示例:lsp-samplelsp-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中查看 调试

三、系列文章