From 6201848e37f6ab5f0ea8267802cf04cc9435d174 Mon Sep 17 00:00:00 2001 From: Denis Bezrukov Date: Fri, 16 Jan 2026 15:22:28 +0200 Subject: [PATCH] [astro] WEB-76003 Improve attribute insert handling for Astro Space-RevId: 89d349e837e10f77861ac1b0fd3534dfab7474cb GitOrigin-RevId: e61ff48a5fc55575dd3acdeda14ccbfc2e852374 --- .../XmlAttributeValueQuotationHandler.kt | 81 +++++++++++++++++++ .../HtmlAttributeSymbolsCompletionProvider.kt | 29 ++++++- .../html/HtmlFrameworkSymbolsSupport.kt | 26 ++++++ 3 files changed, 133 insertions(+), 3 deletions(-) create mode 100644 xml/impl/src/com/intellij/codeInsight/completion/XmlAttributeValueQuotationHandler.kt diff --git a/xml/impl/src/com/intellij/codeInsight/completion/XmlAttributeValueQuotationHandler.kt b/xml/impl/src/com/intellij/codeInsight/completion/XmlAttributeValueQuotationHandler.kt new file mode 100644 index 000000000000..2ef9b2aac2e1 --- /dev/null +++ b/xml/impl/src/com/intellij/codeInsight/completion/XmlAttributeValueQuotationHandler.kt @@ -0,0 +1,81 @@ +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.codeInsight.completion + +import com.intellij.application.options.editor.WebEditorOptions +import com.intellij.codeInsight.AutoPopupController +import com.intellij.codeInsight.editorActions.TabOutScopesTracker +import com.intellij.codeInsight.lookup.LookupElement +import com.intellij.openapi.editor.ScrollType +import com.intellij.util.text.CharArrayUtil + +class XmlAttributeValueQuotationHandler(private val myStyle: AttributeValueQuotationStyle) : InsertHandler { + + enum class AttributeValueQuotationStyle { QUOTES, BRACES, EQUAL_ONLY } + + companion object { + @JvmField + val QUOTES: XmlAttributeValueQuotationHandler = XmlAttributeValueQuotationHandler(AttributeValueQuotationStyle.QUOTES) + @JvmField + val BRACES: XmlAttributeValueQuotationHandler = XmlAttributeValueQuotationHandler(AttributeValueQuotationStyle.BRACES) + @JvmField + val EQUAL_ONLY: XmlAttributeValueQuotationHandler = XmlAttributeValueQuotationHandler(AttributeValueQuotationStyle.EQUAL_ONLY) + } + + override fun handleInsert(context: InsertionContext, item: LookupElement) { + val editor = context.editor + val document = editor.document + val caretOffset = editor.caretModel.offset + + val chars = document.charsSequence + val hasQuotes = CharArrayUtil.regionMatches(chars, caretOffset, "=\"") || + CharArrayUtil.regionMatches(chars, caretOffset, "='") + val hasBraces = CharArrayUtil.regionMatches(chars, caretOffset, "={") + val hasValue = hasQuotes || hasBraces + + if (!hasValue && CharArrayUtil.regionMatches(chars, caretOffset, "=")) { + document.deleteString(caretOffset, caretOffset + 1) + } + + var effective = myStyle + val insertQuotes = WebEditorOptions.getInstance().isInsertQuotesForAttributeValue + if (!insertQuotes && effective == AttributeValueQuotationStyle.QUOTES) { + effective = AttributeValueQuotationStyle.EQUAL_ONLY + } + + if (!hasValue) { + val toInsert = when (effective) { + AttributeValueQuotationStyle.BRACES -> "={}" + AttributeValueQuotationStyle.QUOTES -> "=\"\"" + AttributeValueQuotationStyle.EQUAL_ONLY -> "=" + } + + if (caretOffset < document.textLength && "/> \n\t\r".indexOf(chars[caretOffset]) < 0) { + document.insertString(caretOffset, "$toInsert ") + } + else { + document.insertString(caretOffset, toInsert) + } + + if ('=' == context.completionChar) { + context.setAddCompletionChar(false) + } + } + + val move = when (effective) { + AttributeValueQuotationStyle.BRACES, AttributeValueQuotationStyle.QUOTES -> 2 + AttributeValueQuotationStyle.EQUAL_ONLY -> 1 + } + editor.caretModel.moveToOffset(caretOffset + move) + if (effective != AttributeValueQuotationStyle.EQUAL_ONLY || hasValue) { + TabOutScopesTracker.getInstance().registerEmptyScopeAtCaret(editor) + } + editor.scrollingModel.scrollToCaret(ScrollType.RELATIVE) + editor.selectionModel.removeSelection() + + if (effective == AttributeValueQuotationStyle.BRACES) { + AutoPopupController.getInstance(context.project).autoPopupParameterInfo(editor, null) + } else { + AutoPopupController.getInstance(context.project).scheduleAutoPopup(editor) + } + } +} diff --git a/xml/impl/src/com/intellij/polySymbols/html/attributes/HtmlAttributeSymbolsCompletionProvider.kt b/xml/impl/src/com/intellij/polySymbols/html/attributes/HtmlAttributeSymbolsCompletionProvider.kt index 74666a2bf792..0c75351a431b 100644 --- a/xml/impl/src/com/intellij/polySymbols/html/attributes/HtmlAttributeSymbolsCompletionProvider.kt +++ b/xml/impl/src/com/intellij/polySymbols/html/attributes/HtmlAttributeSymbolsCompletionProvider.kt @@ -20,7 +20,11 @@ import com.intellij.psi.PsiElement import com.intellij.psi.util.PsiTreeUtil import com.intellij.psi.xml.XmlAttribute -class HtmlAttributeSymbolsCompletionProvider : PolySymbolsCompletionProviderBase() { +import com.intellij.codeInsight.completion.InsertHandler +import com.intellij.codeInsight.lookup.LookupElement +import com.intellij.polySymbols.completion.PolySymbolCodeCompletionItem + +open class HtmlAttributeSymbolsCompletionProvider : PolySymbolsCompletionProviderBase() { override fun getContext(position: PsiElement): XmlAttribute? = PsiTreeUtil.getParentOfType(position, XmlAttribute::class.java) @@ -81,8 +85,9 @@ class HtmlAttributeSymbolsCompletionProvider : PolySymbolsCompletionProviderBase .asSingleSymbol() ?: return@runWithTimeoutOrNull null HtmlAttributeSymbolInfo.create(fullName, freshRegistry, match, insertionContext.file) } - if (info != null && info.acceptsValue && !info.acceptsNoValue) { - XmlAttributeInsertHandler.INSTANCE.handleInsert(insertionContext, lookupItem) + if (info != null && shouldInsertValue(parameters, item, info)) { + val handler = selectInsertHandler(parameters, item, info) + handler.handleInsert(insertionContext, lookupItem) } } ).addToResult(parameters, patchedResultSet) @@ -102,4 +107,22 @@ class HtmlAttributeSymbolsCompletionProvider : PolySymbolsCompletionProviderBase } } + + protected open fun selectInsertHandler( + parameters: CompletionParameters, + item: PolySymbolCodeCompletionItem, + info: HtmlAttributeSymbolInfo + ): InsertHandler { + val framework = HtmlFrameworkSymbolsSupport.get(info.symbol) + return framework.createAttributeInsertHandler(parameters, item, info) ?: XmlAttributeInsertHandler.INSTANCE + } + + protected open fun shouldInsertValue( + parameters: CompletionParameters, + item: PolySymbolCodeCompletionItem, + info: HtmlAttributeSymbolInfo + ): Boolean { + val framework = HtmlFrameworkSymbolsSupport.get(info.symbol) + return framework.shouldInsertAttributeValue(parameters, item, info) + } } \ No newline at end of file diff --git a/xml/xml-psi-impl/src/com/intellij/polySymbols/html/HtmlFrameworkSymbolsSupport.kt b/xml/xml-psi-impl/src/com/intellij/polySymbols/html/HtmlFrameworkSymbolsSupport.kt index 1eb69dbe32e8..b1ecaf482cac 100644 --- a/xml/xml-psi-impl/src/com/intellij/polySymbols/html/HtmlFrameworkSymbolsSupport.kt +++ b/xml/xml-psi-impl/src/com/intellij/polySymbols/html/HtmlFrameworkSymbolsSupport.kt @@ -1,8 +1,12 @@ // Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.polySymbols.html +import com.intellij.codeInsight.completion.CompletionParameters +import com.intellij.codeInsight.completion.InsertHandler +import com.intellij.codeInsight.lookup.LookupElement import com.intellij.polySymbols.PolySymbol import com.intellij.polySymbols.PolySymbolProperty +import com.intellij.polySymbols.completion.PolySymbolCodeCompletionItem import com.intellij.polySymbols.framework.FrameworkId import com.intellij.polySymbols.framework.PolySymbolFramework import com.intellij.polySymbols.html.attributes.HtmlAttributeSymbolDescriptor @@ -22,6 +26,28 @@ interface HtmlFrameworkSymbolsSupport { fun getAttributeNameCodeCompletionFilter(tag: XmlTag): Predicate = Predicate { true } + /** + * Creates an insert handler for the given attribute completion item. + * Override this method to provide framework-specific insert handlers (e.g., braces for JSX-like syntax). + * + * Default implementation returns null, which will use the standard XML attribute insert handler. + */ + fun createAttributeInsertHandler( + parameters: CompletionParameters, + item: PolySymbolCodeCompletionItem, + info: HtmlAttributeSymbolInfo + ): InsertHandler? = null + + /** + * Determines whether to insert a value (quotes/braces) after the attribute name. + * Override this method to customize when values should be inserted (e.g., always insert for directives). + */ + fun shouldInsertAttributeValue( + parameters: CompletionParameters, + item: PolySymbolCodeCompletionItem, + info: HtmlAttributeSymbolInfo + ): Boolean = info.acceptsValue && !info.acceptsNoValue + companion object { /** * Provides id of the Symbol's framework, e.g. vue, angular, react, etc.