Skip to content

fix(vscode): clamp open file positions#5711

Merged
wenshao merged 1 commit into
QwenLM:mainfrom
tt-a1i:fix/vscode-openfile-zero-position
Jun 23, 2026
Merged

fix(vscode): clamp open file positions#5711
wenshao merged 1 commit into
QwenLM:mainfrom
tt-a1i:fix/vscode-openfile-zero-position

Conversation

@tt-a1i

@tt-a1i tt-a1i commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

What this PR does

This PR normalizes VS Code companion openFile line and column suffixes so zero values such as src/app.ts:0:0 resolve to the start of the file instead of producing negative editor positions.

It also extends the FileMessageHandler test mock with the VS Code position/range APIs used by the open-file path and adds a regression test for the zero line/column case.

Why it's needed

The web UI can emit file links with a zero-based line value. The VS Code companion treats incoming line/column suffixes as one-based values and subtracts one before creating vscode.Position. When the incoming value is 0, the handler can try to create vscode.Position(-1, -1), which makes the open-file request fail instead of opening the file.

Clamping the converted position keeps the existing one-based behavior for normal :line:column links while making zero-valued links safe.

Reviewer Test Plan

How to verify

Run npm test --workspace=packages/vscode-ide-companion -- FileMessageHandler.test.ts and confirm the new openFile clamps zero line and column values to the file start test passes. Reviewers can also confirm that src/app.ts:0:0 opens src/app.ts and selects line 0 / column 0 in the mocked VS Code editor instead of surfacing an open-file error.

Evidence (Before & After)

Before: src/app.ts:0:0 was converted by subtracting one directly, producing negative VS Code editor positions.

After: src/app.ts:0:0 is clamped to line 0 / column 0, and the regression test asserts that no error is shown and the editor selection/reveal range point at the file start.

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested
🐧 Linux ⚠️ not tested

Environment (optional)

Local workspace tests on macOS with the repository npm workspace setup.

Commands run:

npm test --workspace=packages/vscode-ide-companion -- FileMessageHandler.test.ts
npx prettier --check packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.test.ts
npx eslint packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.test.ts
npm run check-types --workspace=packages/vscode-ide-companion
git diff --check

Sub-agent review: no blocking issues found.

Risk & Scope

  • Main risk or tradeoff: A malformed :0 or :0:0 location now opens at the file start rather than failing the open-file request.
  • Not validated / out of scope: Negative suffixes such as src/app.ts:-1:0 are not treated as line/column suffixes by the existing parser and are unchanged.
  • Breaking changes / migration notes: None.

Linked Issues

Fixes #5710

AI Assistance Disclosure

I used Codex to review the changes, sanity-check the implementation against existing patterns, and help spot potential edge cases.

中文说明

What this PR does

这个 PR 会规范 VS Code companion 的 openFile 行号和列号后缀解析,让 src/app.ts:0:0 这样的零值位置打开到文件开头,而不是产生负数编辑器位置。

同时,测试里的 VS Code mock 补上了 open-file 路径实际用到的 position/range API,并增加了零行号/零列号的回归测试。

Why it's needed

Web UI 可能发出带零基行号的文件链接。VS Code companion 会把收到的行号/列号后缀当作一基值处理,并在创建 vscode.Position 前减一。当传入值是 0 时,handler 可能会创建 vscode.Position(-1, -1),导致打开文件请求失败,而不是正常打开文件。

转换后做下限 clamp 可以保留普通 :line:column 链接的一基行为,同时让零值链接安全落到文件开头。

Reviewer Test Plan

How to verify

运行 npm test --workspace=packages/vscode-ide-companion -- FileMessageHandler.test.ts,确认新增的 openFile clamps zero line and column values to the file start 测试通过。Reviewer 也可以确认 src/app.ts:0:0 会打开 src/app.ts,并在 mock 的 VS Code editor 中选择 line 0 / column 0,而不是显示打开文件错误。

Evidence (Before & After)

Before: src/app.ts:0:0 会被直接减一转换,产生负数 VS Code 编辑器位置。

