From 5fae3419ede960dea03f72450bf686ccafeb4d47 Mon Sep 17 00:00:00 2001 From: Maruthan G Date: Thu, 9 Apr 2026 15:23:11 +0530 Subject: [PATCH] fix: clamp HTML completion ranges for unclosed string literals (#273226) When an attribute value has an opening quote but no closing quote, the HTML parser treats subsequent HTML tags as part of the attribute value, causing completion replacement ranges to extend too far. Post-process completion items to clamp their ranges before any '<' character after the cursor position, preventing deletion of subsequent HTML content. --- .../server/src/modes/htmlMode.ts | 35 +++++++++++++++++-- .../server/src/test/completions.test.ts | 28 +++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/extensions/html-language-features/server/src/modes/htmlMode.ts b/extensions/html-language-features/server/src/modes/htmlMode.ts index 58a3ded2beed2..15066ecabd669 100644 --- a/extensions/html-language-features/server/src/modes/htmlMode.ts +++ b/extensions/html-language-features/server/src/modes/htmlMode.ts @@ -20,14 +20,45 @@ export function getHTMLMode(htmlLanguageService: HTMLLanguageService, workspace: async getSelectionRange(document: TextDocument, position: Position): Promise { return htmlLanguageService.getSelectionRanges(document, [position])[0]; }, - doComplete(document: TextDocument, position: Position, documentContext: DocumentContext, settings = workspace.settings) { + async doComplete(document: TextDocument, position: Position, documentContext: DocumentContext, settings = workspace.settings) { const htmlSettings = settings?.html; const options = merge(htmlSettings?.suggest, {}); options.hideAutoCompleteProposals = htmlSettings?.autoClosingTags === true; options.attributeDefaultValue = htmlSettings?.completion?.attributeDefaultValue ?? 'doublequotes'; const htmlDocument = htmlDocuments.get(document); - const completionList = htmlLanguageService.doComplete2(document, position, htmlDocument, documentContext, options); + const completionList = await htmlLanguageService.doComplete2(document, position, htmlDocument, documentContext, options); + + // Fix completion ranges for unclosed string literals (issue #273226). + // When an attribute value has an opening quote but no closing quote, + // the parser treats everything after the quote as the attribute value, + // causing the replacement range to extend too far (e.g., into subsequent tags). + // We clamp the replacement range to end before the next '<' character after the cursor. + const text = document.getText(); + const offset = document.offsetAt(position); + for (const item of completionList.items) { + if (!item.textEdit) { + continue; + } + const editRange = 'range' in item.textEdit ? item.textEdit.range : item.textEdit.replace; + const editEndOffset = document.offsetAt(editRange.end); + if (editEndOffset <= offset) { + continue; + } + // Check if the text between cursor and end of range contains a '<', + // which indicates the range leaked into subsequent HTML tags + const textAfterCursor = text.substring(offset, editEndOffset); + const angleBracketIndex = textAfterCursor.indexOf('<'); + if (angleBracketIndex !== -1) { + const clampedEnd = document.positionAt(offset + angleBracketIndex); + if ('range' in item.textEdit) { + item.textEdit.range = Range.create(editRange.start, clampedEnd); + } else { + item.textEdit.replace = Range.create(editRange.start, clampedEnd); + } + } + } + return completionList; }, async doHover(document: TextDocument, position: Position, settings?: Settings) { diff --git a/extensions/html-language-features/server/src/test/completions.test.ts b/extensions/html-language-features/server/src/test/completions.test.ts index fbad266e2dea2..0b6885b33aacf 100644 --- a/extensions/html-language-features/server/src/test/completions.test.ts +++ b/extensions/html-language-features/server/src/test/completions.test.ts @@ -94,6 +94,34 @@ suite('HTML Completion', () => { }); }); +suite('HTML Unclosed String Completions', () => { + test('Completion should not replace content after cursor when attribute value has unclosed quote', async () => { + // Issue #273226: When an attribute value has an opening quote but no closing quote, + // the replacement range should not extend into subsequent HTML tags. + await testCompletionFor('' }, + ] + }); + }); + + test('Completion should not replace content after cursor with space before closing tag', async () => { + await testCompletionFor('' }, + ] + }); + }); + + test('Completion with properly closed quotes should still work normally', async () => { + await testCompletionFor('', { + items: [ + { label: 'checkbox', resultText: '' }, + ] + }); + }); +}); + suite('HTML Path Completion', () => { const triggerSuggestCommand = { title: 'Suggest',