LSP介绍并实现语言服务

首发于Enaium的个人博客


LSP (Language Server Protocol) 介绍

前段时间我为Jimmer DTO实现了一个 LSP 的语言服务,这是我第一次实现 LSP,所以在这里我分享一下我实现LSP的经验。

首先来看一下效果,图片太多,我就放一部分,更多的可以看jimmer-dto-lsp

属性提示

结构

触摸

高亮

LSP 是一种协议,用于在 IDE 和语言服务器之间通信。IDE 通过 LSP 请求语言服务器提供代码分析服务,语言服务器通过 LSP 响应 IDE 的请求。在没有 LSP 之前,每个 IDE 都需要为每种语言实现一套代码分析服务,而 LSP 的出现使得 IDE 只需要实现一套 LSP 协议,就可以使用任何支持 LSP 的语言服务器。所以就大大降低了 IDE 的开发成本。

列如,需要从一个地方跳转到其他地方,IDE 会发送一个请求,位置是第 3 行第 12

{"jsonrpc": "2.0","id": 1,"method": "textDocument/definition","params": {"textDocument": {"uri": "file:///p%3A/mseng/VSCode/Playgrounds/cpp/use.cpp"},"position": {"line": 3,"character": 12}}
}

之后服务端会返回一个响应,位置是第 0 行第 4 列到第 0 行第 11 列,这样 IDE 就可以跳转到这个位置

{"jsonrpc": "2.0","id": 1,"result": {"uri": "file:///p%3A/mseng/VSCode/Playgrounds/cpp/provide.cpp","range": {"start": {"line": 0,"character": 4},"end": {"line": 0,"character": 11}}}
}

实现

上面的例子中是使用纯文本实现的,我们可以直接使用封装好的库,比如lsp4j。由于只是简单的教学,我这里只实现代码的高亮,语言是JSON5,词法分析就使用antlr4

首先我们需要创建一个Gradle项目,下面是我们项目中需要的所有依赖和插件。

[versions]
kotlin = "2.1.0"
antlr = "4.13.0"
lsp4j = "0.23.1"
shadow = "9.0.0-beta4"
[libraries]
antlr = { group = "org.antlr", name = "antlr4", version.ref = "antlr" }
lsp4j = { module = "org.eclipse.lsp4j:org.eclipse.lsp4j", version.ref = "lsp4j" }
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
shadow = { id = "com.gradleup.shadow", version.ref = "shadow" }

接着创建一个叫langauge的子项目,并在src\main\antlr\cn\enaium\j5下创建一个J5.g4文件。

import org.gradle.kotlin.dsl.withType
import org.jetbrains.kotlin.gradle.tasks.KotlinCompileplugins {alias(libs.plugins.kotlin.jvm)antlr
}repositories {mavenCentral()
}dependencies {antlr(libs.antlr)testImplementation(kotlin("test"))
}tasks.test {useJUnitPlatform()
}tasks.withType<Jar>().configureEach {dependsOn(tasks.withType<AntlrTask>())
}tasks.withType<KotlinCompile>().configureEach {dependsOn(tasks.withType<AntlrTask>())
}

在grammars-v4中找到JSON5g4文件,之后将grammar JSON5;改为grammar J5;,将单行注释和多行注释的 -> skip给去掉。

