Skip to content
全文约 1277 字 预计阅读 4 分钟

LCUI 语言服务 ​

NOTE

以下内容取自 LOICollectionA 1.17.1 的代码结构,对于后续版本可能会有所不同。

定位 ​

src/LOICollectionA/frontend/lsp/ 是 .lcui 脚本的语言服务核心:它复用编译器前端(Lexer → Parser → ComponentExpander → SemanticAnalyzer)产出诊断、符号、补全、悬停与跳转定义,并按 LSP 协议封装成 JSON-RPC 消息。

它只做协议与分析,不做 I/O:

  • 不监听端口、不读写 stdio、不启动线程
  • 输入是一段字节流,输出是待发回的字节流
  • 传输层(stdio、管道、WebSocket、游戏内命令)由调用方决定

这样设计有两个原因:插件本体不需要为了编辑器支持常驻一个服务端;而语言核心能被 gtest 直接驱动(tests/common/frontend/LspTest.cpp),无需任何进程间通信。

模块结构 ​

文件职责
Protocol.h / .cppLSP 数据类型(Position / Range / Diagnostic / CompletionItem / Hover / Location)、JSON 序列化、JSON-RPC 消息构造、Content-Length 分帧与解帧
Analysis.h / .cpp对一段脚本文本跑完整前端,输出 DocumentAnalysis(诊断 + 符号表)
LanguageServer.h / .cpp文档同步(全量同步)、请求分发、能力声明,以及 completionsAt / hoverAt / definitionAt 三个纯函数

能力清单 ​

LSP 方法行为
initialize返回 textDocumentSync: 1(全量同步)、completionProvider(触发字符 .)、hoverProvider、definitionProvider、codeActionProvider
textDocument/didOpen记录文档,立即推送 publishDiagnostics
textDocument/didChange整体替换文本(取最后一条 contentChanges),重新推送诊断
textDocument/didClose移除文档并清空该文档的诊断
textDocument/completion见「补全规则」
textDocument/hover返回光标处标识符的声明签名
textDocument/definition返回声明处的 Location
textDocument/codeAction为请求区间内的诊断返回 quickfix,见「快速修复规则」
shutdown / exit正常结束

未实现的方法返回 -32601 Method not found,$/... 通知静默忽略。

快速修复规则 ​

textDocument/codeAction 会重新跑一遍分析,只保留与请求区间相交、且消息含 is not declared 的诊断:在标识符前插入 let ,标题为 Declare '<name>' with let。插入点取自诊断起点,并用文档内容校验该位置确实是同名标识符,避免诊断定位偏移时改错位置。

接线方式 ​

LanguageServer::handle 接收任意一段字节流,返回需要发回客户端的字节流(可能为多条消息,也可能为空)。调用方只需把读到的数据喂进去、把返回值写出去:

cpp
lsp::LanguageServer server;

std::string outbound = server.handle(bytesReadFromClient);
if (!outbound.empty())
    writeToClient(outbound);

内部会自行缓冲不完整的消息:一帧被拆成多次到达时不会丢数据,也不会错帧;单帧超过 64 MiB 或 JSON 解析失败时丢弃该帧,不会让缓冲区无限增长。

WARNING

消息编码使用 dump(-1, ' ', false, error_handler_t::replace)。脚本源码可能含非法 UTF-8(模糊测试已覆盖该路径),默认的 strict 处理器会抛异常,而本项目以 set_exceptions("none") 编译,异常即终止进程。新增序列化点时请沿用同样的参数。

符号收集规则 ​

analyze 在语义分析之后遍历 AST,因此 impl 块合并进目标类的方法、component 展开生成的节点都会被纳入:

来源符号类型归属(container)
classClass全局
类字段 / static 字段Field / StaticField类名
类方法 / static 方法Method / StaticMethod类名
traitInterface全局
trait 内的方法Methodtrait 名
impl 内的方法与关联常量Method / StaticMethod / Constantimpl 目标类型名
顶层 funcFunction全局
顶层 let / constVariable / Constant全局

detail 为可读签名,例如 size() -> int、add(x: int, y: int) -> int。

补全规则 ​

触发 . 时(成员补全):取 . 之前的标识符作为接收者。

  • this → 取光标前最近一次 class <Name> 声明的类
  • 已知类名 → 该类的静态成员
  • 带类型标注的变量(let deck: Deck = ...)→ 其标注类型的成员
  • 无法推断 → 返回空列表(不做"猜一个"式的误报)

其他位置(全局补全):全局符号 + 关键字 + 基础类型 + 内置函数(tr / score / entity / print / println / error)+ 表单类(CustomForm / MessageBox / PaginatedForm / ScriptForm)。

已知限制 ​

  • 局部变量(函数参数、块内 let)不进符号表,因此不参与补全与跳转
  • 不解析 import,跨文件符号需要调用方先把导入图展开后逐文档喂入
  • 每次请求都会重新跑一遍完整前端,未做增量分析;单文件脚本规模下开销可忽略
  • 诊断区间长度为 1 个字符(诊断引擎只记录起点,不记录词法单元长度)
  • 快速修复目前只覆盖「变量未声明」一类,且只插入 let,不推断初始值

LOICollectionA · 开箱即用的 LeviLamina 多功能插件集