Skip to content

Harden snippet session against null decorations and selection/placeholder cardinality drift#315206

Merged
dmitrivMS merged 1 commit into
mainfrom
dev/dmitriv/snippets-error-reports
May 8, 2026
Merged

Harden snippet session against null decorations and selection/placeholder cardinality drift#315206
dmitrivMS merged 1 commit into
mainfrom
dev/dmitriv/snippets-error-reports

Conversation

@dmitrivMS
Copy link
Copy Markdown
Contributor

Reconciles four telemetry-reported snippet crashes into a single set of defensive guards in OneSnippet / SnippetSession. The underlying root cause for some of these is broader (lack of editor transactions / re-entrant model edits during changeDecorations), which is out of scope here — this change just protects the snippet code without hiding errors or hacking around the model.

Closes #233164
Closes #233163
Closes #196664
Closes #193172

Changes

OneSnippet.move()

  • Capture _editor.getModel() into a local model once (instead of repeated this._editor.getModel() calls).
  • Null-guard _placeholderDecorations.get(...) and model.getDecorationRange(id) in:
    • the placeholder transform path,
    • the active-placeholder selection path,
    • the enclosing-placeholder loop.
  • A missing decoration is silently skipped instead of dereferencing null/undefined.

OneSnippet._hasPlaceholderBeenCollapsed()

  • Capture model into a local.
  • Treat a missing decoration range on a non-empty placeholder as collapsed (matches the original range.isEmpty() && marker.toString().length > 0 semantics, but tolerant of null).

SnippetSession.merge()

  • Add OneSnippet.activePlaceholderCount getter.
  • Add a cardinality precondition: only run structural merge and follow-on _move(undefined) when snippets.length === sum(activePlaceholderCount). When cursor normalization or external selection changes collapse mirrored placeholder selections (e.g. $1$1 at the same position), we now skip the structural merge instead of crashing on others.shift() returning undefined. Text edits still apply.

SnippetSession.next() / prev()

  • No-op when _move() returns no selections, instead of crashing on newSelections[0].

Tests

Added two regression tests in snippetSession.test.ts:

All 40 tests in snippetSession.test.ts and the surrounding 38 in the snippet test directory pass.

Manual repro (merge-collapse class)

User snippets:

{
  "test": { "prefix": "test", "body": "$1$1$0" },
  "nest": { "prefix": "nest", "body": "${1:nested}$0" }
}
  1. Type test, accept the suggestion. Buffer is empty, one cursor at column 1 (the two $1 cursors collapsed via normalization).
  2. Without leaving the snippet session, type nest and accept the second snippet from the suggestion list.

Pre-fix: TypeError: Cannot read properties of undefined (reading '_offset') logged to the Window output channel; buffer/navigation left in inconsistent state.

Post-fix: No crash. Text edit is applied; structural merge is skipped because the cardinality precondition fails.

The null-decoration class (#233164, #196664) is the result of re-entrant model edits during snippet navigation (e.g. format-on-type / extension edits firing inside changeDecorations) and is not reliably hand-reproducible. Coverage is via the unit test.

Notes on rejected approaches

…lder cardinality drift

Reconciles four telemetry issues (#233164, #233163, #196664, #193172) into clean defensive guards in OneSnippet/SnippetSession:

- Capture model into a local in OneSnippet.move() and _hasPlaceholderBeenCollapsed() and null-guard getDecorationRange/_placeholderDecorations.get lookups (covers #233164, #196664).

- Treat a missing decoration range on a non-empty placeholder as collapsed in _hasPlaceholderBeenCollapsed.

- Add OneSnippet.activePlaceholderCount and gate SnippetSession.merge structural merge + post-merge _move on a cardinality precondition (snippets.length === sum(activePlaceholderCount)). When cursor normalization or external selection changes collapse mirrored placeholder selections, we now skip structural merge instead of crashing on others.shift() (covers #233163, #193172). Text edits still apply.

- next()/prev() no-op when _move() returns no selections.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens snippet navigation and snippet merging in the editor against stale/missing placeholder decorations and against selection/placeholder-count mismatches (e.g., when cursor normalization collapses multiple empty placeholder occurrences into fewer selections), preventing several telemetry-reported crashes in OneSnippet/SnippetSession.

Changes:

  • Add null/undefined guards in OneSnippet.move() and _hasPlaceholderBeenCollapsed() for missing decoration IDs and missing decoration ranges, skipping invalid placeholders instead of dereferencing.
  • Add OneSnippet.activePlaceholderCount and use it in SnippetSession.merge() to only perform structural merges when the number of nested snippets matches the number of active placeholder occurrences.
  • Make SnippetSession.next()/prev() resilient to _move() returning no selections (no-op instead of indexing newSelections[0]).
Show a summary per file
File Description
src/vs/editor/contrib/snippet/browser/snippetSession.ts Adds defensive guards for missing decorations/ranges, adds merge cardinality precondition, and avoids crashes when navigation yields no selections.
src/vs/editor/contrib/snippet/test/browser/snippetSession.test.ts Adds regression tests covering missing-decoration navigation and merge when placeholder occurrences collapse to the same position.

Copilot's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 0

@dmitrivMS dmitrivMS marked this pull request as ready for review May 8, 2026 08:35
@dmitrivMS dmitrivMS enabled auto-merge (squash) May 8, 2026 08:35
@dmitrivMS dmitrivMS merged commit a4a9f2c into main May 8, 2026
30 checks passed
@dmitrivMS dmitrivMS deleted the dev/dmitriv/snippets-error-reports branch May 8, 2026 09:48
@vs-code-engineering vs-code-engineering Bot added this to the 1.120.0 milestone May 8, 2026
lszomoru pushed a commit that referenced this pull request May 8, 2026
…lder cardinality drift (#315206)

Reconciles four telemetry issues (#233164, #233163, #196664, #193172) into clean defensive guards in OneSnippet/SnippetSession:

- Capture model into a local in OneSnippet.move() and _hasPlaceholderBeenCollapsed() and null-guard getDecorationRange/_placeholderDecorations.get lookups (covers #233164, #196664).

- Treat a missing decoration range on a non-empty placeholder as collapsed in _hasPlaceholderBeenCollapsed.

- Add OneSnippet.activePlaceholderCount and gate SnippetSession.merge structural merge + post-merge _move on a cardinality precondition (snippets.length === sum(activePlaceholderCount)). When cursor normalization or external selection changes collapse mirrored placeholder selections, we now skip structural merge instead of crashing on others.shift() (covers #233163, #193172). Text edits still apply.

- next()/prev() no-op when _move() returns no selections.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

3 participants