检测edm语法的vscode插件开发 您所在的位置:网站首页 vscode配置html要哪些插件 检测edm语法的vscode插件开发

检测edm语法的vscode插件开发

2023-03-30 07:23| 来源: 网络整理| 查看: 265

前言

我们要写一个edm的语法插件,就需要用到vscode编程语言扩展。 先看一下vscode的编程语言扩展有哪些:

image.png vscode是支持错误检查的,我们要写一个edm语法插件就需要用到代码扫描的诊断信息,这个诊断信息是以vscode.Diagnostic为载体呈现的。

诊断信息

下图是vscode.Diagnostic类的成员和与相关类的关系:

image.png 以小到大,这些类为:

Position: 定位到一行上的一个字符的坐标 Range: 由起点和终点两个Position决定 Location: 一个Range配上一个URI DiagnosticRelatedInformation: 一个Location配一个message Diagnostic: 主体是一个message字符串,一个Range和一个DiagnosticRelatedInformation.

URL是Uniform Resource Locator的缩写,译为"统一资源定位符"。URL是一种URI,它标识一个互联网资源,并指定对其进行操作或获取该资源的方法。 最大的缺点是当信息资源的存放地点发生变化时,必须对URL作相应的改变。因此人们正在研究新的信息资源表示方法,例如:URI(Universal Resource Identifier)即"通用资源标识" 、URN(Uniform Resource Name)即"统一资源名"和URC(Uniform Resource Citation)即"统一资源引用符"等。 URI还在进一步的研究当中。研究的方向就是弥补URL的缺点。

构造一个诊断信息

以下图的html代码为例,保存为test.html

DOCUMENT

在这个例子中使用了style标签,在edm中不支持。 出现问题的是第8行的第5字符到第19字符,所以我们构造(7,4)到(7,18)这样两个Position为首尾的Range。 有了Range,加上问题描述字符串,和问题严重程序三项,就可以构造出一个Diagnostic。

let diagnostic1:vscode.Diagnostic = new vscode.Diagnostic( new vscode.Range( new vscode.Position(7,4), new vscode.Position(7,18) ), 'edm不支持style标签', vscode.DiagnosticSeverity.Warning )

updateDiags完整的代码

export function updateDiags( document: vscode.TextDocument, collection: vscode.DiagnosticCollection ): void { let diagnostics: vscode.Diagnostic = new vscode.Diagnostic( new vscode.Range(new vscode.Position(7, 4), new vscode.Position(7, 18)), 'edm不支持style标签', vscode.DiagnosticSeverity.Warning ); diagnostics.source = 'edm Helper'; diagnostics.relatedInformation = [ new vscode.DiagnosticRelatedInformation( new vscode.Location( document.uri, new vscode.Range(new vscode.Position(7, 0), new vscode.Position(7, 18)) ), 'edm grammar check' ), ]; diagnostics.code = 102; if (document && path.basename(document.uri.fsPath) === 'test.html') { collection.set(document.uri, [diagnostics]); } else { collection.clear(); } }

然后在active函数里调用刚刚写的方法

export function activate(context: ExtensionContext) { const diag_coll = vscode.languages.createDiagnosticCollection('basic-lint-1'); if (vscode.window.activeTextEditor) { updateDiags(vscode.window.activeTextEditor.document, diag_coll); } context.subscriptions.push( vscode.window.onDidChangeActiveTextEditor((e: vscode.TextEditor | undefined) => { if (e !== undefined) { updateDiags(e.document, diag_coll); } }) ); context.subscriptions.push( workspace.onDidChangeTextDocument((e: vscode.TextDocumentChangeEvent | undefined) => { if (e !== undefined) { updateDiags(e.document, diag_coll); } }) ); }

按F5运行一下,就可以看到检测结果啦

image.png

诊断结果vscode.DiagnosticSeverity的值以下四个,分别是下面的效果:

image.png image.png image.png image.png 语言服务器协议LSP

除了Diagnostic,我们还需要用到语言服务器协议LSP(language sever protocol)。 首先 language server是一种跨编辑器的语言支持实现规范。它由微软提出,目前 vscode 、vim、atom 都已经支持了这个规范。 LSP(language sever protocol)是用来处理语言解析等等东西在各式ide里应用的玩意。ide主要干的活还是要提供各类语言的解析跳转高亮等等的东西,所以lsp就显得很重要。放两张图就能大概理解LSP是具体干什么的,为什么需要LSP。

