From 31bcbea38024f9677c4253fa07b4fcde5acd99a8 Mon Sep 17 00:00:00 2001 From: Maruthan G Date: Wed, 22 Apr 2026 17:05:55 +0530 Subject: [PATCH 1/2] fix: clamp completion range when scanner crosses tag boundary When an attribute value has an unclosed quote but the scanner latches onto an unrelated later quote in the document, the completion replace range could still extend across HTML tag boundaries, wiping markup between the cursor and that later quote. Extend the clamp to also trigger when any '<' appears between the cursor and the alleged closing quote. Refs microsoft/vscode#273226 Signed-off-by: Maruthan G --- src/services/htmlCompletion.ts | 14 +++++++++++++- src/test/completion.test.ts | 12 ++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/services/htmlCompletion.ts b/src/services/htmlCompletion.ts index d6e899b..25cae0a 100644 --- a/src/services/htmlCompletion.ts +++ b/src/services/htmlCompletion.ts @@ -293,7 +293,19 @@ export class HTMLCompletion { let valueContentEnd = valueEnd; // valueEnd points to the char after quote, which encloses the replace range if (valueEnd > valueStart && text[valueEnd - 1] === text[valueStart]) { - valueContentEnd--; + // Even when a matching quote is found, the scanner may have latched + // onto an unrelated later quote across HTML tag boundaries. If any + // `<` appears between the cursor and the alleged closing quote, + // treat the value as unclosed and clamp so the completion cannot + // delete subsequent markup. See microsoft/vscode#273226. + let crossesTagBoundary = false; + for (let i = offset; i < valueEnd - 1; i++) { + if (text.charCodeAt(i) === 0x3C /* < */) { + crossesTagBoundary = true; + break; + } + } + valueContentEnd = crossesTagBoundary ? offset : valueContentEnd - 1; } else { // unclosed quote: clamp the end to the cursor position so that // the replace range does not extend into subsequent HTML content diff --git a/src/test/completion.test.ts b/src/test/completion.test.ts index fc7e2ab..7f6c1b2 100644 --- a/src/test/completion.test.ts +++ b/src/test/completion.test.ts @@ -172,6 +172,18 @@ suite('HTML Completion', () => { testCompletionFor('' }] }); + // unclosed quote with a stray quote later in the document (scanner would + // otherwise latch onto the stray quote and blow away the markup between + // the cursor and that quote). See microsoft/vscode#273226. + testCompletionFor('

', { + items: [{ label: 'checkbox', resultText: '

' }] + }); + testCompletionFor('

', { + items: [{ label: 'checkbox', resultText: '

' }] + }); + testCompletionFor('' }] + }); testCompletionFor('
', { items: [ From 0365fb79c1b268533129be27829e6bdd7cd9e940 Mon Sep 17 00:00:00 2001 From: Maruthan G Date: Sat, 25 Apr 2026 10:05:10 +0530 Subject: [PATCH 2/2] refactor: use _LAN constant from htmlScanner instead of magic number Signed-off-by: Maruthan G --- src/parser/htmlScanner.ts | 2 +- src/services/htmlCompletion.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/parser/htmlScanner.ts b/src/parser/htmlScanner.ts index 01750bd..80f45db 100644 --- a/src/parser/htmlScanner.ts +++ b/src/parser/htmlScanner.ts @@ -139,7 +139,7 @@ class MultiLineStream { } const _BNG = '!'.charCodeAt(0); const _MIN = '-'.charCodeAt(0); -const _LAN = '<'.charCodeAt(0); +export const _LAN = '<'.charCodeAt(0); const _RAN = '>'.charCodeAt(0); const _FSL = '/'.charCodeAt(0); const _EQS = '='.charCodeAt(0); diff --git a/src/services/htmlCompletion.ts b/src/services/htmlCompletion.ts index 25cae0a..f0f1fda 100644 --- a/src/services/htmlCompletion.ts +++ b/src/services/htmlCompletion.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { HTMLDocument, Node } from '../parser/htmlParser.js'; -import { createScanner } from '../parser/htmlScanner.js'; +import { createScanner, _LAN } from '../parser/htmlScanner.js'; import { CompletionConfiguration, ICompletionParticipant, ScannerState, TokenType, LanguageServiceOptions, DocumentContext, Position, CompletionList, CompletionItemKind, Range, TextEdit, InsertTextFormat, CompletionItem, MarkupKind, TextDocument @@ -300,7 +300,7 @@ export class HTMLCompletion { // delete subsequent markup. See microsoft/vscode#273226. let crossesTagBoundary = false; for (let i = offset; i < valueEnd - 1; i++) { - if (text.charCodeAt(i) === 0x3C /* < */) { + if (text.charCodeAt(i) === _LAN) { crossesTagBoundary = true; break; }