// Student Main
// 2020-07-22
// Public domain// JSON5 is a superset of JSON, it included some feature from ES5.1
// See https://json5.org/
// Derived from ../json/JSON.g4 which original derived from http://json.org// $antlr-format alignTrailingComments true, columnLimit 150, minEmptyLines 1, maxEmptyLinesToKeep 1, reflowComments false, useTab false
// $antlr-format allowShortRulesOnASingleLine false, allowShortBlocksOnASingleLine true, alignSemicolons hanging, alignColons hanginggrammar J5;json5: value? EOF;obj: '{' pair (',' pair)* ','? '}'| '{' '}';pair: key ':' value;key: STRING| IDENTIFIER| LITERAL| NUMERIC_LITERAL;value: STRING| number| obj| arr| LITERAL;arr: '[' value (',' value)* ','? ']'| '[' ']';number: SYMBOL? (NUMERIC_LITERAL | NUMBER);// LexerSINGLE_LINE_COMMENT: '//' .*? (NEWLINE | EOF);MULTI_LINE_COMMENT: '/*' .*? '*/';LITERAL: 'true'| 'false'| 'null';STRING: '"' DOUBLE_QUOTE_CHAR* '"'| '\'' SINGLE_QUOTE_CHAR* '\'';fragment DOUBLE_QUOTE_CHAR: ~["\\\r\n]| ESCAPE_SEQUENCE;fragment SINGLE_QUOTE_CHAR: ~['\\\r\n]| ESCAPE_SEQUENCE;fragment ESCAPE_SEQUENCE: '\\' (NEWLINE| UNICODE_SEQUENCE       // \u1234| ['"\\/bfnrtv]          // single escape char| ~['"\\bfnrtv0-9xu\r\n] // non escape char| '0'                    // \0| 'x' HEX HEX            // \x3a);NUMBER: INT ('.' [0-9]*)? EXP? // +1.e2, 1234, 1234.5| '.' [0-9]+ EXP?        // -.2e3| '0' [xX] HEX+          // 0x12345678;NUMERIC_LITERAL: 'Infinity'| 'NaN';SYMBOL: '+'| '-';fragment HEX: [0-9a-fA-F];fragment INT: '0'| [1-9] [0-9]*;fragment EXP: [Ee] SYMBOL? [0-9]*;IDENTIFIER: IDENTIFIER_START IDENTIFIER_PART*;fragment IDENTIFIER_START: [\p{L}]| '$'| '_'| '\\' UNICODE_SEQUENCE;fragment IDENTIFIER_PART: IDENTIFIER_START| [\p{M}]| [\p{N}]| [\p{Pc}]| '\u200C'| '\u200D';fragment UNICODE_SEQUENCE: 'u' HEX HEX HEX HEX;fragment NEWLINE: '\r\n'| [\r\n\u2028\u2029];WS: [ \t\n\r\u00A0\uFEFF\u2003]+ -> skip;

之后编译项目就会生成J5LexerJ5Parser

接着创建一个server项目用于实现我们的语言服务。

plugins {alias(libs.plugins.kotlin.jvm)alias(libs.plugins.shadow)
}repositories {mavenCentral()
}dependencies {implementation(project(":language"))implementation(libs.lsp4j)testImplementation(kotlin("test"))
}tasks.test {useJUnitPlatform()
}tasks.jar {dependsOn(tasks.shadowJar)
}

首先我们需要实现一个LanguageServer接口。

package cn.enaium.j5.lspimport org.eclipse.lsp4j.InitializeParams
import org.eclipse.lsp4j.InitializeResult
import org.eclipse.lsp4j.services.LanguageServer
import org.eclipse.lsp4j.services.TextDocumentService
import org.eclipse.lsp4j.services.WorkspaceService
import java.util.concurrent.CompletableFuture/*** @author Enaium*/
class J5LanguageServer : LanguageServer {override fun initialize(params: InitializeParams): CompletableFuture<InitializeResult> {TODO("Not yet implemented")}override fun shutdown(): CompletableFuture<in Any> {TODO("Not yet implemented")}override fun exit() {TODO("Not yet implemented")}override fun getTextDocumentService(): TextDocumentService {TODO("Not yet implemented")}override fun getWorkspaceService(): WorkspaceService {TODO("Not yet implemented")}
}

接着依次实现TextDocumentServiceWorkspaceService

package cn.enaium.j5.lspimport org.eclipse.lsp4j.DidChangeTextDocumentParams
import org.eclipse.lsp4j.DidCloseTextDocumentParams
import org.eclipse.lsp4j.DidOpenTextDocumentParams
import org.eclipse.lsp4j.DidSaveTextDocumentParams
import org.eclipse.lsp4j.services.TextDocumentService/*** @author Enaium*/
class J5TextDocumentService : TextDocumentService {override fun didOpen(params: DidOpenTextDocumentParams) {TODO("Not yet implemented")}override fun didChange(params: DidChangeTextDocumentParams) {TODO("Not yet implemented")}override fun didClose(params: DidCloseTextDocumentParams) {TODO("Not yet implemented")}override fun didSave(params: DidSaveTextDocumentParams) {TODO("Not yet implemented")}
}
package cn.enaium.j5.lspimport org.eclipse.lsp4j.DidChangeConfigurationParams
import org.eclipse.lsp4j.DidChangeWatchedFilesParams
import org.eclipse.lsp4j.services.WorkspaceService/*** @author Enaium*/
class J5WorkspaceService : WorkspaceService {override fun didChangeConfiguration(params: DidChangeConfigurationParams) {}override fun didChangeWatchedFiles(params: DidChangeWatchedFilesParams) {}
}

