Skip to content
全文约 2387 字 预计阅读 7.5 分钟

LCUI 语言服务 ​

NOTE

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

定位 ​

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

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

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

仓库另提供可选的 LspServer:默认关闭,在配置 "LanguageServer": { "enabled": true, "port": 9257 } 时监听 127.0.0.1,用 TCP 承载同样的 JSON-RPC 帧。核心与传输层分离,既能让插件按需提供编辑器服务,也能让 gtest 直接驱动语言核心(tests/common/frontend/LspTest.cpp),无需进程间通信。

模块结构 ​

文件职责
Protocol.h / .cppLSP 数据类型(Position / Range / Diagnostic / CompletionItem / Hover / Location / SemanticTokens / InlayHint)、JSON 序列化、JSON-RPC 消息构造、Content-Length 分帧与解帧
Analysis.h / .cpp对一段脚本文本跑完整前端,输出 DocumentAnalysis(诊断 + 符号 + 导入符号 + 内联提示)
LanguageServer.h / .cpp文档同步(增量与全量)、请求分发、能力声明,以及补全 / 悬停 / 跳转等纯函数
Server.h / .cpp可选的回环 TCP 传输层,内部持有 LanguageServer 与 asio 事件循环

能力清单 ​

LSP 方法行为
initialize声明 textDocumentSync: 2(增量同步)、补全触发字符 . 与 :、悬停、定义、类型定义、引用、文档高亮、重命名、文档符号、签名帮助、语义令牌(full)、内联提示、格式化与快速修复
textDocument/didOpen记录文档,立即推送 publishDiagnostics
textDocument/didChange按 contentChanges 顺序应用编辑:带 range 的条目按行/列替换,不带 range 的条目整体替换;随后重新推送诊断
textDocument/didClose移除文档并清空该文档的诊断
textDocument/completion见「补全规则」
textDocument/hover返回光标处符号的声明签名、原生成员签名或命名空间说明
textDocument/definition / declaration返回声明处的 Location,跨文件时带导入文档的 URI
textDocument/typeDefinition由变量 / 字段的类型标注或推断结果跳转到类、枚举或 trait 声明
textDocument/references返回解析后属于同一符号的全部引用
textDocument/documentHighlight标记同一符号的读 / 写出现位置(2 读,3 写)
textDocument/rename返回与目标符号解析结果一致的全部 TextEdit;调用方可用 WorkspaceEdit 包装
textDocument/documentSymbol返回非局部符号的扁平列表
textDocument/signatureHelp见「签名帮助规则」
textDocument/semanticTokens/full返回整篇文档的语义令牌;full/delta 当前按全量结果返回
textDocument/inlayHint返回请求区间内的类型提示
textDocument/formatting基于 {} / [] 嵌套重排缩进,返回整篇替换编辑
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 展开生成的节点都会被纳入。符号同时记录声明偏移、作用域起止与 local 标记,供补全、跳转和解析校验使用:

来源符号类型归属(container)局部
classClass全局否
类字段 / static 字段Field / StaticField类名否
类方法 / static 方法Method / StaticMethod类名否
traitInterface全局否
trait 内的方法Methodtrait 名否
impl 内的方法与关联常量Method / StaticMethod / Constantimpl 目标类型名否
enumEnum全局否
枚举变体EnumMember枚举名否
componentComponent全局否
component 参数Parametercomponent 名是
importModule全局否
顶层 funcFunction全局否
顶层 let / constVariable / Constant全局否
函数 / 方法 / lambda 参数Parameter所属函数或类是
块内 let、for-in 变量、match 绑定、if let / while let 绑定Variable全局是

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

导入解析 ​

ImportResolver 把 import 路径映射为 { uri, text }。LanguageServer 在收到 didOpen 后自动构建解析器:先按导入文件的 URI 做相对解析,再在当前打开的文档中按完整路径或后缀匹配,因此只要依赖文件已经打开,跨文件补全、悬停、定义、引用与重命名都能工作。

  • 解析深度上限为 8,出现循环导入时按读取栈去重
  • 仅导入声明的符号时过滤非目标符号;import "path"; 导入该文件的全部导出
  • 解析不到的文件被静默跳过,诊断仍以当前文档为准
  • 纯函数接口可传入自定义 ImportResolver,由调用方从磁盘或虚拟文件系统读取

补全规则 ​

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

  • this → 取光标前最近一次 class <Name> 声明的类
  • 已知类名 → 该类的静态成员
  • 变量 / 字段 / 参数 → 其标注类型,或在 new 表达式、参数类型、返回值类型中推断出的类
  • 枚举名 → 该枚举的变体(如 Suit.He)
  • 原生类:String / Array / Map 等通过 ABI 反射列出字段、方法与签名;字符串与数组的接收者也会映射到对应原生类
  • std::、math::、mc::、GUIManager::、storage::、playerdata:: 等命名空间 → 已注册函数的重载列表
  • 无法推断 → 返回空列表(不做"猜一个"式的误报)

其他位置(全局补全):当前作用域可达的局部符号 + 导入符号 + 全局符号 + 枚举变体 + 关键字 + 基础类型 + 全部已注册原生函数 / 宏 / 类。

签名帮助规则 ​

signatureHelpAt 从光标向前匹配最内层未闭合的 (,识别 命名空间::函数、接收者.方法、类名(...)、枚举变体与宏,并从以下来源收集签名:

  • 当前文档与导入文档中的方法 / 函数声明
  • ABI 反射得到的原生函数重载与方法重载
  • 原生类构造器
  • 枚举变体的载荷签名
  • 宏注册表

activeParameter 按顶层逗号计数,嵌套的 () / [] / {} 不计入。找不到签名时返回 null。

语义令牌与内联提示 ​

语义令牌的图例顺序为:

text
namespace, type, class, enum, interface, struct, typeParameter, parameter,
variable, property, enumMember, event, function, method, macro, keyword,
modifier, comment, string, number, regexp, operator

令牌长度取自真实词法单元:标识符与字符串覆盖完整源码片段,跨行字符串与块注释按行拆分;注释单独扫描补充。文档内的声明符号优先于启发式分类,因此局部变量、参数、枚举、组件与导入模块都能得到准确类型。

内联提示目前只生成类型提示(kind 1):未显式标注类型的函数参数与局部 let 在名称后显示 : 类型。类型来自语义分析器的推断;unknown 与 void 不生成提示。

已知限制 ​

  • 每次请求都会重新跑一遍完整前端,未做增量解析;增量同步只减少文本传输量,单文件脚本规模下开销可忽略
  • 诊断区间优先扩展为完整标识符或字符串,其他位置仍可能只有 1 个字符
  • 快速修复目前只覆盖「变量未声明」一类,且只插入 let,不推断初始值
  • 语义令牌只支持全量请求;full/delta 不返回增量编辑
  • 跨文件解析依赖调用方提供 ImportResolver;内置 TCP 服务只解析已打开的文档

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