image.png LSP主要解决了几个问题: 1、语言插件的复用。举个例子:Eclipse里C++相关的支持是用java写的,原因很简单:eclipse本身是java写的。但是这样如果要在vscode里面写C++那就又得拿js写一遍,相当于重复造了轮子。 2、进程独立。语言解析这件事本身是很重的,有时候会需要花非常长的时间来完成,要是这时候整个vscode都卡住那就别玩了。所以干脆把这块东西单独抽出来放在服务器上。 LSP现在支持的功能大概如下: image.png

所以实际上在涉及各种语言解析的时候,插件需要起一个server来处理,文件夹大体上就会长这样

├── client // 语言客户端 │ ├── package.json │ ├── src │ │ ├── test // 单元测试文件 │ │ └── extension.js // 语言客户端的入口文件 ├── package.json // 插件的描述文件 └── server // 语言服务端 └── package.json └── src └── server.js // 语言服务端的入口文件 LSP生命周期

服务器的生命周期从客服端向服务端发送一个initialize请求开始,参数是一个InitializeParameter对象

interface InitializeParams { /** * The process Id of the parent process that started * the server. Is null if the process has not been started by another process. * If the parent process is not alive then the server should exit (see exit notification) its process. */ processId: number | null; /** * The rootPath of the workspace. Is null * if no folder is open. * * @deprecated in favour of rootUri. */ rootPath?: string | null; /** * The rootUri of the workspace. Is null if no * folder is open. If both `rootPath` and `rootUri` are set * `rootUri` wins. */ rootUri: DocumentUri | null; /** * User provided initialization options. */ initializationOptions?: any; /** * The capabilities provided by the client (editor or tool) */ capabilities: ClientCapabilities; /** * The initial trace setting. If omitted trace is disabled ('off'). */ trace?: 'off' | 'messages' | 'verbose'; /** * The workspace folders configured in the client when the server starts. * This property is only available if the client supports workspace folders. * It can be `null` if the client supports workspace folders but none are * configured. * * Since 3.6.0 */ workspaceFolders?: WorkspaceFolder[] | null; }

而服务器返回的是服务器的能力

/** * The result returned from an initialize request. */ export interface InitializeResult { /** * The capabilities the language server provides. */ capabilities: ServerCapabilities; /** * Information about the server. * * @since 3.15.0 */ serverInfo?: { /** * The name of the server as defined by the server. */ name: string; /** * The server's version as defined by the server. */ version?: string; }; /** * Custom initialization results. */ [custom: string]: any; }

我们这里要用到的就是textDocumentSync

export interface _ServerCapabilities { /** * Defines how text documents are synced. Is either a detailed structure defining each notification or * for backwards compatibility the TextDocumentSyncKind number. */ textDocumentSync?: TextDocumentSyncOptions | TextDocumentSyncKind; ... }

textDocumentSync的取值可以直接写具体的能力,也可以设置类型

export interface TextDocumentSyncOptions { /** * Open and close notifications are sent to the server. If omitted open close notification should not * be sent. */ openClose?: boolean; /** * Change notifications are sent to the server. See TextDocumentSyncKind.None, TextDocumentSyncKind.Full * and TextDocumentSyncKind.Incremental. If omitted it defaults to TextDocumentSyncKind.None. */ change?: TextDocumentSyncKind; /** * If present will save notifications are sent to the server. If omitted the notification should not be * sent. */ willSave?: boolean; /** * If present will save wait until requests are sent to the server. If omitted the request should not be * sent. */ willSaveWaitUntil?: boolean; ...... } /** * Defines how the host (editor) should sync * document changes to the language server. */ export declare namespace TextDocumentSyncKind { /** * Documents should not be synced at all. */ const None = 0; /** * Documents are synced by always sending the full content * of the document. */ const Full = 1; /** * Documents are synced by sending the full content on open. * After that only incremental updates to the document are * send. */ const Incremental = 2; }

客户端收到initialize result之后,按照三次握手的原则,将返回一个initialized消息做确认。至此,一个服务端与客户端通信的生命周期就算是成功建立。

实现一个LSP服务 createConnection

服务端首先要获取一个Connection对象,通过vscode-languageserver提供的createConnection函数来创建Connection.

// Create a connection for the server, using Node's IPC as a transport. // Also include all preview / proposed LSP features. let connection = createConnection(ProposedFeatures.all);

Connection中对于LSP的消息进行了封装

onInitialize