实现initialize方法,这个方法主要是需要返回我们这个语言服务器为支持什么功能。

override fun initialize(params: InitializeParams): CompletableFuture<InitializeResult> {return CompletableFuture.completedFuture(InitializeResult(ServerCapabilities().apply {setTextDocumentSync(TextDocumentSyncOptions().apply {openClose = truechange = TextDocumentSyncKind.FullsetSave(SaveOptions().apply {includeText = true})})semanticTokensProvider = SemanticTokensWithRegistrationOptions().apply {legend = SemanticTokensLegend().apply {tokenTypes = SemanticType.entries.map { it.type }}setFull(true)}}))
}

首先任何一个语言服务都需要具备这个文档同步功能,这个功能会在打开关闭修改和保存文件是触发。之后是提供语义,提供语义之后,IDE就可以根据这个语义来实现代码高亮。

我们需要定义一个SemanticType枚举类。

enum class SemanticType(val id: Int, val type: String) {COMMENT(0, "comment"),KEYWORD(1, "keyword"),FUNCTION(2, "function"),STRING(3, "string"),NUMBER(4, "number"),DECORATOR(5, "decorator"),MACRO(6, "macro"),TYPE(7, "type"),TYPE_PARAMETER(8, "typeParameter"),CLASS(9, "class"),VARIABLE(10, "variable"),PROPERTY(11, "property"),STRUCT(12, "struct"),INTERFACE(13, "interface"),PARAMETER(14, "parameter"),ENUM_MEMBER(15, "enumMember"),NAMESPACE(16, "namespace"),
}

之后实现一下剩余的方法。

override fun shutdown(): CompletableFuture<Any> {return CompletableFuture.completedFuture(true)
}
override fun exit() {
}
override fun getTextDocumentService(): TextDocumentServicereturn J5TextDocumentService()
}
override fun getWorkspaceService(): WorkspaceService {return J5WorkspaceService()
}

然后实现代码同步功能。

val cache = mutableMapOf<String, String>()override fun didOpen(params: DidOpenTextDocumentParams) {cache[params.textDocument.uri] = params.textDocument.text
}
override fun didChange(params: DidChangeTextDocumentParams) {cache[params.textDocument.uri] = params.contentChanges[0].text
}
override fun didClose(params: DidCloseTextDocumentParams) {cache.remove(params.textDocument.uri)
}
override fun didSave(params: DidSaveTextDocumentParams) {cache[params.textDocument.uri] = params.text
}

接着我们需要再在J5TextDocumentService的实现类中实现一个semanticTokensFull方法。

override fun semanticTokensFull(params: SemanticTokensParams): CompletableFuture<SemanticTokens> {val document = cache[params.textDocument.uri] ?: return CompletableFuture.completedFuture(SemanticTokens())val data = mutableListOf<Int>()var previousLine = 0var previousChar = 0val j5Lexer = J5Lexer(CharStreams.fromString(document))val token = CommonTokenStream(j5Lexer)token.fill()token.tokens.forEach { token ->val semanticType = when (token.type) {J5Lexer.STRING -> SemanticType.STRINGJ5Lexer.NUMBER -> SemanticType.NUMBERJ5Lexer.NUMERIC_LITERAL -> SemanticType.NUMBERJ5Lexer.LITERAL -> SemanticType.KEYWORDJ5Lexer.SINGLE_LINE_COMMENT -> SemanticType.COMMENTJ5Lexer.MULTI_LINE_COMMENT -> SemanticType.COMMENTJ5Lexer.IDENTIFIER -> SemanticType.VARIABLEJ5Lexer.SYMBOL -> SemanticType.KEYWORDelse -> return@forEach}token.text.split("\n").forEachIndexed { index, s ->val start = Position(token.line - 1, token.charPositionInLine)val currentLine = start.line + indexval currentChar = if (index == 0) start.character else 0data.add(currentLine - previousLine)data.add(if (previousLine == currentLine) currentChar - previousChar else currentChar)data.add(s.length)data.add(semanticType.id)data.add(0)previousLine = currentLinepreviousChar = currentChar}}return CompletableFuture.completedFuture(SemanticTokens(data))
}

最后我们需要创建一个主方法来启动我们的语言服务。

fun main() {val server = J5LanguageServer()val launcher = Launcher.createLauncher(server, LanguageClient::class.java, System.`in`, System.out)launcher.startListening()
}

测试

新建一个后缀为j5的文件,然后输入以下内容。

