[新手] 有关 LSP 协议服务端和客户端的实现的请教

背景和吐槽

大家好,经常看见论坛里的大佬写 lsp client
说到 LSP 的话,目前个人感觉体验最好的支持是 Jetbrains的IDEA 为 Java/Kotlin 提供的支持
后来去学习 Julia 语言,做了一些数据结构的项目,虽然是门现代语言,他的类型设计比隔壁的
Python 好那么一点,有静态类型和动态类型,但是他的 LanguageServer 支持是由社区支持的,理论上来讲他的实现会比 Python 更好实现, 结果在开发的时候没有良好的补全和诊断支持,导致大部分错误发生在运行时,需要手动code review,而且可能还没有错误提示

不经感叹,Python LSP 的贡献人员真多,还有大厂人员的支持,Julia语言任重道远啊(架不住别人有一个好爹啊)

我的尝试

我今天尝试了下用 lsp4j 写 LSP 服务端实现,然而自己光顾着看 GPT 尝试的答案,调试的时候发现自己不知道怎么触发补全

class YamlLanguageServer: LanguageServer, LanguageClientAware, KoinComponent {
    val _textDocumentService: TextDocumentService by inject<TextDocumentService>()
    val _workspaceService: WorkspaceService by inject<WorkspaceService>()
    var client: LanguageClient? = null

    override fun initialize(params: InitializeParams): CompletableFuture<InitializeResult> {
        val capabilities = ServerCapabilities().apply {
            setTextDocumentSync(TextDocumentSyncKind.Full)
            completionProvider = CompletionOptions()
        }

        val result = InitializeResult(capabilities)
        return CompletableFuture.completedFuture(result)
    }

    override fun shutdown(): CompletableFuture<Any> {
        return CompletableFuture.completedFuture(null)
    }

    override fun exit() {
        System.exit(0)
    }

    override fun getTextDocumentService(): TextDocumentService {
        return _textDocumentService
    }

    override fun getWorkspaceService(): WorkspaceService {
        return _workspaceService
    }

    override fun connect(client: LanguageClient) {
        this.client = client
    }
}

然后我发现,不止要写后端,还要写前端适配他,工作量又大了起来(怪不得大家都写 lsp client 给 emacs)

我的目的

扩展和强化 Julia 的 LanguageServer.jl 实现,可能的话改下 julia 的 vscode plugin 关于 LSP 前端的实现

明天

明天我打算看下这些中文资料,现在感觉自己有点钻牛角尖,病急乱投医的感觉了, 我得调整下状态,明天继续探索
就是资料有点旧,没有进行更新

我的计划和需要帮助的地方

我打算一步一步来,先做几个简单的语言支持,比如说

  1. yaml
  2. toml
  3. markdown (这个好像不是很简单)

然后我去看了下 vscode 的 redhat-developer 的 yaml 支持,这种配置语言没必要写那么多的代码吧 :joy:

然后我想做个简单的 Julia LSP,然后依次实现

  1. 代码补全
  2. 错误诊断
  3. 代码导航
  4. 重构支持
  5. 文档交互
  6. 代码格式化

想看看有没有入门级项目可以参考的,其实我已经找过好多仓库了(这代码量太大了,看不下去😭), 怕自己走太多弯路,看看有没有大佬指路

这里还有几个问题

  1. LSP 中需要用到编译原理来进行更强大的解析吗
  2. LSP 需要处理语法高亮吗
  3. 编译原理学的不怎么好,分不清 tree-sitter 和 antlr4

ps: 我怎么感觉写着写着就变成吐槽和抱怨了,请见谅

1 个赞

我们公司的免费软件 cloudquery就是使用 LSP 技术实现的SQL智能提示,具体的一些能力可以看我这篇文章. https://zhuanlan.zhihu.com/p/15549298362

具体技术是 前端 monaco编辑器 ,后端java lsp4J + 自己实现的netty websocket处理lsp消息转发到 java LSP4J. LSP的提示逻辑我用到了编译原理知识,具体来说是基于antlr4分析SQL语法,分析当前光标位置应该提示什么东西, 基于antlr4分析SQL语句的列来源关系. 编译原理学习的话,其实花时间慢慢看书,慢慢学就行了,坚持看完一本书.

2 个赞

Update 2025-2-16

终于能调试到 前端了,我这边前端是 vscode,开发的时候原来是自己忘了将源代码编译,并在package.json 中设置 “main” 字段,修改后的 package.json 代码如下

{
  "name": "client",
  "displayName": "client",
  "description": "",
  "version": "0.0.1",
  "engines": {
    "vscode": "^1.97.0"
  },
  "categories": [
    "Programming Languages"
  ],
  "scripts": {
    "compile": "tsc -p ./",
    "watch": "tsc --watch -p ./"
  },
  "activationEvents": [
    "onLanguage:yaml"
  ],

  "main": "out/extension.js",
  "contributes": {
    "languages": [
      {
        "id": "yaml",
        "aliases": [
          "yaml",
          "yml"
        ],
        "extensions": [
          ".yaml"
        ],
        "configuration": "./language-configuration.json"
      }
    ],
    "grammars": [
      {
        "language": "yaml",
        "scopeName": "source.yaml",
        "path": "./syntaxes/.tmLanguage.json"
      }
    ]
  },
  "dependencies": {
    "@types/node": "^22.13.4",
    "vscode": "^1.1.37",
    "vscode-languageclient": "^9.0.1"
  },
  "devDependencies": {
    "typescript": "^5.7.3"
  }
}

tsconfig.json 如下

{
    "compilerOptions": {
        "lib": ["es2016","WebWorker"],
        "module": "commonjs",
        "moduleResolution": "node",
        "outDir": "out",
        "skipLibCheck": true,
        "sourceMap": true,
        "target": "es6"
      },
      "exclude": ["node_modules"],
      "include": ["src", "test"]
}

顺带修改了连接方式,使用本地套接字
前端的代码如下

let client: LanguageClient | null = null

export const activate = (context: vscode.ExtensionContext) => {
    const serverOptions = () => {
        let socket = net.connect({host: "localhost", port: 8080})
        socket.on("error", (err) => {
            vscode.window.showErrorMessage(`LSP connect failed: ${err.message}`)
        })
        
        let streamInfo: StreamInfo = {
            writer: socket,
            reader: socket
        }

        return Promise.resolve(streamInfo)
    }

    const clientOptions: LanguageClientOptions = {
        documentSelector: [
            { scheme: "file", language: "*"}
        ],

        outputChannel: vscode.window.createOutputChannel("LSP Connection Log"),

        synchronize: {
            // PROBLEM: don't understand here
            fileEvents: vscode.workspace.createFileSystemWatcher("**/*")
        }
    }

    client = new LanguageClient(
        "lsp-from-scratch server hello",
        "lsp-from-scratch server world",
        serverOptions,
        clientOptions
    )

    client.start()
}

相应的,后端也做了更改

try {
        val server = ServerSocket(8080)
        val languageServer = ScratchLanguageServer()
        println("listening...")
        val socket = server.accept()
        println("accepted")
        val launcher = Launcher.createLauncher(
            languageServer,
            LanguageClient::class.java,
            socket.inputStream,
            socket.outputStream,
        )

        launcher.startListening()
    } catch (_: IOException) {
        println("closed")
    }

至此,问题解决,接下来就要自己慢慢探索了