监听客户端发送的initialize,返回服务端的能力,这里我们设置类型为增量监听,每次只传变化的部分。

connection.onInitialize((params: InitializeParams) => { let capabilities = params.capabilities; return { capabilities: { textDocumentSync: TextDocumentSyncKind.Incremental } }; });

根据三次握手的原则,客户端还会返回initialized notification进行通知,服务器在这向客服端发送信息了。

onInitialized connection.onInitialized(() => { connection.window.showInformationMessage('Hello World! form server side'); Listen on the connection //在最后设置监听 // Listen on the connection connection.listen();

连接成功时效果如图

image.png 客户端连接LSP服务 import * as path from "path"; import { workspace, ExtensionContext } from "vscode"; import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind, } from "vscode-languageclient/node"; 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: 'html' }], }; client = new LanguageClient( 'DemoLanguageServer', 'Demo Language Server', serverOptions, clientOptions ); // 启动客户端,同时启动语言服务器 client.start(); } export function deactivate(): Thenable | undefined { if (!client) { return undefined; } return client.stop(); } 写一个完整的LSP工程 先写server端 package.json

server端主要是用到vscode-languageserver和vscode-languageserver-textdocument

{ "name": "edm-helper-server", "description": "Example implementation of a language server in node.", "version": "1.0.0", "author": "Microsoft Corporation", "license": "MIT", "engines": { "node": "*" }, "repository": { }, "dependencies": { "vscode-languageserver": "^7.0.0", "vscode-languageserver-textdocument": "^1.0.1" }, "scripts": {} } tsconfig.json { "compilerOptions": { "target": "es2019", "lib": ["ES2019"], "module": "commonjs", "moduleResolution": "node", "sourceMap": true, "strict": true, "outDir": "out", "rootDir": "src" }, "include": ["src"], "exclude": ["node_modules", ".vscode-test"] } server.ts

先引入依赖

import { createConnection, TextDocuments, Diagnostic, DiagnosticSeverity, ProposedFeatures, InitializeParams, TextDocumentSyncKind, InitializeResult, } from "vscode-languageserver/node"; import { TextDocument } from "vscode-languageserver-textdocument";

这样我们就可以调用createConnection来创建连接了:

let connection = createConnection(ProposedFeatures.all);

还需要生成一个文本管理器用来监听文本的变化

let documents: TextDocuments = new TextDocuments(TextDocument);

初始化事件

connection.onInitialize((params: InitializeParams) => { const result: InitializeResult = { capabilities: { textDocumentSync: TextDocumentSyncKind.Incremental, }, }; return result; });

三次握手之后,我们可以在vscode上显示一条消息:

connection.onInitialized(() => { connection.window.showInformationMessage("Hello World! form server side"); });

连接成功后监听文件变化,诊断html文件内容是否符合edm语法规范

