Chat: editing text before an inline #file: reference deletes the reference
- Dominant language
- TypeScript
- Stars
- 193k
- Forks
- 42.4k
- PR merge metrics
- PR metrics pending
Description
Type: Bug
### Summary
In the **unsubmitted** chat input, editing text *before* an inline `#file:` reference makes the workbench **delete the reference text itself**. This is not the keyboard removing an attachment chip — `chatDynamicVariableModel` actively calls `executeEdits(..., text: "")` on the reference range.
This is distinct from #281766 (editing an *already submitted* prompt, closed/completed) and #284059 (ghost decorations left behind). Here the input has never been submitted.
### Steps to Reproduce
1. Open Chat.
2. Type a sentence, e.g. `Compare the ownership section of` .
3. Type `#`, pick a file from the picker so `#file:BACKLOG.md` is inserted inline, then continue typing `with the current draft.`
4. Move the caret **before** the reference (into the earlier part of the sentence) and edit there — e.g. press `Backspace` at the position immediately preceding the reference, or delete/retype a word right in front of it.
### Expected
The reference keeps tracking its text. Editing unrelated text earlier in the prompt must not affect it.
### Actual
The `#file:BACKLOG.md` text is removed from the input entirely, and the variable is dropped from the model. The user must re-open the picker and re-insert it.
### Why this matters
Inline references are the only mechanism that puts context **at a specific position in the sentence**. That positional placement is the whole point: it lets you write "compare `#file:a.md` against `#file:b.md`" instead of relying on pronouns and hoping the model binds them to the right attachment chips. Attachment chips are not a substitute — they carry no position. So losing an inline reference during ordinary editing is not a cosmetic glitch; it silently degrades the prompt into ambiguous demonstratives, and the loss is easy to miss because only the highlight disappears.
### Root cause
`chatDynamicVariableModel._subscribeToEditor()` (`src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts`), in the `onDidChangeModelContent` handler:
```js
if (model.getValueInRange(decorationRange) !== data.text) {
const change = e.changes.find(c =>
c.rangeOffset <= data.rangeOffset &&
c.rangeOffset + c.rangeLength >= data.rangeOffset + data.text.length); // must FULLY ENCLOSE the ref
const newRange = change && this.findReferenceRangeInReplacement(model, e.changes, change, data);
return newRange
? { ...ref, range: newRange }
: (change || this.widget.inputEditor.executeEdits(this.id, [{ range: decorationRange, text: '' }]),
toDispose.push(ref), null);
}
```
The recovery path (`findReferenceRangeInReplacement`) is only attempted when a change **fully encloses** the reference. An edit *before* the reference does not enclose it, so `change` is `undefined` and control falls to the `else` branch, which deletes the reference text and drops the variable.
An edit before the reference reaches this branch at all only because the decoration range no longer yields the exact reference text — decoration stickiness lets an edit adjacent to the reference boundary grow or shift the range, so `getValueInRange(decorationRange) !== data.text` becomes true even though the reference itself was untouched.
### Suggested fix
Deleting the user's text should not be the fallback for "I lost track of this range". Either:
1. Widen the recovery to handle changes that merely *precede* the reference (recompute the offset from the change delta and re-anchor), or
2. When recovery fails, drop the variable but **leave the text in place**, so the user can re-select it instead of hunting for what vanished.
### Version
```
Version: 1.135.0 (user setup)
Commit: 08d4889f9ec4a1685d257b9b95de036c8e1ce1e5
Date: 2026-08-25T14:26:52Z
Electron: 42.8.1
OS: Windows_NT x64 10.0.26200
```
Contributor guide
Assessment
This issue has not been assessed yet.