Inline Chat 与 IDE 集成:从选中代码到 Diff 应用

这一篇专门看 Inline Chat 这一条链路:选中一段代码,输入一句自然语言指令,让 IDE 给出一份可 review 的 Diff,并且安全地应用到工作区。

先限定一下 Inline Chat 在企业级 IDE 里的定位:

  • 主要解决

    • 局部重写:改善可读性、改错误处理、重构一个函数体;
    • 局部转换:从 callback 写法改成 async/await,或者从 Promise 改成 Observable;
    • 局部解释与注释:在不改代码的前提下生成解释或注释草稿。
  • 刻意不解决(或者至少不在这一条链路解决)

    • 大范围的跨文件重构(这更适合走“多文件改写/重构”管线);
    • 有全局副作用的操作(例如批量删改接口、改权限逻辑等)。

这样可以保证 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 编排层拿到这个请求后,大概做几件事:

  1. 任务识别

    • type 已经标明是 inline_edit,自然语言指令再补充具体意图(重构/翻译/加日志等)。
  2. 上下文构建

    • 必选上下文:
      • 选中代码本身;
      • 所在函数/方法的完整定义;
      • 这一文件头部的 import / type 定义。
    • 可选上下文(视 token 预算而定):
      • 同一文件中与之强相关的辅助函数;
      • 最近一次相关诊断(例如这段代码有 type error)。
  3. 输出约束

    • 要求模型只修改选中范围内的代码,不跨越边界;
    • 明确输出为统一的 patch 格式,例如:
1@@ file: src/services/user.ts @@
2@@ range: 42,68 @@
3- 原始行...
4+ 新的行...

或者使用类 unified diff 的约定,但至少要包括:

  • 目标文件路径;
  • 修改范围(行号/偏移);
  • 前后内容。

这样 IDE 在解析时就有了明确的锚点。

为了让模型在“理解上下文 + 精准重写”之间取得平衡,可以用类似这样的 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:方便后续用机器解析,而不是再从自然语言里“抠代码”。

IDE 侧接到 patch 之后,一般会走这样一个管线:

  1. 解析 patch

    • 确认目标文件路径与当前文件一致;
    • 检查 patch 中声明的行号范围和当前文件版本是否匹配(避免“基于旧版本打的 patch”)。
  2. Dry-run / 预应用

    • 在内存里把 patch 应用到文件文本上,得到“候选新版本”;
    • 通过本地 parser 或 LSP 对新版本做一次语法快速检查。
  3. Diff 预览 UI

    • 高亮展示前后差异,支持:
      • 一键接受全部;
      • 只接受部分 hunk;
      • 完全拒绝。
  4. 真正写回工作区

    • 用户确认后,才把修改应用到真实 buffer 里;
    • 应用后自动触发 format / organize imports / diagnostics。

这里可以顺带回答一个问题:“如何保证 Diff 安全应用?”
在 Inline Chat 这条链路里,一个相对务实的做法是:

  • 补丁永远先在内存里试跑 + 语法检查;
  • 永远通过 diff 视图让用户看清“要改什么”;
  • 永远尊重选区边界,不跨范围写。

当用户指令明显涉及多文件/全局行为时,比如:

  • “把所有调用 foo 的地方改成 async 风格”;
  • “把这个 DTO 增加一个字段,并串起所有调用链”;
  • “把这一层 API 的错误处理统一收敛到一个中间件里”;

Inline Chat 这条链路就不太合适了,应该:

  • 在编排层识别出“这是多文件/跨作用域重构”;
  • 把请求转交给“多文件改写/重构”那条管线:
    • 先用 LSP/索引找影响面;
    • 再构造跨文件的 patch 列表和对应 UI。

这样可以保持两条能力的内聚性:

  • Inline Chat:专注于局部、可视、可回滚的小改动
  • 多文件重构:走一条更重的 pipeline,有更多安全网和确认步骤。

站在 IDE 工程师视角,Inline Chat 这一块如果设计成:

  • IDE 只负责:准确标记选区 + 渲染 diff + 写回 buffer;
  • 编排层负责:任务识别、上下文构建、prompt 与输出格式;
  • LSP/语法分析负责:改前/改后的事实校验;

那它就能在 “帮用户省手工活”“不乱改、不失控” 之间找到一个相对舒服的平衡点。