Skip to content

Fix insertSnippet() destroying active snippet session tabstops #322008

Open
almayor wants to merge 4 commits into
microsoft:mainfrom
almayor:main
Open

Fix insertSnippet() destroying active snippet session tabstops #322008
almayor wants to merge 4 commits into
microsoft:mainfrom
almayor:main

Conversation

@almayor

@almayor almayor commented Jun 18, 2026

Copy link
Copy Markdown

Problem

Related: #322010

Since VS Code 1.86, calling editor.insertSnippet() from an extension while a snippet session is active destroys the outer session's remaining tabstops. This is a regression — it worked correctly in 1.85.2 and earlier.

This breaks extensions like HyperSnips, LTeX, and others that programmatically insert snippets, making nested snippet expansion unusable.

Root cause

In 1.86, mainThreadEditor.ts was refactored to route insertSnippet() calls through snippetController.apply(edits[]) instead of snippetController.insert(template). The apply() path calls this.cancel() when a session is active:

if (this._session && typeof template !== 'string') {
    this.cancel();
}

The insert() path instead calls this._session.merge(template, opts), which properly nests the new snippet inside the active placeholder. The merge infrastructure (OneSnippet.merge(), _nestingLevel, _renormalizePlaceholderIndices()) is fully implemented and works — the regression was the API bridge switching code paths.

Fix

Route single-edit insertSnippet() calls back through snippetController.insert() (the merge path), falling back to apply() only for multi-range edits.

Note on issue #214757

This bug was reported in #214757, which was closed as a duplicate of #63129. However, #63129 is about snippet auto-indentation (fixed in #234858), not tabstop destruction — they are unrelated issues. The bug remains present through current versions.

Fixes #214757

Test

  1. Install HyperSnips or any extension that calls editor.insertSnippet() during editing
  2. Trigger a snippet with multiple tabstops (e.g. \int_{$1}^{$2} $3 d$4)
  3. While cursor is at $1, trigger another snippet insertion via the extension
  4. Press Tab — cursor should advance to $2 (currently it doesn't; the session is destroyed)

This fix was researched and developed with the assistance of Claude Code (Claude Opus 4.6).

claude and others added 2 commits June 18, 2026 21:53
editor.insertSnippet() previously routed every expansion through
SnippetController2.apply(ISnippetEdit[]), whose _doInsert() guard
unconditionally cancels any active snippet session before inserting.
This destroyed the outer snippet's remaining tabstops whenever a new
snippet was inserted while a session was active (e.g. auto-expanding
a snippet inside another snippet's placeholder) - a regression
introduced in VS Code 1.86 and tracked as microsoft#214757.

Route single contiguous snippet edits through the string-based
insert() path instead, which merges (nests) the new snippet into the
active placeholder via the existing OneSnippet.merge() infrastructure,
preserving the outer session. Multi-range edits, which cannot be
merged into a single active placeholder, still fall back to apply().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FTkA2Uv3LgJZWrtA32tq33
Fix snippet nesting by using insert API for single-range edits
Copilot AI review requested due to automatic review settings June 18, 2026 22:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 addresses a regression (since 1.86) where extension calls to TextEditor.insertSnippet() during an active snippet session can cancel the outer session and destroy remaining tabstops. The approach is to route single-range snippet insertions through the merge-capable SnippetController2.insert(...) path instead of the edits-array apply(...) path that cancels active sessions.

Changes:

  • Refactors MainThreadTextEditor.insertSnippet to build a shared insertOptions object for snippet insertion.
  • Routes single-range insertions through snippetController.insert(template, ...) after setting the editor selection to the target range.
  • Keeps multi-range insertions on the snippetController.apply(edits, ...) path.

Comment thread src/vs/workbench/api/browser/mainThreadEditor.ts Outdated
@almayor

almayor commented Jun 18, 2026

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

almayor and others added 2 commits June 19, 2026 00:14
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

editor.insertSnippet() removes the tabstops from snippets extended by this method

4 participants