Mouse selection snapping around decoration before/after content: both edges of injected content should snap to the adjacent position
- Dominant language
- TypeScript
- Stars
- 193k
- Forks
- 42.4k
- PR merge metrics
- PR metrics pending
Description
Does this issue occur when all extensions are disabled?: Yes (reproduced with `--disable-extensions` plus only the minimal repro extension below)
- VS Code Version: 1.128.1 (5264f2156cbcd7aea5fd004d29eaa10209155d66, x64)
- OS Version: Windows 11 Home 10.0.26200
## Summary
When a decoration renders `before`/`after` `contentText` (injected content) inside a line, the injected content contributes its full rendered width to the mouse hit-testing "cell" between the two surrounding document positions. Because mouse selection snaps to the *nearest document position* by x-coordinate, and no document position exists inside the injected content, the snap midpoint between the character before the content and the position after it lands roughly in the *middle of the injected content*.
The practical effect: with wide injected content (e.g. alignment padding, inline hints/blame-style annotations), selecting text that ends **just before** the injected content with the mouse requires dragging about *half the injected content's width past* the intended character — for both the selection endpoint (drag release side) and the anchor (mousedown side). This makes ordinary click-and-drag selection around injected content feel unreliable, and it gets worse the wider the content is.
This is the mouse-hit-testing counterpart of #127703 (cursor stops around inlay hints, keyboard). The suggestion there — "duplicate the cursor position on either side of these labels" — is exactly what mouse snapping needs: **both the left and the right visual edge of injected content should act as snap targets for the same (adjacent) document position**, so that the usual midpoint rule applies only within real characters.
(Related but distinct: #88873, which was about `margin` on attachments breaking hit testing entirely and was mitigated. This report is about the snapping geometry that remains with plain `contentText`, no margin involved.)
## Steps to Reproduce
1. Create this minimal extension and run it (F5):
`package.json`
```json
{
"publisher": "repro",
"name": "injected-content-selection-repro",
"version": "0.0.0",
"engines": { "vscode": "^1.90.0" },
"activationEvents": ["onStartupFinished"],
"main": "index.js"
}
```
`index.js`
```js
const vscode = require("vscode");
exports.activate = function () {
const type = vscode.window.createTextEditorDecorationType({});
const decorate = (editor) => {
if (!editor) return;
// Insert 12 characters of injected content between "c" and "=" on line 0
const pos = new vscode.Position(0, 3);
editor.setDecorations(type, [{
range: new vscode.Range(pos, pos),
renderOptions: {
before: {
contentText: "____________",
color: "#00000000",
backgroundColor: "#8888885f"
}
}
}]);
};
vscode.window.onDidChangeActiveTextEditor(decorate);
decorate(vscode.window.activeTextEditor);
};
```
2. In the Extension Development Host, open a file whose first line is:
```
abc=def
```
It renders as `abc____________=def` (the `____________` region is injected content, not document text).
3. **Endpoint case (left-to-right drag):** try to select `abc` by pressing the mouse down before `a` and dragging right. After the pointer passes `c`, the selection endpoint stays at column 3 (`…b|c`)* until the pointer reaches roughly the middle of the injected content; only then does it advance to column 4 (`c|=`). You must drag ~half the injected width past `c` to select `abc`.
4. **Anchor case (right-to-left drag):** try to select `abc` by pressing the mouse down just to the right of `c` (i.e. at the left edge of the injected content) and dragging left. The mousedown anchor snaps to the position between `b` and `c` (the nearest document position by x-distance), so the resulting selection is `ab` — `c` is silently excluded. To include `c` you must start the drag from the right half of the injected content.
5. For contrast, selection on the *right* side of the injected content behaves normally: selecting `=def` right-to-left only requires dragging to the middle of `=`, because both edges of that snap interval are real characters.
\* column numbers 1-based as displayed in the status bar.
## Screen capture
## Expected Behavior
The left and right visual edges of injected content should both act as snap boundaries mapping to the same document position (here: between `c` and `=`). Concretely:
- mousedown/drag anywhere from the midpoint of `c` up to the injected content should resolve to the position between `c` and `=` — the injected width should not be counted when deciding between that position and the one before it;
- symmetric behavior on the other side.
With that rule, selecting `abc` behaves exactly as it does without the decoration, and the injected content stays selectable-across as today.
## Actual Behavior
Snapping treats the injected content's width as part of the single interval between the two surrounding document positions, so the decision midpoint sits inside the injected content. Selections that should end (or anchor) immediately before the content require dragging ~half the content's width further, and mousedown just before the content silently anchors one character early.
## Context
I maintain an extension ([Ghost Align](https://github.com/upu/ghost-align)) that renders code alignment as decoration-based padding precisely to avoid modifying documents. The behavior above is currently documented as a known limitation because the extension API offers no way to influence hit testing (`ThemableDecorationAttachmentRenderOptions` has only styling properties; `InjectedTextCursorStops` is internal and keyboard-only). Any extension that injects wide inline content (inline blame, error lenses, parameter hints via decorations, alignment padding) is affected.
Contributor guide
Assessment
This issue has not been assessed yet.