After: src/app.ts:0:0 会被 clamp 到 line 0 / column 0,回归测试断言不会显示错误,并且 editor selection/reveal range 指向文件开头。

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested
🐧 Linux ⚠️ not tested

Environment (optional)

在 macOS 上使用仓库 npm workspace 进行本地测试。

已运行命令:

npm test --workspace=packages/vscode-ide-companion -- FileMessageHandler.test.ts
npx prettier --check packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.test.ts
npx eslint packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.test.ts
npm run check-types --workspace=packages/vscode-ide-companion
git diff --check

子代理审查:未发现阻塞问题。

Risk & Scope

  • Main risk or tradeoff: 格式异常的 :0:0:0 位置现在会打开到文件开头,而不是让打开文件请求失败。
  • Not validated / out of scope: src/app.ts:-1:0 这类负数后缀不会被现有 parser 当作行号/列号后缀处理,本 PR 不改变这部分行为。
  • Breaking changes / migration notes: 无。

Linked Issues

Fixes #5710

AI Assistance Disclosure

I used Codex to review the changes, sanity-check the implementation against existing patterns, and help spot potential edge cases.

@tt-a1i
tt-a1i marked this pull request as ready for review June 22, 2026 20:52
@wenshao

wenshao commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

On direction: this is a straightforward bug fix for a real user-facing issue — web UI file links with zero-based line/column values crash the VS Code openFile handler by producing negative Position values. Linked issue #5710 clearly describes the problem. Well within scope for the VS Code companion. CHANGELOG has no direct reference but the area (IDE integration / file operations) is core functionality.

On approach: scope is tight and appropriate. The fix extracts a small toZeroBasedEditorPosition helper that clamps the result of parseInt - 1 to zero, replacing the two inline expressions. The regression test covers the exact failure mode. No scope creep, no unrelated changes. One small observation: the helper name is quite long for what it does — toEditorPosition or clampPosition would be shorter — but that's a style nit, not worth blocking on.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

方向:这是一个直接的用户问题修复——web UI 文件链接中的零值行号/列号会导致 VS Code openFile handler 产生负数 Position 值而崩溃。关联 issue #5710 描述清晰。在 VS Code companion 范围内。CHANGELOG 没有直接相关条目,但 IDE 集成/文件操作是核心功能。

方案:范围紧凑合理。提取了一个小的 toZeroBasedEditorPosition 方法来将 parseInt - 1 的结果 clamp 到零,替换了两处内联表达式。回归测试覆盖了具体的失败场景。没有范围蔓延,没有无关改动。唯一小瑕疵:helper 方法名相对其功能偏长——toEditorPositionclampPosition 更短——但这是风格问题,不构成阻塞。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

The diff is minimal and well-focused — exactly what a bug fix should look like.

Production code (FileMessageHandler.ts, +9/-2): adds toZeroBasedEditorPosition() which combines parseInt, the one-based-to-zero-based subtraction, and a Math.max(0, ...) clamp. The two inline expressions that previously did parseInt(lineStr, 10) - 1 are replaced by calls to this helper. Clean consolidation, no over-engineering.

Test code (FileMessageHandler.test.ts, +71/-0): extends the vscodeMock with Position, Selection, Range, and TextEditorRevealType classes — the VS Code APIs the openFile path actually uses. The mock Position constructor throws on negative values (matching real VS Code behavior), which makes the regression test genuinely enforce the invariant. The new test sends src/app.ts:0:0 and asserts no error + correct editor position.

Reuse check: the parseInt - 1 pattern only appears at this one call site. No duplication to flag.

No correctness issues, no security concerns, no AGENTS.md violations.

Testing

Unit Tests

All 8 tests pass, including the new regression test:

 ✓ src/webview/handlers/FileMessageHandler.test.ts (8 tests) 15ms
   ✓ openFile clamps zero line and column values to the file start

 Test Files  1 passed (1)
      Tests  8 passed (8)

Regression Verification (reverted fix + re-ran test)

To confirm the test actually catches the bug, I reverted the production code change and re-ran:

 FAIL  src/webview/handlers/FileMessageHandler.test.ts
 ✗ openFile clamps zero line and column values to the file start

 Expected spy "showErrorMessage" to not be called, but it was called 1 time:
   1st call: ["Failed to open file: Position line and character must be non-negative"]

 Test Files  1 failed (1)
      Tests  1 failed | 7 passed (8)

