🔢 前言
Hello~大家好,我是秋天的一阵风
你有没有好奇过?不管是传统的VS Code,还是Cursor 这类 AI 编辑器,都具备点一下函数名秒跳定义、输入代码自动补全、改代码实时报错的功能,哪怕是几百个文件的大项目,也丝滑不卡顿。
反观用普通文本编辑器 + grep 找代码,翻半天找不全、还一堆无关结果,完全不在一个档次。
其实这些代码编辑器的 "聪明劲儿",核心靠两个底层技术:AST(抽象语法树) 和 LSP(语言服务器协议) 。今天我尝试一次性讲透它们的原理、优势,还有 AI 编辑器背后的工作逻辑,希望看完你也能懂!
一、先搞懂:AST 是什么?代码的 "结构化骨架"
1. 通俗理解:AST 是代码的 "拆解说明书"
AST(Abstract Syntax Tree ) 全称抽象语法树,简单说就是:把我们手写的字符串代码,转换成机器可识别的结构化树形数据。
人写代码是写给人看的,是松散的文本;AST 是写给机器看的,是规整、有层级、有关联的结构化数据。
AST(抽象语法树),就是把代码拆成有逻辑的 "零件" ,搭成一棵结构化的树,让电脑彻底读懂代码:哪个是函数、哪个是变量、参数是什么、谁调用了谁、嵌套关系如何。
2. 实战例子:一行代码变 AST
简单写一段 JS 代码
javascript
// b.js(定义函数)
export function calculatePrice(price, discount) {
return price * (1 - discount);
}
// a.js(调用函数)
import { calculatePrice } from './b.js';
let total = calculatePrice(100, 0.2);
经过词法 + 语法分析后,生成的简化 AST 长这样:
yaml
Program(整个代码文件)
├─ ImportDeclaration(导入语句)
│ └─ ImportSpecifier(导入calculatePrice)
├─ VariableDeclaration(变量声明)
│ └─ CallExpression(函数调用)
│ └─ Identifier(函数名:calculatePrice)
└─ FunctionDeclaration(函数定义)
├─ id: Identifier(函数名:calculatePrice)
├─ params: [price, discount](参数)
└─ body: BlockStatement(函数体+返回逻辑)
3. AST的核心作用与局限
依托AST,机器能精准区分代码所有元素:函数定义、函数调用、变量、参数、循环、判断语句,彻底摆脱"瞎匹配"。
但AST有个致命问题:太底层、使用成本太高。
如果开发者每次跳转定义、查引用都要手动遍历AST树、解析节点,开发效率极低。而且不同语言、不同编辑器的AST结构各不相同,根本没法统一复用。
于是,LSP 应运而生,专门解决这个问题。
二、LSP 是什么?连接编辑器与 AST 的 "标准化桥梁"
1. LSP是什么?
LSP 全称语言服务器协议(Language Server Protocol),是微软 2016 年开源推出的编辑器与语言服务器通用通信标准,所有主流编辑器、AI 代码工具全部适配。
目前最新稳定版本为 3.17 (2022 年发布),完整规范见 LSP 3.17 官方规范,Github仓库地址为: GitHub 仓库 Releases
这里先理清两个核心角色:
- 客户端(Client):VS Code、Cursor 等所有代码编辑器
- 服务端(Server):本地语言服务器(JS 对应 tsserver、Python 对应 pyright、Go 对应 gopls)
通俗理解
想象你有一份很大的课题资料,需要一个研究助理帮你查阅:
- AST 就是你整理好的资料索引------每一段笔记的出处、关键词、引用关系都标注清楚了
- 语言服务器 就是研究助理,已经把所有资料读透并建立好了索引
- LSP 是你和助理之间的「标准提问方式」------规定好几种固定问法:"这个概念出自哪篇文献?"、"哪些地方引用了它?"、"它是什么意思?"
你不需要知道助理怎么查索引,只要按格式提问,助理就按格式回答。编辑器和语言服务器的关系也是如此:编辑器发标准请求,语言服务器查 AST 符号表,返回标准结果。
2. LSP完整工作流程
以最常用的 「Ctrl+点击跳转函数定义」 为例,完整流程如下:
- 初始化解析:打开项目,本地语言服务器启动,解析所有代码生成AST,构建全局符号表(记录所有函数、变量的位置、作用域、引用关系)
- 客户端发请求 :你点击函数调用处,编辑器通过
JSON-RPC发送标准请求:查询当前位置符号的定义 - 服务端处理:语言服务器直接查询内存中的符号表,无需重新解析代码
- 返回结果 :通过
JSON-RPC返回函数所在文件、行号、字符位置 - 编辑器渲染:编辑器根据返回结果,自动跳转到对应代码位置
3. 核心通信方式:JSON-RPC
3.1 先搞懂 RPC
RPC 全称 Remote Procedure Call(远程过程调用),核心思想就一句话:让一台程序调用另一台程序的函数,写法上和调用本地函数一样。
举个例子,你写 getUserById(1),看起来是普通函数调用,但 RPC 框架会帮你:
- 把函数名
getUserById和参数1序列化成一条消息 - 通过网络/管道发给远端程序
- 远端程序执行真正的函数,拿到结果
- 把结果序列化回来,返回给调用方
你不需要自己拼 HTTP 请求、解析响应,RPC 框架帮你把这些脏活干了。
3.2 JSON-RPC:RPC 的 JSON 版本
JSON-RPC 是 RPC 的一种极简实现------用 JSON 格式描述「调用哪个函数、传什么参数、返回什么结果」。规范很短,核心就三种消息类型(JSON-RPC 2.0 规范)。
| 类型 | 有没有 id |
谁发 | 对方回不回 | 一句话说明 |
|---|---|---|---|---|
| Request | 有 | 任意一方 | 必须回 Response | 「我问你一个问题,请回答」 |
| Response | 有(与 Request 一致) | 被问的一方 | --- | 「这是我的回答」 |
| Notification | 无 | 任意一方 | 不回复 | 「跟你说一声,不用回」 |
3.3 JSON-RPC vs HTTP:不是替代关系
很多人会混淆这两个,其实它们解决的是不同层面的问题:
| HTTP | JSON-RPC | |
|---|---|---|
| 核心思想 | 面向资源 ,用动词操作(GET /users、POST /orders) |
面向函数调用 (getUserById(1)) |
| 消息格式 | 请求行 + Header + Body | 纯 JSON(method + params + id) |
| 典型场景 | 浏览器访问网页、REST API | 程序间互相调用能力 |
一句话总结:HTTP 是「我访问一个地址,拿到一些数据」;JSON-RPC 是「我调用你那边的一个函数,拿到返回值」。
LSP 选 JSON-RPC 而不是 HTTP,是因为它描述的不是「资源操作」而是「函数调用」------textDocument/definition 就是一个函数,传入位置,返回定义地址。
3.4 LSP 里实际怎么用:一个完整例子
你在编辑器里点击 foo,背后发生了三件事:
json
// ① 客户端 → 服务端:Request(有 id,等回复)
{
"jsonrpc": "2.0",
"id": 1,
"method": "textDocument/definition",
"params": {
"textDocument": { "uri": "file:///a.js" },
"position": { "line": 1, "character": 2 }
}
}
// ② 服务端 → 客户端:Response(id 与 Request 对应)
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"uri": "file:///b.js",
"range": { "start": { "line": 0, "character": 16 }, "end": { "line": 0, "character": 19 } }
}
}
// ③ 客户端 → 服务端:Notification(无 id,不等回复)
// 「我打开了 a.js,你记一下」
{
"jsonrpc": "2.0",
"method": "textDocument/didOpen",
"params": {
"textDocument": { "uri": "file:///a.js", "languageId": "javascript", "version": 1, "text": "..." }
}
}
// 注意:没有 id,服务端不会回复
didOpen、didChange 也是 textDocument/* 方法,但它们是 Notification------客户端同步文档内容给 Server,不是「问一个问题」。
3.5 LSP 的传输层:Content-Length 头
JSON-RPC 规范本身不规定传输方式(可以用 TCP、WebSocket 等)。LSP 选择了 stdio (stdin/stdout 管道),并在 JSON 前加了一个 Content-Length 头,解决「怎么从字节流里切出一条完整消息」的问题(来自 LSP 官方规范):
text
Content-Length: 119\r\n
\r\n
{"jsonrpc":"2.0","id":1,"method":"textDocument/definition","params":{"textDocument":{"uri":"file:///a.js"}}}
119 就是后面 JSON 体按 UTF-8 编码后的字节长度 (ASCII 字符每个 1 字节,逐个数就行)。接收端先读到 Content-Length: 119,就知道接下来要读 119 个字节才是一条完整消息。
消息头与消息体之间用 \r\n\r\n 分隔。读端按 Content-Length 指定的字节数切包,不能按行读------因为 JSON 体内部可能有换行符,按行读会把一条消息切成多段。
实际代码中,客户端用子进程的 stdin.write() 发消息,从 stdout.on("data") 读回复,后续可以参考我们demo里的 lsp-io.js。
3.6 方法名:命名空间/动作
在上面3.4 里 method 的写法可概括为 命名空间/动作 :斜杠前是作用范围 ,斜杠后是具体动作。
| 完整 method | 命名空间 | 动作 | 典型消息类型 |
|---|---|---|---|
textDocument/definition |
某个已打开文件 | 查定义 | Request |
textDocument/hover |
同上 | 悬停说明 | Request |
textDocument/completion |
同上 | 代码补全 | Request |
textDocument/references |
同上 | 查找引用 | Request |
textDocument/rename |
同上 | 重命名符号 | Request |
textDocument/didOpen |
同上 | 文件已打开 | Notification |
textDocument/didChange |
同上 | 文件内容变更 | Notification |
textDocument/publishDiagnostics |
某文件 | 推送诊断(红线) | Notification(Server→Client) |
workspace/executeCommand |
整个工作区 | 执行自定义命令 | Request |
workspace/symbol |
整个工作区 | 搜索工作区符号 | Request |
window/showMessage |
编辑器 UI | 弹出提示 | Notification(Server→Client) |
前缀归纳(对应上表「命名空间」列):
| 方向 | 前缀 | 作用范围 |
|---|---|---|
| Client → Server | textDocument/ |
单文件(常含 uri、position) |
| Client → Server | workspace/ |
整个工作区 |
| Server → Client | window/ |
编辑器 UI |
| Server → Client | textDocument/ |
某文件的推送(如诊断) |
textDocument/* 多与「当前文件 + 光标」相关;
workspace/* 面向项目级;publish*、show* 多为 Server 主动推送。
连接建立与关闭还会用到不带斜杠的方法,如 initialize、shutdown。
3.7 从连接到使用
连接后按时间顺序发消息;进入「使用中」后,才是日常的智能编辑能力。
① 按阶段发送的 method
| 阶段 | method | 类型 | 作用 |
|---|---|---|---|
| 连接 | initialize |
Request | 交换 capabilities(Server 声明支持跳转、补全等) |
| 连接 | initialized |
Notification | 客户端就绪 |
| 使用中 | textDocument/didOpen、didChange |
Notification | 持续同步文件内容 |
| 使用中 | textDocument/definition、hover、completion 等 |
Request | 用户操作触发的智能能力 |
| 使用中 | textDocument/publishDiagnostics 等 |
Notification | Server 主动推送诊断等 |
| 关闭 | shutdown → exit |
Request → Notification | 收尾并退出 |
initialize 里须声明 definitionProvider: true,客户端才会发 textDocument/definition。
② Language Features(上表「使用中」的 Request 能力)
下列动作在规范中统称 Language Features,写法为 textDocument/<动作>,多数参数为 文档 URI + 光标 position:
| 分类 | 动作 | 返回(典型) | 说明 |
|---|---|---|---|
| 导航 | definition / declaration / references |
Location |
定义、声明、引用 |
| 导航 | typeDefinition / implementation |
Location |
类型定义、接口实现 |
| 阅读 | hover / completion / signatureHelp |
Hover / CompletionItem[] / SignatureHelp |
悬停、补全、参数提示 |
| 编辑 | rename / formatting / codeAction |
WorkspaceEdit / TextEdit[] / CodeAction[] |
重命名、格式化、快速修复 |
| 呈现 | documentSymbol / documentHighlight / codeLens / semanticTokens/full |
各类结构 | 大纲、高亮、透镜、语义着色 |
工作区级能力(如 workspace/executeCommand)使用 workspace/<动作> 命名,具体参数在 initialize 的 capabilities 里声明。
三、Grep文本检索 VS LSP语义检索
| 对比维度 | Grep(文本搜索) | LSP(语义检索) |
|---|---|---|
| 核心原理 | 纯字符串模糊匹配 | AST语法解析+符号表精准匹配 |
| 检索精度 | 极低,会匹配注释、字符串内的同名文本 | 100%精准,只匹配真实代码定义与引用 |
| 别名适配 | 不支持,无法识别import别名、间接调用 | 完全支持,可追踪所有间接引用、重命名调用 |
| 检索速度 | 千文件耗时分钟级,逐文件遍历 | 毫秒级,直接内存查表 |
| 适用场景 | 简单文本查找、注释检索 | 项目级代码溯源、重构、语法分析 |
举个最直观的例子:
我们导出函数并起别名调用:
js
// 原函数
function add(a, b) {
return a + b;
}
export const calcAdd = add;
// 其他文件别名调用
import { calcAdd as sum } from './test.js';
sum(3, 4);
Grep只能搜到「add」字符串,完全识别不到「calcAdd」「sum」和原函数的关联;
而LSP依托AST语义分析,能精准追踪到所有关联调用,没有任何遗漏。
四、为什么LSP检索能做到毫秒级极速?
很多人疑惑:项目几千上万个文件,每次检索如果都解析AST,肯定会卡顿,为什么LSP始终秒响应?
核心是LSP三大专属优化机制,也是现代编辑器的核心黑科技:
4.1 长进程常驻内存
语言服务器启动后不会立即销毁,会长期驻留后台内存。项目首次加载时,一次性解析所有文件、生成AST、构建全局符号表,后续所有查询都是内存查表(O(1)复杂度) ,无需重复解析代码。
4.2 增量更新机制
代码修改时,不会全项目重新解析。只会针对被修改的单个文件重新生成AST、更新对应符号表,极大节省性能。
4.3 文件依赖追踪
服务器会自动记录文件间的import/require依赖关系。如果B文件被修改,只会更新依赖B文件的相关节点,不会全局刷新,进一步提升响应速度。
简单总结:LSP服务器就是一个常驻内存、实时增量更新的代码语义数据库,查代码本质是查表,自然秒响应。
五、多语言项目如何处理?LSP隔离机制
日常开发大多是混合语言项目(JS+Python+Go等),LSP的处理逻辑非常清晰:一门语言,对应一个独立的本地语言服务器,进程完全隔离、互不干扰。
5.1 主流语言对应服务器
- JS/TS → tsserver(微软官方)
- Python → pyright
- Go → gopls(Google官方)
- Rust → rust-analyzer
- Java → jdtls
5.2 启停逻辑
- 打开对应语言文件,编辑器自动拉起专属语言服务器进程
- 多语言文件同时打开,多服务器并行运行、独立解析
- 关闭所有对应语言文件,进程闲置超时自动销毁,释放内存
这套机制完美解决了多语言项目解析冲突、资源占用过高的问题。
六、实战落地:完整可运行 LSP+AI 案例 Demo
前面讲解的 AST 语法解析、LSP 协议通信、语义检索、AI 增强能力,下面用一个完整的案例demo带大家来学习。大家可以在我的仓库里获取demo完整代码:github.com/FatMii/ai-l...
本次 Demo 是一套轻量化、可落地的 LSP 基础交互案例,完整实现了 LSP 协议核心的进程 IO 通信、文件同步、定义跳转、悬停提示、引用查找、代码补全功能,并接入小米 MIMO 大模型实现基础代码解释能力。
6.1 项目整体结构与运行前置说明
先整体梳理项目文件分工,所有底层支撑、功能实现、测试调用均对应以下文件,后续所有变量、方法、代码片段均可在该结构中溯源:
bash
ai-lsp-demo
├── package.json / tsconfig.json # 项目环境、依赖、编译配置
├── lsp-io.js # LSP底层通信底座(核心封装请求/通知/编解码)
├── server.ts # LSP服务端:全局变量+工具方法+五大功能实现
├── client.js # LSP客户端:模拟编辑器、提供测试工具方法
├── a.js / b.js # 跨文件测试源码(所有演示的代码载体)
└── .env # AI大模型密钥配置
6.2 LSP通信核心底座(lsp-io.js)
该文件是项目通用通信底层,封装LSP标准JSON-RPC消息编解码、本地语言服务器启停、进程间消息交互能力,所有LSP功能的客户端请求、服务端响应均基于此模块实现,是五大核心功能运行的前置基础。
js
import { spawn } from "node:child_process";
import path from "node:path";
// JSON-RPC消息编码(LSP标准格式)
function encodeMessage(obj) {
const body = JSON.stringify(obj);
return `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`;
}
// JSON-RPC消息解码(解析服务端返回数据流)
function decodeMessages(buffer, onMessage) {
let rest = buffer;
for (;;) {
const headerEnd = rest.indexOf("\r\n\r\n");
if (headerEnd < 0) break;
const match = rest.slice(0, headerEnd).toString().match(/Content-Length:\s*(\d+)/i);
if (!match) break;
const bodyLen = Number(match[1]);
if (rest.length < headerEnd + 4 + bodyLen) break;
onMessage(JSON.parse(rest.slice(headerEnd + 4, headerEnd + 4 + bodyLen).toString()));
rest = rest.slice(headerEnd + 4 + bodyLen);
}
return rest;
}
// 启动语言服务器,封装标准LSP通信能力
export function startLanguageServer(projectRoot) {
const proc = spawn("npx", ["tsx", "server.ts", "--stdio"], {
cwd: projectRoot,
stdio: ["pipe", "pipe", "inherit"],
shell: true,
});
let nextId = 0;
const pending = new Map();
let readBuffer = Buffer.alloc(0);
proc.stdout.on("data", (chunk) => {
readBuffer = decodeMessages(Buffer.concat([readBuffer, chunk]), (msg) => {
if (msg.id == null || !pending.has(msg.id)) return;
const { resolve } = pending.get(msg.id);
pending.delete(msg.id);
resolve(msg.result);
});
});
const send = (payload) => proc.stdin.write(encodeMessage(payload));
return {
fileUri(filename) { return `file:///${path.resolve(projectRoot, filename).replace(/\/g, "/")}`; },
request(method, params) {
const id = ++nextId;
return new Promise((resolve) => { pending.set(id, { resolve }); send({ jsonrpc: "2.0", id, method, params }); });
},
notify(method, params) { send({ jsonrpc: "2.0", method, params }); },
close() { proc.kill(); }
};
}
核心能力说明:该模块统一封装LSP标准通信逻辑,提供文件路径格式化、请求调用、消息通知、进程管理四大通用能力,所有后续功能的客户端交互均复用该底座,无需重复编写通信逻辑。
6.3 五大核心功能
功能一:跨文件定义跳转(definition)
功能作用 :对应编辑器 Ctrl+点击跳转 核心功能,精准定位函数、变量的原始定义位置,支持跨文件溯源,彻底规避传统文本搜索的误匹配问题。
核心原理 :LSP服务端启动后,预解析项目所有文件并基于AST记录代码符号的精准坐标。 接收客户端跳转请求后,直接读取预存的全局符号位置数据,基于AST语义精准匹配返回标准化Location结构,零错误匹配。
客户端完整调用源码(client.js) :模拟编辑器点击代码的操作,传入全局aText上下文,获取foo精准光标位置,发起标准definition协议请求,解析loc位置并打印结果。
js
/**
* 演示:跨文件定义跳转
* @param lsp 已初始化的LSP通信实例
* @param aText a.js完整文本上下文(全局传入)
*/
async function demoGoToDefinition(lsp, aText) {
console.log("【1】LSP:textDocument/definition 定义跳转\n");
// 获取当前点击的光标位置(工具方法)
const pos = fooPositionInA(aText);
// 向服务端发送标准LSP跳转请求
const result = await lsp.request("textDocument/definition", {
textDocument: { uri: lsp.fileUri("a.js") },
position: pos,
});
// 兼容LSP返回数组/单个对象两种格式
const loc = Array.isArray(result) ? result[0] : result;
// 解析目标文件名与行号(行号+1对齐人类阅读习惯)
const file = loc.uri.split("/").pop();
console.log(` 跳转结果: ${file} 第 ${loc.range.start.line + 1} 行`);
}
客户端变量释义
lsp:lsp-io.js初始化后的通信实例,自带request/notify请求能力aText:全局读取的a.js完整代码文本,提供上下文pos:当前光标精准坐标,模拟用户点击位置result:服务端返回的原始跳转数据loc:格式化后的标准Location位置对象,存储最终跳转信息
服务端完整源码(server.ts)
js
// 导入LSP官方标准类型
import { DefinitionParams, Location } from "vscode-languageserver/node";
/**
* 处理用户「点击跳转定义」请求
* @param params LSP标准跳转请求参数
* @returns 标准化代码位置对象
*/
function goToDefinition(params: DefinitionParams) {
// 仅针对 a.js 文件的符号跳转生效
if (!params.textDocument.uri.includes("a.js")) return null;
// 调用LSP原生Location.create,生成标准跳转位置
// 参数1:目标文件URI,参数2:目标代码范围
return Location.create(bJsUri, fooRangeInB);
}
全量变量释义
DefinitionParams:LSP官方跳转参数类型,自带当前文件URI、光标位置等信息params.textDocument.uri:客户端当前操作的文件路径bJsUri:上文全局预定义的b.js标准文件路径fooRangeInB:上文全局预定义的foo函数代码范围Location.create:LSP标准方法,生成编辑器可识别的跳转位置
js
// 导入LSP官方标准类型
import { DefinitionParams, Location } from "vscode-languageserver/node";
/**
* 处理用户「点击跳转定义」请求
* @param params LSP标准跳转请求参数
* @returns 标准化代码位置对象
*/
function goToDefinition(params: DefinitionParams) {
// 仅针对 a.js 文件的符号跳转生效
if (!params.textDocument.uri.includes("a.js")) return null;
// 调用LSP原生Location.create,生成标准跳转位置
// 参数1:目标文件URI,参数2:目标代码范围
return Location.create(bJsUri, fooRangeInB);
}
功能二:代码悬停提示(hover)
功能作用 :日常开发高频功能,鼠标悬浮在代码符号上,自动展示函数归属、定义文件、源码片段,大幅提升陌生代码阅读、溯源效率。
核心原理 :服务端实时监听客户端鼠标悬浮坐标,匹配当前AST语法符号,调取预存的符号元信息,格式化生成Markdown结构化提示文案,返回客户端渲染展示。
客户端完整调用源码(client.js)
js
/**
* 演示:代码鼠标悬停提示
* @param lsp LSP通信实例
* @param aText a.js完整代码上下文
*/
async function demoHover(lsp, aText) {
console.log("【2】LSP:textDocument/hover 代码悬停\n");
// 获取悬浮光标位置
const pos = fooPositionInA(aText);
// 发起悬浮查询请求
const result = await lsp.request("textDocument/hover", {
textDocument: { uri: lsp.fileUri("a.js") },
position: pos,
});
// 兼容字符串/对象两种返回格式,兜底默认无内容
const content = result?.contents;
const text = typeof content === "string" ? content : content?.value ?? "(无)";
// 格式化缩进输出
console.log(text.replace(/^/gm, " "));
}
客户端核心逻辑说明:模拟鼠标悬浮操作,定位目标代码光标位置,向服务端发起悬停查询请求,接收并格式化渲染结构化提示信息。
服务端完整源码(server.ts)
js
import { HoverParams, Hover } from "vscode-languageserver/node";
/**
* 处理鼠标悬浮提示请求
* @param params LSP标准悬浮参数
* @returns 结构化悬浮提示文案
*/
function getHover(params: HoverParams): Hover | null {
// 仅对a.js文件生效
if (!params.textDocument.uri.includes("a.js")) return null;
// 返回LSP标准悬浮结构
return {
contents: {
kind: "markdown",
value: [
"**foo** --- 函数",
"",
"定义:`b.js` 第 1 行",
"",
"```javascript",
"export function foo() { ... }",
"```",
].join("\n"),
},
};
}
服务端变量释义
HoverParams:LSP悬浮请求标准参数类型Hover:LSP悬浮返回值标准结构类型contents:悬浮展示的内容主体,支持markdown富文本格式
功能三:符号全局引用查找(references)
功能作用 :一键检索函数、变量在项目中的所有定义位置、调用位置,自动过滤注释、字符串中的同名无效文本,实现精准全局溯源,是代码重构、迭代排坑的核心能力。
核心原理 :依托AST语法树遍历统计全局符号的引用关系,精准区分「纯文本字符」和「真实语法调用」,从根源解决传统Grep搜索的漏查、误查问题,批量返回所有标准Location位置对象。
客户端完整调用源码+释义(client.js)
javascript
/**
* 演示:全局引用查找
* @param lsp LSP通信实例
* @param aText a.js代码上下文
*/
async function demoReferences(lsp, aText) {
console.log("【3】LSP:textDocument/references 引用查找\n");
const pos = fooPositionInA(aText);
// includeDeclaration: true 代表「同时包含定义位置+调用位置」
const result = await lsp.request("textDocument/references", {
textDocument: { uri: lsp.fileUri("a.js") },
position: pos,
context: { includeDeclaration: true },
});
// 兜底空数组,遍历所有引用位置输出
const list = result ?? [];
for (const loc of list) {
const file = loc.uri.split("/").pop();
const { line } = loc.range.start;
console.log(` · ${file} 第 ${line + 1} 行`);
}
}
客户端核心逻辑说明:定位目标代码光标位置,开启定义包含配置,发起全局引用查询,批量遍历输出代码所有关联位置。
服务端完整源码(server.ts)
php
import { ReferenceParams, Location } from "vscode-languageserver/node";
/**
* 全局引用查找
* @param params LSP引用查询参数
* @returns 所有关联代码位置数组
*/
function getReferences(params: ReferenceParams): Location[] {
// 仅处理a.js文件内的符号查询
if (!params.textDocument.uri.includes("a.js")) return [];
// 返回两组位置:1.当前文件调用位置 2.目标文件定义位置
return [
// a.js 中 foo 调用位置
Location.create(aJsUri, {
start: { line: 1, character: 0 },
end: { line: 1, character: 3 },
}),
// b.js 中 foo 定义位置
Location.create(bJsUri, fooRangeInB),
];
}
功能四:静态+AI 双模式代码补全(completion)
功能作用 :输入代码前缀自动匹配项目内可用变量、函数, 同时叠加小米MIMO大模型能力,实现基于上下文的智能代码片段补全,兼顾精准度与场景智能化。
核心原理:本地预存项目所有导出符号,实现高精度静态匹配补全;实时读取当前文件完整上下文,调用大模型生成适配场景的个性化代码片段,双模式结合提升补全体验。
客户端完整调用源码+释义(client.js)
js
async function demoCompletion(lsp, aText) {
console.log("【4】LSP:textDocument/completion 智能补全\n");
// 模拟用户实时输入代码
const draft = `${aText.trimEnd()}\nconst result = fo`;
// 上报文件内容变更
lsp.notify("textDocument/didChange", {
textDocument: { uri: lsp.fileUri("a.js"), version: 2 },
contentChanges: [{ text: draft }],
});
// 定位最新输入的光标位置
const lines = draft.split(/\r?\n/);
const result = await lsp.request("textDocument/completion", {
textDocument: { uri: lsp.fileUri("a.js") },
position: { line: lines.length - 1, character: lines.at(-1).length },
});
// 遍历输出所有补全结果
(result?.items || []).forEach(item => {
console.log(` 补全结果:${item.label}`);
});
}
客户端核心逻辑说明:模拟用户实时输入代码、同步上报文件内容变更,精准定位光标位置触发补全请求,最终遍历输出静态+AI双重补全结果。
服务端完整源码
js
import {
CompletionParams,
CompletionItem,
CompletionItemKind,
TextDocument,
Position
} from "vscode-languageserver/node";
import { TextDocuments } from "vscode-languageserver";
// 承接全局AI实例、文档管理器
let mimo: any;
const docs = new TextDocuments();
/**
* 双模式代码补全
* @param params 补全请求参数
* @returns 静态+AI补全列表
*/
async function getCompletions(params: CompletionParams) {
const doc = docs.get(params.textDocument.uri);
// 获取光标前输入前缀
const prefix = wordBeforeCursor(doc, params.position);
const items: CompletionItem[] = [];
// 1. 本地静态符号补全(精准无幻觉)
for (const sym of exportedFromB) {
if (!prefix || sym.name.startsWith(prefix)) {
items.push({
label: sym.name,
kind: sym.kind,
detail: sym.detail,
insertText: sym.name,
});
}
}
// 2. MIMO AI 智能补全(场景化增强)
if (mimo && process.env.AI_COMPLETION !== "false") {
const raw = await askMimo(
"只输出一行可运行JS代码,无需解释",
`文件上下文:${doc.getText()}\n输入前缀:${prefix}`,
60
);
// 清洗AI返回内容,去除代码块标记
const snippet = raw?.trim().replace(/^```[\w]*\n?|```$/g, "").split("\n")[0].trim() || "";
if (snippet) items.push({ label: `🤖${snippet}`, kind: CompletionItemKind.Snippet, insertText: snippet });
}
return { isIncomplete: false, items };
}
功能五:AI 代码语义解释(executeCommand)
功能作用:AI编辑器核心增强能力,选中任意代码片段,即可一键生成通俗易懂的中文逻辑解释,快速读懂陌生代码、理清业务逻辑、降低阅读成本。
核心原理:依托LSP获取精准、完整的代码上下文,规避手动文本截取不全、错乱的问题,调用小米MIMO大模型做语义解析,输出简洁易懂的代码逻辑说明。
客户端完整调用源码(client.js)
js
/**
* 演示:AI代码语义解释
* @param lsp LSP通信实例
* @param aText 待解释的完整代码片段
*/
async function demoExplainCode(lsp, aText) {
console.log("【5】LSP+AI:代码智能解释\n");
// 发起自定义LSP指令,传递选中代码
const explanation = await lsp.request("workspace/executeCommand", {
command: "ai.explain",
arguments: [aText.trim()],
});
// 格式化输出AI解释结果
console.log(" AI解释结果:\n", String(explanation).replace(/^/gm, " "));
}
前置底层依赖 + 服务端完整源码(server.ts)
js
/**
* 封装MIMO大模型请求工具方法
* @param system 系统提示词
* @param user 用户输入上下文
* @param maxTokens 最大生成长度
* @returns AI解析结果
*/
async function askMimo(system: string, user: string, maxTokens: number): Promise<string | null> {
if (!mimo) return null;
try {
const res = await mimo.chat.completions.create({
messages: [
{ role: "system", content: system },
{ role: "user", content: user }
],
max_tokens: maxTokens
});
return res.choices?.[0]?.message?.content || null;
} catch (e) {
return null;
}
}
js
import { ExecuteCommandParams } from "vscode-languageserver/node";
/**
* 自定义LSP指令:AI代码解释
* @param params 自定义指令参数(携带选中代码片段)
* @returns 通俗解释文本
*/
async function explainCode(params: ExecuteCommandParams): Promise<string> {
// 匹配自定义AI指令
if (params.command !== "ai.explain") return "未知指令";
// 获取客户端传递的选中代码片段
const code = String(params.arguments?.[0] ?? "");
// 校验AI实例是否初始化
if (!mimo) return "未配置AI密钥,无法使用AI解释功能";
// 调用MIMO模型解释代码逻辑
const text = await askMimo(
"用2-3句通俗中文解释JS代码",
`解释这段代码:\n${code}`,
300
);
return text ?? "解释失败";
}
js
/**
* 封装MIMO大模型请求工具方法
* @param system 系统提示词
* @param user 用户输入上下文
* @param maxTokens 最大生成长度
* @returns AI解析结果
*/
async function askMimo(system: string, user: string, maxTokens: number): Promise<string | null> {
if (!mimo) return null;
try {
const res = await mimo.chat.completions.create({
messages: [
{ role: "system", content: system },
{ role: "user", content: user }
],
max_tokens: maxTokens
});
return res.choices?.[0]?.message?.content || null;
} catch (e) {
return null;
}
}
6.4 项目运行方式
项目配置极简,克隆本地后安装依赖,一键即可运行全部演示,控制台会逐行输出五大功能的执行结果,方便大家对照调试、验证效果:
bash
# 克隆项目
git clone https://github.com/FatMii/ai-lsp-demo
cd ai-lsp-demo
# 安装依赖
npm install
# 一键执行所有LSP+AI演示
npm run demo
如需开启AI智能补全、代码解释能力,只需在项目.env文件中填入个人MIMO密钥即可。