Inline Chat 与 IDE 集成:从选中代码到 Diff 应用
这一篇专门看 Inline Chat 这一条链路:选中一段代码,输入一句自然语言指令,让 IDE 给出一份可 review 的 Diff,并且安全地应用到工作区。
能力边界:Inline Chat 要解决什么,不解决什么?
先限定一下 Inline Chat 在企业级 IDE 里的定位:
主要解决:
- 局部重写:改善可读性、改错误处理、重构一个函数体;
- 局部转换:从 callback 写法改成 async/await,或者从 Promise 改成 Observable;
- 局部解释与注释:在不改代码的前提下生成解释或注释草稿。
刻意不解决(或者至少不在这一条链路解决):
- 大范围的跨文件重构(这更适合走“多文件改写/重构”管线);
- 有全局副作用的操作(例如批量删改接口、改权限逻辑等)。
这样可以保证 Inline Chat 的“职责小而清晰”:
只在“用户明确选中的局部范围内”出手,做“一小块、可见、可回滚”的改动。
从 IDE 出发:如何打包一次 Inline Chat 请求?
当用户触发 Inline Chat 时,IDE 侧需要准备的东西至少包括:
- 当前文件路径;
- 当前文件内容(或 hash + 增量 diff);
- 用户选区的起止位置(行列);
- 光标所在的函数/方法(可选,用于上下文判断);
- 用户输入的自然语言指令。
一个典型的请求 payload 可以长这样:
1{
2 "type": "inline_edit",
3 "filePath": "src/services/user.ts",
4 "fileContent": "...",
5 "selection": {
6 "start": { "line": 42, "character": 2 },
7 "end": { "line": 68, "character": 4 }
8 },
9 "cursorContext": {
10 "functionName": "createUser",
11 "surroundingLines": 10
12 },
13 "instruction": "把这段改成 async/await 风格,并保留同样的错误处理逻辑"
14}
IDE 的责任是:准确、稳定地描述“用户这次想动的是哪一段代码,处在什么上下文下”。
编排层处理:确定上下文与输出格式
AI 编排层拿到这个请求后,大概做几件事:
任务识别
type已经标明是inline_edit,自然语言指令再补充具体意图(重构/翻译/加日志等)。
上下文构建
- 必选上下文:
- 选中代码本身;
- 所在函数/方法的完整定义;
- 这一文件头部的 import / type 定义。
- 可选上下文(视 token 预算而定):
- 同一文件中与之强相关的辅助函数;
- 最近一次相关诊断(例如这段代码有 type error)。
- 必选上下文:
输出约束
- 要求模型只修改选中范围内的代码,不跨越边界;
- 明确输出为统一的 patch 格式,例如:
1@@ file: src/services/user.ts @@
2@@ range: 42,68 @@
3- 原始行...
4+ 新的行...
或者使用类 unified diff 的约定,但至少要包括:
- 目标文件路径;
- 修改范围(行号/偏移);
- 前后内容。
这样 IDE 在解析时就有了明确的锚点。
Prompt 结构:给模型的输入长什么样?
为了让模型在“理解上下文 + 精准重写”之间取得平衡,可以用类似这样的 prompt 结构:
1系统提示:
2你是一个 IDE 助手。用户选中了一段代码,并给出了修改指令。
3请只在选中范围内进行改写,保持接口签名和外部行为不变。
4输出统一使用 patch 格式,不要额外解释。
5
6上下文:
7// File: src/services/user.ts
8// Imports:
9...
10
11// Function: createUser
12function createUser(...) {
13 ...
14}
15
16选中代码:
17<<SELECTED_CODE>>
18
19用户指令:
20把这段改成 async/await 风格,并保留同样的错误处理逻辑。
关键点在于:
- 限定修改范围:避免模型擅自改动 import 或其它函数;
- 保持接口不变:函数签名和调用方式尽量不要乱动;
- 输出只给 patch:方便后续用机器解析,而不是再从自然语言里“抠代码”。
Diff 解析与应用:如何保证“不乱改”?
IDE 侧接到 patch 之后,一般会走这样一个管线:
解析 patch
- 确认目标文件路径与当前文件一致;
- 检查 patch 中声明的行号范围和当前文件版本是否匹配(避免“基于旧版本打的 patch”)。
Dry-run / 预应用
- 在内存里把 patch 应用到文件文本上,得到“候选新版本”;
- 通过本地 parser 或 LSP 对新版本做一次语法快速检查。
Diff 预览 UI
- 高亮展示前后差异,支持:
- 一键接受全部;
- 只接受部分 hunk;
- 完全拒绝。
- 高亮展示前后差异,支持:
真正写回工作区
- 用户确认后,才把修改应用到真实 buffer 里;
- 应用后自动触发 format / organize imports / diagnostics。
这里可以顺带回答一个问题:“如何保证 Diff 安全应用?”
在 Inline Chat 这条链路里,一个相对务实的做法是:
- 补丁永远先在内存里试跑 + 语法检查;
- 永远通过 diff 视图让用户看清“要改什么”;
- 永远尊重选区边界,不跨范围写。
与多文件重构的边界:Inline Chat 何时应该“跳”到另一条管线?
当用户指令明显涉及多文件/全局行为时,比如:
- “把所有调用
foo的地方改成 async 风格”; - “把这个 DTO 增加一个字段,并串起所有调用链”;
- “把这一层 API 的错误处理统一收敛到一个中间件里”;
Inline Chat 这条链路就不太合适了,应该:
- 在编排层识别出“这是多文件/跨作用域重构”;
- 把请求转交给“多文件改写/重构”那条管线:
- 先用 LSP/索引找影响面;
- 再构造跨文件的 patch 列表和对应 UI。
这样可以保持两条能力的内聚性:
- Inline Chat:专注于局部、可视、可回滚的小改动;
- 多文件重构:走一条更重的 pipeline,有更多安全网和确认步骤。
小结:一条“从选中到 Diff 应用”的清晰链路
站在 IDE 工程师视角,Inline Chat 这一块如果设计成:
- IDE 只负责:准确标记选区 + 渲染 diff + 写回 buffer;
- 编排层负责:任务识别、上下文构建、prompt 与输出格式;
- LSP/语法分析负责:改前/改后的事实校验;
那它就能在 “帮用户省手工活” 和 “不乱改、不失控” 之间找到一个相对舒服的平衡点。