Re: 从零开始的语言服务器开发冒险:文档链接

01 Jul 2024 3465 words 12 minutes BY-SA 4.0
develop lsp

“小女孩,你看,我遵守了诺言。”

– 刘慈欣《三体Ⅲ·死神永生》

完结系列文章,并赠送一点边角料。

文档链接

这里快速实现一个文档链接的 LSP feature :将文档中所有的链接用波浪线标出,当用户光标停留在链接处时,显示超链接的路径。用户可以按快捷键打开链接,如果链接是网页则直接打开链接,如果是文本则用当前编辑器打开该文本。

以下是 clangd 实现的文档链接。(妈妈再也不担心我找不到头文件在哪里啦)

link

neomutt 是笔者非常喜欢的一款电子邮件阅读器。其配置文件 neomuttrc 亦是语法酷似 vim script 和 zathurarc 的 DSL 。

neomutt

这里回到系列文章第一篇的方案:在不依靠抽象语法树仅靠正则表达式的情况下实现语言服务器。

在 neomuttrc 中,用 source XXX 来包含文件 XXX 。我们知道匹配 source XXXXXX 的正则表达式是 (?<=\bsource\b\s)\S+ ,所以:

import re

PAT = re.compile(r"(?<=\bsource\b\s)\S+")

仿照系列文章第一篇,我们实现一个语言服务器:

from lsprotocol.types import *
from pygls.server import LanguageServer


class MuttLanguageServer(LanguageServer):
    def __init__(self, *args: Any, **kwargs: Any) -> None:
        super().__init__(*args, **kwargs)
  1. 获取文档内容
  2. 循环:在每一行搜索正则表达式
  3. 循环:对每个搜索结果成一个文档链接对象
  4. 返回所有的文档链接对象
import os


class MuttLanguageServer(LanguageServer):
    def __init__(self, *args: Any, **kwargs: Any) -> None:
        # ...
        @self.feature(TEXT_DOCUMENT_DOCUMENT_LINK)
        def document_link(params: DocumentLinkParams) -> list[DocumentLink]:
            document = self.workspace.get_document(params.text_document.uri)
            links = []
            for i, line in enumerate(document.source.splitlines()):
                for m in PAT.finditer(line):
                    _range = Range(
                        Position(i, m.start()), Position(i, m.end())
                    )
                    url = os.path.join(
                        os.path.dirname(params.text_document.uri),
                        os.path.expanduser(line[m.start() : m.end()]),
                    )
                    links += [DocumentLink(_range, url)]
            return links

document link

本文代码开源于 mutt-language-server

用正则表达式实现这个 LSP feature 是为了再次强调一件事,就是:

语言服务器协议只规定语言服务器和语言客户端如何通信,不规定具体的实现方式。无论使用什么语言、什么算法,哪怕是一个人坐在小黑屋里,接受输入,这个人给出符合标准的返回都是合法的语言服务器!请灵活地使用任何技术去实现它!

碎碎念

术语翻译

不少术语都是笔者自己翻译的,比如很多 LSP features ,微软的标准文档就是英文,哪有什么官方的中文翻译……

笔者尽量做到信和达,所以会增加一些词语,比如在 UI 设计中, hover 指当鼠标挪到某个按钮上时显示该按钮用途文档的气泡窗口。在 LSP 中就翻译为文档悬停,因为单纯的悬停总是让人联想到直升机的悬停实在出戏

调试

neovim 有插件 playground 可以查看光标下 token 在抽象语法树中的节点信息:

image

陷阱

系列文章中其实有不少设计不妥的“陷阱”。它们的存在是出于以下原因:

无论如何,笔者保证这些陷阱都是善意而非恶意的。(不过说陷阱是善意的确实很奇怪?)

接下来我们将撕开所有真相的遮羞布:

尽管没有多少读者在看文章时会动手运行代码查看结果,但还是在这里给出所有解释和解决方法,作为对发现问题的读者的嘉奖~

教科书 v.s. 教程

除去科普 LSP 历史的文章外,系列文章每篇都试图解释清楚一个概念,再给出相关的实践:

  1. LSP 相关标准,如何实现语言服务器
  2. 抽象语法树,如何实现解析器
  3. 跳转,如何实现搜索算法
  4. 验证器,如何实现验证模式
  5. 总结

笔者本人反对什么概念原理通通不提,只有手把手的保姆级教程,也不感冒全篇都是道理的不带任何实践性质的教科书。好的知识普及应当是理论和实践相辅相成的。如果读者仅仅是照着保姆级教程抄一大段代码,切换到新的相近的但没有保姆级教程的问题时会有独立思考的能力吗?如果全是理论,又显得太枯燥了一点。当然,这也只是笔者个人观点而已。

后续

有后续嘛?应该不会有。因为:

  1. 写文章讲究厚积薄发。光这几篇系列文章差不多就是笔者一个月以上阅读各类标准文档,实践 LSP 相关技术的积累了。笔者已经没有更多的积累了。
  2. 系列文章从结构看已经很完整了
  3. 确实没人看……

心里话

最后说点心里话。

这是一代不如一代吗?不是这样的。时代发展就是会带来新的技术。笔者也只是尝试拥抱这种新的技术,甚至更加深入地了解这种技术。

回忆笔者了解技术的源头,是笔者因为老师项目的缘故接触了许许多多的 DSL ,像设备树源文件 *.dts, bitbake 语言 *.bb, Makefile, cmake, autoconf, gdb script 等等,在编辑器编辑这些 DSL 根本没有任何补全,有些甚至连语法高亮都不一定有,所以开始抽出业余时间研究这些补全是怎么来的,如何支持补全这些语言等等。

诚然,有的语言问世时间已久,甚至有极大的影响力,但对代码补全、定义跳转却没有任何好的支持。这不是很奇怪吗?因为:

啊, CAP 不能三全,所以大家是都不乐意牺牲自己的时间嘛?

很多技术、工具确实无利可图,它们的出现就是一部分人牺牲自己的时间让更多的人得到方便。开源圈正是:

让我们朝着最终“所有人都得到了所有领域的方便”迈进吧。本系列文章和代码也不过是一点小小的牺牲。也希望大家在享受编辑器编辑代码的便利之时想起默默无闻之辈做出的牺牲。

https://user-images.githubusercontent.com/32936898/199681341-1c5cfa61-4411-4b67-b268-7cd87c5867bb.png https://user-images.githubusercontent.com/32936898/199681363-1094a0be-85ca-49cf-a410-19b3d7965120.png https://user-images.githubusercontent.com/32936898/199681368-c34c2be7-e0d8-43ea-8c2c-d3e865da6aeb.png