{/* play with comments{  true, NaN   ] , {}* / aaa{}// make sure we included all \p{L},yes, json5, and ECMAScript 5+ supports them
//*/全世界无产者: "联合起来",n1: 1e2,n2: 0.2e-4,// May not works in some poor IDE// but works in official parserInfinity: -Infinity,NaN: -NaN,true: true,false: false,// yes, it works in their parser too一: "Unicode!"
}// comment ends with eof

之后我这里使用neovim来测试,确保你已经安装了lspconfig

· 在init.lua中添加以下内容。

vim.cmd [[au BufRead,BufNewFile *.j5                set filetype=J5]]local lsp = require('lspconfig')
local lsp_config = require('lspconfig.configs')lsp_config.j5 = {default_config = {cmd = { 'java', '-cp', 'D:/Projects/teaching-lsp/server/build/libs/server-1.0-SNAPSHOT-all.jar', 'cn.enaium.j5.lsp.MainKt' },filetypes = { 'J5' },root_dir = function(fname)return lsp.util.root_pattern('*.j5')(fname)end,}
}lsp_config.j5.setup {}

neovim

源码

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/66173.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

谷粒商城项目125-spring整合high-level-client

新年快乐! 致2025年还在努力学习的你! 你已经很努力了&#xff0c;今晚就让自己好好休息一晚吧! 在后端中选用哪种elasticsearch客户端&#xff1f; elasticsearch可以通过9200或者9300端口进行操作 1&#xff09;9300&#xff1a;TCP spring-data-elasticsearch:transport-…

MyBatis-plus sql拦截器

因为业务需求&#xff0c;重新写了一套数据权限。项目中用的是mybtis-plus&#xff0c;正好MyBatis-Plus提供了插件数据权限插件 | MyBatis-Plus&#xff0c;那就根据文档来实现这个需求。 实现&#xff1a; 实现MultiDataPermissionHandler 首先创建MultiDataPermissionHan…

Docker 远程访问完整配置教程以及核心参数理解

Docker 远程访问完整配置教程 以下是配置 Docker 支持远程访问的完整教程&#xff0c;包括参数说明、配置修改、云服务器安全组设置、主机防火墙配置&#xff0c;以及验证远程访问的详细步骤。 1. 理解 -H fd:// 参数的作用&#xff08;理解了以后容易理解后面的操作&#xff…

第十一章 图论

/* * 题目名称&#xff1a;连通图 * 题目来源&#xff1a;吉林大学复试上机题 * 题目链接&#xff1a;http://t.cn/AiO77VoA * 代码作者&#xff1a;杨泽邦(炉灰) */#include <iostream> #include <cstdio>using namespace std;const int MAXN 1000 10;int fathe…

Flutter踩坑记-第三方SDK不兼容Gradle 8.0,需适配namespace

最近需要集成Flutter作为Module&#xff0c;Flutter依赖了第三方库&#xff0c;Gradle是8.0版本。 编译报错&#xff1a; 解决办法是在.android根目录下的build.gradle下新增一行代码&#xff1a; buildscript {ext.kotlin_version "1.8.22"repositories {google()…

SMMU软件指南之系统架构考虑

安全之安全(security)博客目录导读 目录 5.1 I/O 一致性 5.2 客户端设备 5.2.1 地址大小 5.2.2 缓存 5.3 PCIe 注意事项 5.3.1 点对点通信 5.3.2 No_snoop 5.3.3 ATS 5.4 StreamID 分配 5.5 MSI 本博客介绍与 SMMU 相关的一些系统架构注意事项。 5.1 I/O 一致性 如…

【信息系统项目管理师】【综合知识】【备考知识点】【思维导图】第十一章 项目成本管理

word版☞【信息系统项目管理师】【综合知识】【备考知识点】第十一章 项目成本管理 移动端【思维导图】☞【信息系统项目管理师】【思维导图】第十一章 项目成本管理

计算机毕业设计PyHive+Hadoop深圳共享单车预测系统 共享单车数据分析可视化大屏 共享单车爬虫 共享单车数据仓库 机器学习 深度学习

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…

C++ 复习总结记录二

C 复习总结记录二 主要内容 1、认识面向过程和面向对象 2、类的引入 3、类的定义 4、类的访问限定符及封装 5、类的作用域 6、类的实例化 7、类的对象大小的计算 8、类成员函数的 this 指针 一 认识面向过程和面向对象 C语言是面向过程的&#xff0c;关注的是过程&a…

Mysql运维利器之备份恢复-xtrabackup 安装

