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-…

springboot3 redis 批量删除特定的 key 或带有特定前缀的 key

在 Spring Boot 3 中与 Redis 一起使用时&#xff0c;可以通过 Redis 的命令来实现批量删除特定的 Key 或带有特定前缀的 Key。以下是实现方式和注意事项。 使用 RedisTemplate RedisTemplate 是 Spring Boot 提供的一个操作 Redis 的工具&#xff0c;支持各种 Redis 操作。 …

MyBatis-plus sql拦截器

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

Java字符编码与正则表达式深度解析

Java字符编码与正则表达式深度解析 1. 字符编码发展 1.1 ASCII 码 在计算机最初发明时&#xff0c;主要用于数值计算&#xff0c;但随着计算需求的增加&#xff0c;人们发现计算机可以用来处理文本信息。因此&#xff0c;将字符映射为数字来表示。 字母 ‘A’ 映射为 65&am…

前端(十)js的使用

js的使用 文章目录 js的使用一、模态框二、使用js控制盒子变色三、图片轮播效果四、图片5s消失 一、模态框 <!doctype html> <html lang"en"> <head><meta charset"UTF-8"><title>Document</title><style>* {m…

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…

新服务器Linux网络配置

1、查看网口 ifconfig找到enp3s0或者 ens33&#xff0c;如果有ip&#xff0c;不用配置&#xff0c;本文结束。 2、如果不显示ip,打开文件/etc/sysconfig/network-scripts&#xff08;以enp3s0为例&#xff09; vi /etc/sysconfig/network-scripts/ifcfg-enp3s03、修改 //修…

leetcode hot 100 只出现一次的数字

136. 只出现一次的数字 已解答 简单 相关标签 相关企业 提示 给你一个 非空 整数数组 nums &#xff0c;除了某个元素只出现一次以外&#xff0c;其余每个元素均出现两次。找出那个只出现了一次的元素。 你必须设计并实现线性时间复杂度的算法来解决此问题&#xff0c;且…

汇编学习笔记

汇编 1. debug指令 -R命令(register) 查看、改变CPU寄存器的内容 r ax 修改AX中的内容 -D命令(display) 查看内存中的内容 -E命令(enter) 改写内存中的内容 -U命令(unassenble反汇编) 将内存中的机器指令翻译成汇编指令 -T命令(trace跟踪) 执行一条机器指令 -A命令…

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()…

【Qt】如何保证线程安全(以日志写入为例)

前言 在近日学习中发现&#xff0c;如果开发一个单例模式的日志系统&#xff0c;难免会出现多个线程记录日志的情况&#xff0c;这个时候线程可能导致竞争&#xff0c;或者始料未及的情况发生。 通过学习&#xff0c;如果要保证线程安全&#xff0c;要使用互斥锁QMutex&#xf…

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版☞【信息系统项目管理师】【综合知识】【备考知识点】第十一章 项目成本管理 移动端【思维导图】☞【信息系统项目管理师】【思维导图】第十一章 项目成本管理

九进制转10进制

//第一种 运用循环 public class Main { public static void main(String[] args) { Scanner scan new Scanner(System.in); //在此输入您的代码... int numscan.nextInt(); int result0; int p1; while(num>0) { int nnum%10; resultn*p; numnum/10; pp*9; } System.out.…

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

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

Java集合框架全面解析:从基本集合到线程安全集合

在Java中&#xff0c;集合&#xff08;Collection&#xff09;是用来存储和操作对象的框架&#xff0c;它是Java程序中不可或缺的组件之一。集合框架提供了丰富的数据结构&#xff0c;使数据的存储、查找、插入和删除变得更加高效。在这篇博客中&#xff0c;我们将详细介绍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;

Java枚举和常量类的区别以及优缺点

枚举&#xff08;Enum&#xff09;的深度解析 定义与语法&#xff1a; public enum Color {RED, GREEN, BLUE;private final String hexCode;Color() {// 默认构造函数可以为空&#xff0c;或根据需要初始化。this.hexCode "default";}Color(String hexCode) {thi…