Without the clamp, parseInt("0", 10) - 1 = -1 triggers the negative Position error — exactly the bug described in #5710. The test correctly catches it.

Tmux / CLI Testing

Not applicable — this is a VS Code extension internal handler, not a CLI-invokable feature. The behavior only manifests through the VS Code companion webview messaging API.

Type Check

The check-types failures (@qwen-code/webui module not found in sessionGrouping.ts and utils.ts) are pre-existing and unrelated to this PR — the webui workspace package wasn't built in the test environment.

中文说明

代码审查

Diff 极简且聚焦——正是 bug fix 应有的样子。

生产代码FileMessageHandler.ts,+9/-2):添加了 toZeroBasedEditorPosition() 方法,将 parseInt、一基到零基的减法、和 Math.max(0, ...) clamp 合并。原来两处内联的 parseInt(lineStr, 10) - 1 表达式被替换为调用该 helper。干净的整合,没有过度设计。

测试代码FileMessageHandler.test.ts,+71/-0):在 vscodeMock 中补上了 PositionSelectionRangeTextEditorRevealType 类——openFile 路径实际使用的 VS Code API。mock 的 Position 构造函数在负值时抛出异常(匹配真实 VS Code 行为),使回归测试真正强制执行了约束条件。新测试发送 src/app.ts:0:0 并断言无错误+正确的编辑器位置。

复用检查: parseInt - 1 模式仅出现在此一处调用点。无重复可标记。

无正确性问题,无安全隐患,无 AGENTS.md 违规。

测试

单元测试

8 个测试全部通过,包括新增的回归测试。

回归验证(回退修复 + 重跑测试)

为确认测试真正能捕捉该 bug,我回退了生产代码改动并重新运行:没有 clamp 时,parseInt("0", 10) - 1 = -1 触发负数 Position 错误——正是 #5710 描述的 bug。测试正确地捕捉到了它。

Tmux / CLI 测试

不适用——这是 VS Code 扩展内部 handler,不是 CLI 可调用的功能。

类型检查

check-types 的失败(sessionGrouping.tsutils.ts 中找不到 @qwen-code/webui 模块)是预先存在的问题,与本 PR 无关。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Reflection

This is exactly the kind of PR that's easy to approve: a small, focused fix for a real bug, with a test that actually catches the failure mode.

My independent proposal was Math.max(0, line - 1) inline — the PR's approach is better because the extracted helper also consolidates the parseInt call, so the two call sites become trivially readable. I verified the regression test genuinely enforces the invariant: reverting the production code makes the test fail with the exact error from issue #5710.

The mock additions (Position, Selection, Range throwing on negative values) are a nice touch — they make the test a real safety net rather than a rubber stamp. Every change in the diff serves the stated goal. No scope creep, no drive-by refactors.

The only nit I had was the helper method name being slightly long, but that's genuinely not worth a round trip. Ships it.

中文说明

总结

这是一个非常容易批准的 PR:一个针对真实 bug 的小而聚焦的修复,带有真正能捕捉失败模式的测试。

我的独立方案是内联 Math.max(0, line - 1) —— PR 的方案更好,因为提取的 helper 同时整合了 parseInt 调用,使两个调用点变得一目了然。我验证了回归测试真正强制执行了约束:回退生产代码后测试以 #5710 中描述的确切错误失败。

Mock 添加(Position、Selection、Range 在负值时抛出异常)是一个很好的设计——使测试成为真正的安全网而非橡皮图章。Diff 中的每一处改动都服务于既定目标。无范围蔓延,无顺手重构。

唯一的小瑕疵是 helper 方法名稍长,但不值得为此增加一轮往返。通过。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@wenshao

wenshao commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

✅ Local runtime verification — recommend merge

