can1357/oh-my-pi · error · ToolError

snippet-formatted LSP edits are unsupported

Error message

snippet-formatted LSP edits are unsupported

What it means

LSP TextEdits may declare insertTextFormat=2, meaning newText is a snippet with placeholders/tab stops (${1:foo}). Snippet syntax is not plain text and cannot be applied literally to the file, so the edit pipeline rejects snippet-formatted edits up front rather than writing snippet markup into source.

Source

Thrown at packages/coding-agent/src/lsp/edits.ts:87

}

/** True when two ranges overlap (share any position other than a touching boundary). */
export function rangesOverlap(a: Range, b: Range): boolean {
	return comparePosition(a.start, b.end) < 0 && comparePosition(b.start, a.end) < 0;
}

/**
 * Sort edits bottom-to-top for in-place application and reject overlaps.
 * Equal start positions tiebreak by original array index descending so that,
 * applied bottom-up, inserts at the same position land in array order
 * (LSP spec: the order of edits in the array defines the order in the result).
 * Byte-identical non-empty range edits are idempotent, so duplicate server
 * output is collapsed before overlap validation.
 */
function rejectSnippetTextEdits(edits: TextEdit[]): void {
	for (const edit of edits) {
		if ("insertTextFormat" in edit && edit.insertTextFormat === 2) {
			throw new ToolError("snippet-formatted LSP edits are unsupported");
		}
	}
}

export function sortAndValidateTextEdits(edits: TextEdit[]): TextEdit[] {
	rejectSnippetTextEdits(edits);
	const sorted = edits
		.map((edit, index) => ({ edit, index }))
		.sort((a, b) => {
			if (a.edit.range.start.line !== b.edit.range.start.line) {
				return b.edit.range.start.line - a.edit.range.start.line;
			}
			if (a.edit.range.start.character !== b.edit.range.start.character) {
				return b.edit.range.start.character - a.edit.range.start.character;
			}
			return b.index - a.index;
		})
		.map(entry => entry.edit);

View on GitHub (pinned to 9690622007)

Solutions

  1. Use a code action / edit kind that produces plain-text edits, or configure the server to disable snippet support
  2. Convert snippet newText to plain text (strip ${...} placeholders) before passing to the edit pipeline
  3. Report/patch the server integration to not request snippet-formatted edits (e.g. don't advertise snippetCapability)

Example fix

// before: passing server edit through verbatim
applyEdits(serverResponse.edit.entries);
// after: strip snippet formatting first
const plain = edits.map(e => ({...e, insertTextFormat: 1, newText: stripSnippetTokens(e.newText)}));
applyEdits(plain);
Defensive patterns

Strategy: validation

Validate before calling

function hasSnippetEdits(edits) {
  return edits.some(e => 'insertTextFormat' in e && e.insertTextFormat === 2);
}
if (hasSnippetEdits(serverEdits)) {
  // convert or reject before calling sortAndValidateTextEdits
}

Type guard

function isPlainTextEdit(e) {
  return !('insertTextFormat' in e) || e.insertTextFormat !== 2;
}

Prevention

When it happens

Trigger: A language server returns text edits (e.g. from code actions, rename, or prepare) with insertTextFormat: 2 in the TextEdit payload; sortAndValidateTextEdits runs rejectSnippetTextEdits before applying.

Common situations: Servers that emit snippet edits for completion-like code actions; applying quickfixes from servers configured with snippet support; version changes where a server started emitting snippets.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/101946156420ad6f. Report an issue: GitHub.