documents.onDidChangeContent((change) => { connection.window.showInformationMessage("validateTextDocument"); validateTextDocument(change.document); //检查是否符合语法 }); // 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();

检查的语法规则如下:

let errorList: any[] = [ { content: "position:absolute", message: "position is not support by edm", }, { content: "position:relative", message: "position is not support by edm", }, { content: "position:fixed", message: "position is not support by edm", }, { content: "position", message: "position is not support by edm", }, { content: "class", message: "class is not support by edm, use Inline CSS Style", }, { content: "", message: " is not support by edm, use Inline CSS Style", }, { content: "", message: " is not support by edm, use Inline CSS Style", }, { content: "", message: " is not support by edm, use Inline CSS Style", }, { content: "", message: " is not support by edm, use Inline CSS Style", }, { content: "margin", message: "margin will fail in some mailboxes, use padding", }, { content: "rgb", message: "rgb will fail in some mailboxes, use hexadecimal color,like #ffffff", }, { content: "!important", message: "!important is not support by edm", }, { content: "background: url", message: "background image is not support by edm", }, { content: "background-image", message: "background image is not support by edm", }, { content: "border-radius", message: "border-radius is not support by edm, use Image", }, { content: "", message: "Do not use div, Use TABLE not DIV", }, { content: "", message: "Do not use div, Use TABLE not DIV", }, { content: "#[a-zA-Z_0-9]{3,3};", message: "Do not abbreviate colors, such as #fff, write them as #ffffff", }, ]; async function validateTextDocument(textDocument: TextDocument): Promise { // The validator creates diagnostics for all uppercase words length 2 and more let text = textDocument.getText(); let regList = errorList.map((item) => { return item.content; }); let patternStr = regList.join("|"); let reg = new RegExp(patternStr, "g"); let m: RegExpExecArray | null; let diagnostics: Diagnostic[] = []; while ((m = reg.exec(text))) { let diagnostic: Diagnostic = { severity: DiagnosticSeverity.Warning, range: { start: textDocument.positionAt(m.index), end: textDocument.positionAt(m.index + m[0].length), }, message: errorList[regList.indexOf(m[0])].message, source: "edmHelper", }; diagnostic.relatedInformation = [ { location: { uri: textDocument.uri, range: Object.assign({}, diagnostic.range), }, message: "edm grammar", }, ]; diagnostics.push(diagnostic); } // Send the computed diagnostics to VSCode. connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); } client客户端

服务端开发完成后,我们继续写客户端

package.json

客户端要用到的库是vscode-languageclient

{ "name": "edm-helper-client", "description": "VSCode part of a language server", "author": "Microsoft Corporation", "license": "MIT", "version": "0.0.1", "publisher": "vscode", "repository": { }, "engines": { "vscode": "^1.52.0" }, "dependencies": { "vscode-languageclient": "^7.0.0" }, "devDependencies": { "@types/vscode": "^1.52.0", "vscode-test": "^1.3.0" } } tsconfig.json

与服务端的基本一致

{ "compilerOptions": { "module": "commonjs", "target": "es2019", "lib": ["ES2019"], "outDir": "out", "rootDir": "src", "sourceMap": true }, "include": ["src"], "exclude": ["node_modules", ".vscode-test"] } extension.ts

与之前给的例子一模一样

import * as path from "path"; import { ExtensionContext } from "vscode"; import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind, } from "vscode-languageclient/node"; let client: LanguageClient; export function activate(context: ExtensionContext) { console.log("activate"); // The server is implemented in node 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: "html" }], }; // Create the language client and start the client. client = new LanguageClient( "languageServerExample", "Language Server Example", serverOptions, clientOptions ); // Start the client. This will also launch the server client.start(); } export function deactivate(): Thenable | undefined { if (!client) { return undefined; } return client.stop(); } 组装运行

插件根目录底下的package.json

package.json

重点在入口文件和activationEvents的配置,配置在打开html文件的时候激活插件

"activationEvents": [ "onLanguage:html" ], "main": "./client/out/extension", { "name": "edm-helper", "description": "A language server example", "author": "Microsoft Corporation", "license": "MIT", "version": "1.0.0", "repository": { "type": "git", "url": "https://github.com/Microsoft/vscode-extension-samples" }, "publisher": "vscode-samples", "categories": [], "keywords": [ "multi-root ready" ], "engines": { "vscode": "^1.43.0" }, "activationEvents": [ "onLanguage:html" ], "main": "./client/out/extension", "contributes": { "configuration": { "type": "object", "title": "Example configuration", "properties": { "languageServerExample.maxNumberOfProblems": { "scope": "resource", "type": "number", "default": 100, "description": "Controls the maximum number of problems produced by the server." }, "languageServerExample.trace.server": { "scope": "window", "type": "string", "enum": [ "off", "messages", "verbose" ], "default": "off", "description": "Traces the communication between VS Code and the language server." } } } }, "scripts": { "vscode:prepublish": "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": "^8.0.3", "@types/node": "^12.12.0", "@typescript-eslint/parser": "^2.3.0", "eslint": "^6.4.0", "mocha": "^8.1.1", "typescript": "^4.2.2" } } tsconfig.json

我们还需要一个总的tsconfig.json,引用client和server两个目录:

{ "compilerOptions": { "module": "commonjs", "target": "es2019", "lib": ["ES2019"], "outDir": "out", "rootDir": "src", "sourceMap": true }, "include": [ "src" ], "exclude": [ "node_modules", ".vscode-test" ], "references": [ { "path": "./client" }, { "path": "./server" } ] } 配置vscode

下面我们在.vscode目录中写两个配置文件,使我们可以更方便地调试和运行。

.vscode/launch.json

有了这个文件之后,我们就有了运行的配置,可以通过F5来启动。

{ "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两个脚本。 这样⇧⌘B就可以对生成server和client的out目录下的js和map。

{ "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" ] } ] }

这时候就全部完成啦,按⇧⌘B生成js和js.map后。再摁F5调试就能看到效果啦。

image.png


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有