✨ 代码秒跳转、自动补全?全靠 LSP 和 AST!

🔢 前言

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+点击跳转函数定义」 为例,完整流程如下:

  1. 初始化解析:打开项目,本地语言服务器启动,解析所有代码生成AST,构建全局符号表(记录所有函数、变量的位置、作用域、引用关系)
  2. 客户端发请求 :你点击函数调用处,编辑器通过JSON-RPC发送标准请求:查询当前位置符号的定义
  3. 服务端处理:语言服务器直接查询内存中的符号表,无需重新解析代码
  4. 返回结果 :通过JSON-RPC返回函数所在文件、行号、字符位置
  5. 编辑器渲染:编辑器根据返回结果,自动跳转到对应代码位置

3. 核心通信方式:JSON-RPC

3.1 先搞懂 RPC

RPC 全称 Remote Procedure Call(远程过程调用),核心思想就一句话:让一台程序调用另一台程序的函数,写法上和调用本地函数一样

举个例子,你写 getUserById(1),看起来是普通函数调用,但 RPC 框架会帮你:

  1. 把函数名 getUserById 和参数 1 序列化成一条消息
  2. 通过网络/管道发给远端程序
  3. 远端程序执行真正的函数,拿到结果
  4. 把结果序列化回来,返回给调用方

你不需要自己拼 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 /usersPOST /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,服务端不会回复

didOpendidChange 也是 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)

前缀归纳(对应上表「命名空间」列):

flowchart LR C[客户端] -->|Request / Notification| S[语言服务] S -->|Notification 推送| C
方向 前缀 作用范围
Client → Server textDocument/ 单文件(常含 uriposition
Client → Server workspace/ 整个工作区
Server → Client window/ 编辑器 UI
Server → Client textDocument/ 某文件的推送(如诊断)

textDocument/* 多与「当前文件 + 光标」相关;

workspace/* 面向项目级;publish*show* 多为 Server 主动推送。

连接建立与关闭还会用到不带斜杠的方法,如 initializeshutdown

3.7 从连接到使用

连接后按时间顺序发消息;进入「使用中」后,才是日常的智能编辑能力。

① 按阶段发送的 method

阶段 method 类型 作用
连接 initialize Request 交换 capabilities(Server 声明支持跳转、补全等)
连接 initialized Notification 客户端就绪
使用中 textDocument/didOpendidChange Notification 持续同步文件内容
使用中 textDocument/definitionhovercompletion Request 用户操作触发的智能能力
使用中 textDocument/publishDiagnostics Notification Server 主动推送诊断等
关闭 shutdownexit 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/<动作> 命名,具体参数在 initializecapabilities 里声明。

三、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 启停逻辑

  1. 打开对应语言文件,编辑器自动拉起专属语言服务器进程
  2. 多语言文件同时打开,多服务器并行运行、独立解析
  3. 关闭所有对应语言文件,进程闲置超时自动销毁,释放内存

这套机制完美解决了多语言项目解析冲突、资源占用过高的问题。

六、实战落地:完整可运行 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密钥即可。

相关推荐
沉默王二1 小时前
又一个神级 Codex Skill 诞生了!
agent·ai编程
ThatMonth1 小时前
Chroma 向量数据库使用教程
ai编程
程序员小淞1 小时前
写一个行政区划下拉选组件(异步+搜索)
前端
用户298698530141 小时前
Java 中的 HTML 解析:从文件读取、URL 抓取到数据提取
java·后端
星栈1 小时前
用 Rust + Makepad 做一个 JSON 查看器:从零到能用的全过程
前端·rust
Dark-Source1 小时前
Oh-My-Pi (omp) 入门指导手册
ai编程
yijianace1 小时前
Python爬虫实战:分页爬取 + 详情页采集 + CSV存储
前端·爬虫·python
咖啡星人k1 小时前
从 Vibe Coding 到专业开发:MonkeyCode 如何重新定义AI编程工作流
人工智能·ai编程·monkeycode
AskHarries1 小时前
ZJF.AI:简单、稳定、免费的图片托管与外链分享平台
后端