1、插件下载 xtrabackup 下载地址 找到自己mysql版本对应得 插件版本下载 2、执行安装命令 yum localinstall percona-xtrabackup-80-8.0.26-18.1.el7.x86_64.rpm 安装完毕&#xff01;查看版本信息 xtrabackup --version 安装完毕&#xff01;&#xff01;&#xff01;

Hoverfly 任意文件读取漏洞(CVE-2024-45388)

漏洞简介 Hoverfly 是一个为开发人员和测试人员提供的轻量级服务虚拟化/API模拟/API模拟工具。其 /api/v2/simulation​ 的 POST 处理程序允许用户从用户指定的文件内容中创建新的模拟视图。然而&#xff0c;这一功能可能被攻击者利用来读取 Hoverfly 服务器上的任意文件。尽管…

Aloudata AIR | 逻辑数据平台的 NoETL 之道

一文为你介绍 Aloudata AIR 逻辑数据平台的技术原理与核心价值 本文主旨是介绍逻辑数据平台的技术原理与核心价值&#xff0c;包含几个部分的内容&#xff1a; 首先&#xff0c;简要阐述逻辑数据平台出现的背景&#xff1b;其次&#xff0c;详细讲解逻辑数据平台的构建方法&am…

c# CodeFirst生成表字段加注释

前置&#xff1a;ORM框架工具使用的FreeSql 背景&#xff1a;开发环境中运行接口&#xff0c;所有的表字段以及备注会自动加上&#xff0c;但是在测试环境时运行就只生成了表&#xff0c;没有把每个字段的注释加上 问题检查&#xff1a; FreeSql CodeFirst 支持将 c# 代码内的注…

【pyqt】(四)Designer布局

布局 之前我们利用鼠标拖动的控件的时候&#xff0c;发现一些部件很难完成对齐这些工作&#xff0c;pyqt为我们提供的多种布局功能不仅可以让排版更加美观&#xff0c;还能够让界面自适应窗口大小的变化&#xff0c;使得布局美观合理。最常使用的三种布局就是垂直河子布局、水…

Flutter Android修改应用名称、应用图片、应用启动画面

修改应用名称 打开Android Studio&#xff0c;打开对应项目的android文件。 选择app下面的manifests->AndroidManifest.xml文件&#xff0c;将android:label"bluetoothdemo2"中的bluetoothdemo2改成自己想要的名称。重新启动或者重新打包&#xff0c;应用的名称…

【HENU】河南大学计院2024 计算机体系结构 期末复习知识点

和光同尘_我的个人主页 一直游到海水变蓝。 体系结构 第一章&#xff1a;计算机系统基础知识计算机系统的实质计算机系统的设计的4个定量原理Amdahl定律CPU性能公式程序的局部性原理: 第二章&#xff1a;指令系统的设计指令系统结构的分类通用寄存器型结构 哈夫曼编码MIPS指令…

计算机网络复习(大题)

&#x1f4e2;&#x1f4e2;&#x1f4e2;传送门 一、简答题&#xff08;1&#xff09;五层原理体系结构每层功能&#xff1a;&#xff08;2&#xff09;TCP建立连接三次握手过程&#xff1a;&#xff08;3&#xff09;访问浏览器的过程&#xff1a;&#xff08;4&#xff09;抓…

AWS re:Invent 的创新技术

本月早些时候&#xff0c;Amazon 于 12 月 1 日至 5 日在内华达州拉斯维加斯举行了为期 5 天的 re&#xff1a;Invent 大会。如果您从未参加过 re&#xff1a;Invent 会议&#xff0c;那么最能描述它的词是“巨大”——不仅从与会者人数&#xff08;60,000 人&#xff09;来看&…

深入理解Java的 JIT(即时编译器)

&#x1f9d1; 博主简介&#xff1a;CSDN博客专家&#xff0c;历代文学网&#xff08;PC端可以访问&#xff1a;https://literature.sinhy.com/#/literature?__c1000&#xff0c;移动端可微信小程序搜索“历代文学”&#xff09;总架构师&#xff0c;15年工作经验&#xff0c;…

民宿酒店预订系统小程序+uniapp全开源+搭建教程

一.介绍 一.系统介绍 基于ThinkPHPuniappuView开发的多门店民宿酒店预订管理系统&#xff0c;快速部署属于自己民宿酒店的预订小程序&#xff0c;包含预订、退房、WIFI连接、吐槽、周边信息等功能。提供全部无加密源代码&#xff0c;支持私有化部署。 二.搭建环境 系统环境…