I verified this PR on Linux by driving the real FileMessageHandler.handle({ type: 'openFile' }) end-to-end against a faithful mock vscode (the same one this PR adds, where new vscode.Position(line, character) throws on negatives — exactly like the real VS Code API). VS Code extension code can only execute against a mocked vscode surface, so vitest is the package's real runtime; I ran it in tmux as an A/B (reverting only the source for BEFORE) and added an adversarial 7-scenario matrix well beyond the PR's single test.

Note: a standalone esbuild bundle of the handler isn't viable here — its import graph reaches @qwen-code/qwen-code-core (tree-sitter .wasm), so the mock-vscode vitest path is the faithful way to exercise the compiled handler.

Behavioral evidence — real handler A/B (observed)

Each row drives handle({ openFile, path }) and records what the editor actually did:

path suffix BEFORE (parent 2fd2104f) AFTER (this PR)
src/app.ts:0:0 Failed to open file: Position … must be non-negativeerror toast, cursor never set ✅ opens, selects (0,0), reveals
src/app.ts:0 ❌ error toast, no navigation ✅ opens, selects (0,0)
src/app.ts:0:5 ❌ error toast, no navigation ✅ opens, selects (0,4)
src/app.ts:5:0 ❌ error toast, no navigation ✅ opens, selects (4,0)
src/app.ts:1:1 (valid) ✅ opens, selects (0,0) ✅ opens, selects (0,0) — identical
src/app.ts:12:5 (valid) ✅ opens, selects (11,4) ✅ opens, selects (11,4) — identical
src/app.ts (no suffix) ✅ opens, no selection ✅ opens, no selection — identical

This reproduces #5710 exactly: any zero-containing suffix (a web-UI link can emit a zero-based line) made BEFORE compute a negative position → vscode.Position(-1, …) throws → the handler surfaces a "Failed to open file" error and never places the cursor. AFTER, Math.max(0, parseInt(v,10) - 1) clamps each axis to the file start, so the file opens and the cursor lands at (0,0). Every valid one-based link and the plain path behave identically to before.

Tests

  • Full suite on AFTER: vitest run FileMessageHandler.test.ts8/8 pass.
  • Mutation / teeth check: revert only FileMessageHandler.ts to the parent while keeping the new test → the new openFile clamps zero line and column values to the file start test fails with the precise bug (Position line and character must be non-negativeFailed to open file); the other 7 tests still pass. The new test genuinely guards the fix.

Risk

Low / surgical. Only the previously-broken zero-valued suffixes change behavior (now open at file start instead of erroring) — intended per #5710. Valid one-based :line:column links and suffix-less paths are byte-for-byte unchanged. As the PR notes, negative suffixes like :-1 aren't parsed as line/column by the existing regex, so they're unaffected.

Verdict: ✅ behaves exactly as described. Recommend merge.

🇨🇳 中文版(点击展开)

✅ 本地运行时验证 — 建议合并

我在 Linux 上做了真实验证:用一个忠实的 mock vscode(正是本 PR 新增的那个,其中 new vscode.Position(line, character) 在负值时会抛错 —— 和真实 VS Code API 完全一致)端到端驱动真实的 FileMessageHandler.handle({ type: 'openFile' })。VS Code 扩展代码只能在被 mock 的 vscode 接口上运行,所以 vitest 就是这个包的真实运行环境;我在 tmux 里把它跑成了 A/B(BEFORE 只回退源码),并加了一个远超 PR 单个测试的 7 场景对抗性矩阵

说明:这里没法对 handler 单独做 esbuild 打包 —— 它的依赖图会牵到 @qwen-code/qwen-code-core(tree-sitter .wasm),所以 mock-vscode 的 vitest 路径才是忠实驱动编译后 handler 的方式。

行为证据 —— 真实 handler A/B(实测)

每一行都驱动 handle({ openFile, path }),并记录 editor 实际做了什么:

path 后缀 BEFORE(父提交 2fd2104f AFTER(本 PR)
src/app.ts:0:0 Failed to open file: Position … must be non-negative —— 弹错误提示,光标从未设置 ✅ 打开,选中 (0,0),reveal
src/app.ts:0 ❌ 错误提示,未跳转 ✅ 打开,选中 (0,0)
src/app.ts:0:5 ❌ 错误提示,未跳转 ✅ 打开,选中 (0,4)
src/app.ts:5:0 ❌ 错误提示,未跳转 ✅ 打开,选中 (4,0)
src/app.ts:1:1(合法) ✅ 打开,选中 (0,0) ✅ 打开,选中 (0,0) —— 完全一致
src/app.ts:12:5(合法) ✅ 打开,选中 (11,4) ✅ 打开,选中 (11,4) —— 完全一致
src/app.ts(无后缀) ✅ 打开,无选区 ✅ 打开,无选区 —— 完全一致

这精确复现了 #5710:任何含零的后缀(web UI 链接可能发出零基行号)在 BEFORE 时会算出负数位置 → vscode.Position(-1, …) 抛错 → handler 弹出 “Failed to open file” 错误且不放置光标。AFTER 时,Math.max(0, parseInt(v,10) - 1) 把每个轴 clamp 到文件开头,于是文件正常打开、光标落在 (0,0)。所有合法的一基链接和无后缀路径行为与改动前完全一致

测试

  • AFTER 上的完整套件: vitest run FileMessageHandler.test.ts8/8 通过
  • 变异 / 牙齿检查(teeth check): 只把 FileMessageHandler.ts 回退到父提交、保留新测试 → 新增的 openFile clamps zero line and column values to the file start 测试失败,报的正是这个 bug(Position line and character must be non-negativeFailed to open file);其余 7 个测试仍然通过。说明新测试确实在守护这个修复。

风险

低 / 改动精准。 只有原本就坏掉的零值后缀行为会改变(现在打开到文件开头,而不是报错)—— 这正是 #5710 的目的。合法的一基 :line:column 链接和无后缀路径完全不变。如 PR 所述,:-1 这类负数后缀本就不会被现有正则当作行/列后缀,因此不受影响。

结论:✅ 行为与描述完全一致,建议合并。

@yiliang114

Copy link
Copy Markdown
Collaborator

I think the clamp is fine as defensive handling, but we should clarify the source of :0.

The handler only gets -1 after it receives a :0 suffix and subtracts 1. I don't see MarkdownRenderer generating 0 by itself unless the original link already contains #L0 / :0.

One related source is read_file.offset: it is documented as 0-based, and toolLocations() returns it as line. If that line is later rendered via FileLink, it gets appended as :<line> and interpreted by openFile as 1-based. That means clamping fixes 0 -> -1, but not the broader off-by-one case for offset > 0.

Could we either add a test through the real producer of :0, or normalize ToolLocation.line before it reaches FileLink?

@wenshao

wenshao commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

✅ Local verification report (maintainer)

Built and tested this PR locally to confirm the fix before merge.

Environment

  • macOS (Darwin 25.5), Node v22.22.2
  • PR head 826b8cca0MERGEABLE, FileMessageHandler.ts has no drift vs current origin/main.
  • Method: isolated git worktree at the PR head with a clean npm ci, then verified each claim independently.

Results

Check Command Result
Target tests vitest run FileMessageHandler.test.ts (vscode-ide-companion) ✅ 8/8 pass
Types npm run check-types (vscode-ide-companion) ✅ exit 0, 0 errors
Lint / Format eslint / prettier --check on both changed files ✅ clean

Mutation test (the key evidence). I reverted FileMessageHandler.ts to main while keeping the new test — it fails with exactly the #5710 bug:

× openFile clamps zero line and column values to the file start
  [FileMessageHandler] Failed to open file: Error: Position line and character must be non-negative

src/app.ts:0:0 made the old code compute parseInt('0') - 1 = -1, and new vscode.Position(-1, -1) throws, so the handler fell into the catch and surfaced showErrorMessage(...). The new test pins exactly that (asserts no error + selection/range at {0,0}); it passes once the clamp is restored.

Correctness review

  • New toZeroBasedEditorPosition(value) returns 0 for empty/undefined, else Math.max(0, parseInt(value, 10) - 1). So the 1-based → 0-based conversion can no longer go negative: :0 (and any value the regex captures) clamps to the file start. Applied to both line and column. The regex (?::(\d+))? guarantees the captured groups are digit-only, so parseInt is safe, and the if (lineStr) guard keeps navigation opt-in. Minimal and on-point for bug(vscode): openFile accepts zero line and column values #5710.

Verdict: LGTM. Correct, minimal, mutation-verified (reproduces #5710 exactly); merges cleanly, types/lint/format clean. Safe to merge from my side.

🇨🇳 中文版

✅ 本地验证报告(维护者)

合并前在本地构建并测试了本 PR 以确认修复。

环境

  • macOS(Darwin 25.5),Node v22.22.2
  • PR head 826b8cca0 —— MERGEABLEFileMessageHandler.ts 相对当前 origin/main 无漂移。
  • 方法:在 PR head 上建独立 git worktree,做干净的 npm ci,再逐条独立验证。

结果

检查项 命令 结果
目标测试 vitest run FileMessageHandler.test.ts(vscode-ide-companion) ✅ 8/8 通过
类型 npm run check-types(vscode-ide-companion) ✅ exit 0,0 错误
Lint / 格式 对两个改动文件跑 eslint / prettier --check ✅ 干净

变异测试(关键证据)。 我把 FileMessageHandler.ts 还原成 main,保留新测试 —— 它#5710 的原 bug 失败

× openFile clamps zero line and column values to the file start
  [FileMessageHandler] Failed to open file: Error: Position line and character must be non-negative

src/app.ts:0:0 让旧代码算出 parseInt('0') - 1 = -1,而 new vscode.Position(-1, -1) 会抛错,于是 handler 落入 catch 弹出 showErrorMessage(...)。新测试正好锁定这点(断言无错误 + selection/range 在 {0,0});恢复 clamp 后即通过。

正确性审查

  • 新的 toZeroBasedEditorPosition(value) 对空/未定义返回 0,否则 Math.max(0, parseInt(value, 10) - 1)。于是 1-based → 0-based 的转换不会再变负::0(以及正则捕获到的任何值)都会夹到文件开头。行和列都套用。正则 (?::(\d+))? 保证捕获组是纯数字,所以 parseInt 安全,且 if (lineStr) 守卫让导航是可选的。对 bug(vscode): openFile accepts zero line and column values #5710 而言最小且对症。

结论:LGTM。 正确、最小化、经变异测试验证(精确复现 #5710);可干净合并,类型/lint/格式干净。从我这边看可以安全合并。

@wenshao

wenshao commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

@yiliang114 Thanks — I traced the whole chain against the code. Short version: the data-model inconsistency you spotted is real, but the off-by-one you describe isn't reachable today, and read_file isn't the producer of :0. The clamp is a correct defensive fix and is safe to merge.

What's correct in your analysis

The 0-based vs 1-based mismatch is genuinely there in the data:

  • read_file.offset is 0-based — getDescription() renders it as offset + 1 (packages/core/src/tools/read-file.ts).
  • toolLocations() returns it raw: return [{ path, line: this.params.offset }] (read-file.ts:97), no +1.
  • The ACP emitter also doesn't convert: .map((loc) => ({ path: loc.path, line: loc.line ?? null })) (ToolCallEmitter.ts:309).
  • FileLink expects 1-based (line prop doc says "starting from 1") and appends :${line}; openFile then subtracts 1.

So ToolLocation.line being 0-based while FileLink is 1-based is a real latent inconsistency.

Why the off-by-one isn't actually reachable (and read_file isn't the :0 source)

Your inference is hedged on "if that line is later rendered via FileLink" — and it currently isn't:

  • routing.ts routes read_file (kind read) to ReadToolCall.
  • Every FileLink in ReadToolCall.tsx (lines 155 / 183 / 209 / 239) passes path only — no line, and ReadToolCall doesn't use LocationsList (the component that does forward loc.line).

So read_file's 0-based offset is dropped at render time and never reaches FileLink.line. It produces neither a clickable :0 nor an offset > 0 off-by-one. (edit.toolLocations() returns no line; grep/ripGrep don't override toolLocations() at all — so the 0-based read_file offset is the only value that could be inconsistent, and it isn't rendered.)

The actual source of :0

The other source you mentioned is the real one: a Markdown link with #L0 / #0. MarkdownRenderer's normalizeExplicitFileLink uses ^L?(\d+)(?:-\d+)?$, so #L0base:0. A model-authored #L5 (humans write 1-based) → :5-1 → line 5, which is correct; only the degenerate #L0 / :0 hit Position(-1). The clamp is exactly the right guard for that, and it only affects :0/negative inputs (:1+ unchanged), so it masks nothing.

Suggestion

  • Merge this PR as-is — the defensive clamp is correct.
  • The 0-based/1-based inconsistency is worth a separate follow-up: normalize in read-file.ts toolLocations() (line: offset + 1) or in the emitter, so it stays correct if ReadToolCall ever renders a line. Not a blocker here since that path is unwired.
  • On testing: a test "through the real producer" would go through the MarkdownRenderer #L0 path, not read_file; the handler-level unit test added here is still a valid boundary guard.

(Static dataflow verification — routing + component source make "the read_file line never reaches FileLink" deterministic, so I didn't need to drive the webview.)

中文版(完整对应)

@yiliang114 谢谢 —— 我对照代码把整条链路追了一遍。结论先说:你发现的数据模型不一致确实存在,但你描述的 off-by-one 当前不可达,且 :0 的来源并不是 read_file。这个 clamp 是正确的防御性修复,可以合并。

你分析中正确的部分

0-based 与 1-based 的不匹配在数据层是真实存在的:

  • read_file.offset 是 0-based —— getDescription()offset + 1 显示(packages/core/src/tools/read-file.ts)。
  • toolLocations() 原样返回:return [{ path, line: this.params.offset }](read-file.ts:97),没有 +1
  • ACP emitter 也不转换:.map((loc) => ({ path: loc.path, line: loc.line ?? null }))(ToolCallEmitter.ts:309)。
  • FileLink 期望 1-based(line prop 文档写 "starting from 1")并拼接 :${line};openFile 再减 1。

所以"ToolLocation.line 是 0-based 而 FileLink 是 1-based"是真实的隐性不一致。

为什么 off-by-one 实际不可达(且 read_file 不是 :0 来源)

你的推断用了"if that line is later rendered via FileLink"这个假设 —— 而当前并未接通:

  • routing.tsread_file(kind read)路由到 ReadToolCall
  • ReadToolCall.tsx 中所有 FileLink(155 / 183 / 209 / 239 行)都只传 path,不传 line,且 ReadToolCall 不使用会转发 loc.lineLocationsList

所以 read_file 的 0-based offset 在渲染时就被丢弃,根本到不了 FileLink.line,既不会产生可点的 :0,也没有 offset > 0 的 off-by-one。(edit.toolLocations() 不返回 line;grep/ripGrep 压根没 override toolLocations() —— 所以 0-based 的 read_file offset 是唯一可能不一致的值,而它没被渲染。)

:0 的真实来源

你提到的另一条才是真的:Markdown 链接里的 #L0 / #0MarkdownRenderernormalizeExplicitFileLink^L?(\d+)(?:-\d+)?$,于是 #L0base:0。模型手写的 #L5(人类按 1-based 写)→ :5 → 减 1 → 第 5 行,是对的;只有退化的 #L0 / :0 会砸到 Position(-1)。clamp 正是针对这种输入的正确兜底,而且只影响 :0/负数(:1+ 不变),不会 mask 任何东西。

建议

  • 本 PR 按现状合并 —— 防御性 clamp 是正确的。
  • 0-based/1-based 不一致值得开一个独立 follow-up:在 read-file.tstoolLocations() 归一化(line: offset + 1)或在 emitter 处理,这样将来若 ReadToolCall 真渲染行号也不会踩雷。由于该路径当前未接通,不应阻塞本 PR。
  • 测试方面:"通过真实产生源加测试"应走 MarkdownRenderer#L0 路径而非 read_file;这里新增的 handler 级单测作为边界守卫本身也是成立的。

(静态 dataflow 核验 —— 路由 + 组件源码让"read_file 的 line 永远到不了 FileLink"是确定性的,所以无需驱动 webview。)

@wenshao
wenshao merged commit 0255b04 into QwenLM:main Jun 23, 2026
38 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(vscode): openFile accepts zero line and column values

4 participants